gitbook/趣谈Linux操作系统/docs/100576.md
2022-09-03 22:05:03 +08:00

626 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 33 | 字符设备(下):如何建立直销模式?
上一节,我们讲了一个设备能够被打开、能够读写,主流的功能基本就完成了。我们讲输入输出设备的时候说到,如果一个设备有事情需要通知操作系统,会通过中断和设备驱动程序进行交互,今天我们就来解析中断处理机制。
鼠标就是通过中断,将自己的位置和按键信息,传递给设备驱动程序。
```
static int logibm_open(struct input_dev *dev)
{
if (request_irq(logibm_irq, logibm_interrupt, 0, "logibm", NULL)) {
printk(KERN_ERR "logibm.c: Can't allocate irq %d\n", logibm_irq);
return -EBUSY;
}
outb(LOGIBM_ENABLE_IRQ, LOGIBM_CONTROL_PORT);
return 0;
}
static irqreturn_t logibm_interrupt(int irq, void *dev_id)
{
char dx, dy;
unsigned char buttons;
outb(LOGIBM_READ_X_LOW, LOGIBM_CONTROL_PORT);
dx = (inb(LOGIBM_DATA_PORT) & 0xf);
outb(LOGIBM_READ_X_HIGH, LOGIBM_CONTROL_PORT);
dx |= (inb(LOGIBM_DATA_PORT) & 0xf) << 4;
outb(LOGIBM_READ_Y_LOW, LOGIBM_CONTROL_PORT);
dy = (inb(LOGIBM_DATA_PORT) & 0xf);
outb(LOGIBM_READ_Y_HIGH, LOGIBM_CONTROL_PORT);
buttons = inb(LOGIBM_DATA_PORT);
dy |= (buttons & 0xf) << 4;
buttons = ~buttons >> 5;
input_report_rel(logibm_dev, REL_X, dx);
input_report_rel(logibm_dev, REL_Y, dy);
input_report_key(logibm_dev, BTN_RIGHT, buttons & 1);
input_report_key(logibm_dev, BTN_MIDDLE, buttons & 2);
input_report_key(logibm_dev, BTN_LEFT, buttons & 4);
input_sync(logibm_dev);
outb(LOGIBM_ENABLE_IRQ, LOGIBM_CONTROL_PORT);
return IRQ_HANDLED
```
要处理中断,需要有一个中断处理函数。定义如下:
```
irqreturn_t (*irq_handler_t)(int irq, void * dev_id);
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device or was not handled
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
```
其中,irq是一个整数,是中断信号。dev\_id是一个void \*的通用指针,主要用于区分同一个中断处理函数对于不同设备的处理。
这里的返回值有三种:IRQ\_NONE表示不是我的中断,不归我管;IRQ\_HANDLED表示处理完了的中断;IRQ\_WAKE\_THREAD表示有一个进程正在等待这个中断,中断处理完了,应该唤醒它。
上面的例子中,logibm\_interrupt这个中断处理函数,先是获取了x和y的移动坐标,以及左中右的按键,上报上去,然后返回IRQ\_HANDLED,这表示处理完毕。
其实,写一个真正生产用的中断处理程序还是很复杂的。当一个中断信号A触发后,正在处理的过程中,这个中断信号A是应该暂时关闭的,这样是为了防止再来一个中断信号A,在当前的中断信号A的处理过程中插一杠子。但是,这个暂时关闭的时间应该多长呢?
如果太短了,应该原子化处理完毕的没有处理完毕,又被另一个中断信号A中断了,很多操作就不正确了;如果太长了,一直关闭着,新的中断信号A进不来,系统就显得很慢。所以,很多中断处理程序将整个中断要做的事情分成两部分,称为上半部和下半部,或者成为关键处理部分和延迟处理部分。在中断处理函数中,仅仅处理关键部分,完成了就将中断信号打开,使得新的中断可以进来,需要比较长时间处理的部分,也即延迟部分,往往通过工作队列等方式慢慢处理。
这个写起来可以是一本书了,推荐你好好读一读《Linux Device Drivers》这本书,这里我就不详细介绍了。
有了中断处理函数,接下来要调用request\_irq来注册这个中断处理函数。request\_irq有这样几个参数:
* unsigned int irq是中断信号;
* irq\_handler\_t handler是中断处理函数;
* unsigned long flags是一些标识位;
* const char \*name是设备名称;
* void \*dev这个通用指针应该和中断处理函数的void \*dev相对应。
```
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
```
中断处理函数被注册到哪里去呢?让我们沿着request\_irq看下去。request\_irq调用的是request\_threaded\_irq。代码如下:
```
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
......
desc = irq_to_desc(irq);
......
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
......
retval = __setup_irq(irq, desc, action);
......
}
```
对于每一个中断,都有一个对中断的描述结构struct irq\_desc。它有一个重要的成员变量是struct irqaction,用于表示处理这个中断的动作。如果我们仔细看这个结构,会发现,它里面有next指针,也就是说,这是一个链表,对于这个中断的所有处理动作,都串在这个链表上。
```
struct irq_desc {
......
struct irqaction *action; /* IRQ action list */
......
struct module *owner;
const char *name;
};
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @name: name of the device
* @dev_id: cookie to identify the device
* @percpu_dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @flags: flags (see IRQF_* above)
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @secondary: pointer to secondary irqaction (force threading)
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
* @dir: pointer to the proc/irq/NN/name entry
*/
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
};
```
每一个中断处理动作的结构struct irqaction,都有以下成员:
* 中断处理函数handler;
* void \*dev\_id为设备id;
* irq为中断信号;
* 如果中断处理函数在单独的线程运行,则有thread\_fn是线程的执行函数,thread是线程的task\_struct。
在request\_threaded\_irq函数中,irq\_to\_desc根据中断信号查找中断描述结构。如何查找呢?这就要区分情况。一般情况下,所有的struct irq\_desc都放在一个数组里面,我们直接按下标查找就可以了。如果配置了CONFIG\_SPARSE\_IRQ,那中断号是不连续的,就不适合用数组保存了,
我们可以放在一棵基数树上。我们不是第一次遇到这个数据结构了。这种结构对于从某个整型key找到value速度很快,中断信号irq是这个整数。通过它,我们很快就能定位到对应的struct irq\_desc。
```
#ifdef CONFIG_SPARSE_IRQ
static RADIX_TREE(irq_desc_tree, GFP_KERNEL);
struct irq_desc *irq_to_desc(unsigned int irq)
{
return radix_tree_lookup(&irq_desc_tree, irq);
}
#else /* !CONFIG_SPARSE_IRQ */
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
}
};
struct irq_desc *irq_to_desc(unsigned int irq)
{
return (irq < NR_IRQS) ? irq_desc + irq : NULL;
}
#endif /* !CONFIG_SPARSE_IRQ */
```
为什么中断信号会有稀疏,也就是不连续的情况呢?这里需要说明一下,这里的irq并不是真正的、物理的中断信号,而是一个抽象的、虚拟的中断信号。因为物理的中断信号和硬件关联比较大,中断控制器也是各种各样的。
作为内核,我们不可能写程序的时候,适配各种各样的硬件中断控制器,因而就需要有一层中断抽象层。这里虚拟中断信号到中断描述结构的映射,就是抽象中断层的主要逻辑。
下面我们讲真正中断响应的时候,会涉及物理中断信号。可以想象,如果只有一个CPU,一个中断控制器,则基本能够保证从物理中断信号到虚拟中断信号的映射是线性的,这样用数组表示就没啥问题,但是如果有多个CPU,多个中断控制器,每个中断控制器各有各的物理中断信号,就没办法保证虚拟中断信号是连续的,所以就要用到基数树了。
接下来,request\_threaded\_irq函数分配了一个struct irqaction,并且初始化它,接着调用\_\_setup\_irq。在这个函数里面,如果struct irq\_desc里面已经有struct irqaction了,我们就将新的struct irqaction挂在链表的末端。如果设定了以单独的线程运行中断处理函数,setup\_irq\_thread就会创建这个内核线程,wake\_up\_process会唤醒它。
```
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
struct irqaction *old, **old_ptr;
unsigned long flags, thread_mask = 0;
int ret, nested, shared = 0;
......
new->irq = irq;
......
/*
* Create a handler thread when a thread function is supplied
* and the interrupt does not nest into another interrupt
* thread.
*/
if (new->thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
}
......
old_ptr = &desc->action;
old = *old_ptr;
if (old) {
/* add new interrupt at end of irq queue */
do {
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old);
}
......
*old_ptr = new;
......
if (new->thread)
wake_up_process(new->thread);
......
}
static int
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
struct task_struct *t;
struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO/2,
};
t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
sched_setscheduler_nocheck(t, SCHED_FIFO, &param);
get_task_struct(t);
new->thread = t;
......
return 0;
```
至此为止,request\_irq完成了它的使命。总结来说,它就是根据中断信号irq,找到基数树上对应的irq\_desc,然后将新的irqaction挂在链表上。
接下来,我们就来看,真正中断来了的时候,会发生一些什么。
真正中断的发生还是要从硬件开始。这里面有四个层次。
* 第一个层次是外部设备给中断控制器发送物理中断信号。
* 第二个层次是中断控制器将物理中断信号转换成为中断向量interrupt vector,发给各个CPU。
* 第三个层次是每个CPU都会有一个中断向量表,根据interrupt vector调用一个IRQ处理函数。注意这里的IRQ处理函数还不是咱们上面指定的irq\_handler\_t,到这一层还是CPU硬件的要求。
* 第四个层次是在IRQ处理函数中,将interrupt vector转化为抽象中断层的中断信号irq,调用中断信号irq对应的中断描述结构里面的irq\_handler\_t。
![](https://static001.geekbang.org/resource/image/dd/13/dd492efdcf956cb22ce3d51592cdc113.png)
在这里,我们不解析硬件的部分,我们从CPU收到中断向量开始分析。
CPU收到的中断向量是什么样的呢?这个定义在文件arch/x86/include/asm/irq\_vectors.h中。这里面的注释非常好,建议你仔细阅读。
```
/*
* Linux IRQ vector layout.
*
* There are 256 IDT entries (per CPU - each entry is 8 bytes) which can
* be defined by Linux. They are used as a jump table by the CPU when a
* given vector is triggered - by a CPU-external, CPU-internal or
* software-triggered event.
*
* Linux sets the kernel code address each entry jumps to early during
* bootup, and never changes them. This is the general layout of the
* IDT entries:
*
* Vectors 0 ... 31 : system traps and exceptions - hardcoded events
* Vectors 32 ... 127 : device interrupts
* Vector 128 : legacy int80 syscall interface
* Vectors 129 ... INVALIDATE_TLB_VECTOR_START-1 except 204 : device interrupts
* Vectors INVALIDATE_TLB_VECTOR_START ... 255 : special interrupts
*
* 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
*
* This file enumerates the exact layout of them:
*/
#define FIRST_EXTERNAL_VECTOR 0x20
#define IA32_SYSCALL_VECTOR 0x80
#define NR_VECTORS 256
#define FIRST_SYSTEM_VECTOR NR_VECTORS
```
通过这些注释,我们可以看出,CPU能够处理的中断总共256个,用宏NR\_VECTOR或者FIRST\_SYSTEM\_VECTOR表示。
为了处理中断,CPU硬件要求每一个CPU都有一个中断向量表,通过load\_idt加载,里面记录着每一个中断对应的处理方法,这个中断向量表定义在文件arch/x86/kernel/traps.c中。
```
gate_desc idt_table[NR_VECTORS] __page_aligned_bss;
```
对于一个CPU可以处理的中断被分为几个部分,第一部分0到31的前32位是系统陷入或者系统异常,这些错误无法屏蔽,一定要处理。
这些中断的处理函数在系统初始化的时候,在start\_kernel函数中调用过trap\_init()。这个咱们讲系统初始化和系统调用的时候,都大概讲过这个函数,这里还需要仔细看一下。
```
void __init trap_init(void)
{
int i;
...
set_intr_gate(X86_TRAP_DE, divide_error);
//各种各样的set_intr_gate,不都贴在这里了,只贴一头一尾
...
set_intr_gate(X86_TRAP_XF, simd_coprocessor_error);
/* Reserve all the builtin and the syscall vector: */
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors);
#ifdef CONFIG_X86_32
set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
/*
* Set the IDT descriptor to a fixed read-only location, so that the
* "sidt" instruction will not leak the location of the kernel, and
* to defend the IDT against arbitrary memory write vulnerabilities.
* It will be reloaded in cpu_init() */
__set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);
idt_descr.address = fix_to_virt(FIX_RO_IDT);
......
```
我这里贴的代码省略了很多,在trap\_init函数的一开始,调用了大量的set\_intr\_gate,最终都会调用\_set\_gate,代码如下:
```
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
write_idt_entry(idt_table, gate, &s);
}
```
从代码可以看出,set\_intr\_gate其实就是将每个中断都设置了中断处理函数,放在中断向量表idt\_table中。
在trap\_init中,由于set\_intr\_gate调用的太多,容易让人眼花缭乱。其实arch/x86/include/asm/traps.h文件中,早就定义好了前32个中断。如果仔细对比一下,你会发现,这些都在trap\_init中使用set\_intr\_gate设置过了。
```
/* Interrupts/Exceptions */
enum {
X86_TRAP_DE = 0, /* 0, Divide-by-zero */
X86_TRAP_DB, /* 1, Debug */
X86_TRAP_NMI, /* 2, Non-maskable Interrupt */
X86_TRAP_BP, /* 3, Breakpoint */
X86_TRAP_OF, /* 4, Overflow */
X86_TRAP_BR, /* 5, Bound Range Exceeded */
X86_TRAP_UD, /* 6, Invalid Opcode */
X86_TRAP_NM, /* 7, Device Not Available */
X86_TRAP_DF, /* 8, Double Fault */
X86_TRAP_OLD_MF, /* 9, Coprocessor Segment Overrun */
X86_TRAP_TS, /* 10, Invalid TSS */
X86_TRAP_NP, /* 11, Segment Not Present */
X86_TRAP_SS, /* 12, Stack Segment Fault */
X86_TRAP_GP, /* 13, General Protection Fault */
X86_TRAP_PF, /* 14, Page Fault */
X86_TRAP_SPURIOUS, /* 15, Spurious Interrupt */
X86_TRAP_MF, /* 16, x87 Floating-Point Exception */
X86_TRAP_AC, /* 17, Alignment Check */
X86_TRAP_MC, /* 18, Machine Check */
X86_TRAP_XF, /* 19, SIMD Floating-Point Exception */
X86_TRAP_IRET = 32, /* 32, IRET Exception */
};
```
我们回到trap\_init中,当前32个中断都用set\_intr\_gate设置完毕。在中断向量表idt\_table中填完了之后,接下来的for循环,for (i = 0; i < FIRST\_EXTERNAL\_VECTOR; i++),将前32个中断都在used\_vectors中标记为1,表示这些都设置过中断处理函数了。
接下来,trap\_init单独调用set\_intr\_gate来设置32位系统调用的中断。IA32\_SYSCALL\_VECTOR,也即128,单独将used\_vectors中的第128位标记为1。
在trap\_init的最后,我们将idt\_table放在一个固定的虚拟地址上。trap\_init结束后,中断向量表中已经填好了前32位,外加一位32位系统调用,其他的都是用于设备中断。
在start\_kernel调用完毕trap\_init之后,还会调用init\_IRQ()来初始化其他的设备中断,最终会调用到native\_init\_IRQ。
```
void __init native_init_IRQ(void)
{
int i;
i = FIRST_EXTERNAL_VECTOR;
#ifndef CONFIG_X86_LOCAL_APIC
#define first_system_vector NR_VECTORS
#endif
for_each_clear_bit_from(i, used_vectors, first_system_vector) {
/* IA32_SYSCALL_VECTOR could be used in trap_init already. */
set_intr_gate(i, irq_entries_start +
8 * (i - FIRST_EXTERNAL_VECTOR));
}
......
}
```
这里面从第32个中断开始,到最后NR\_VECTORS为止,对于used\_vectors中没有标记为1的位置,都会调用set\_intr\_gate设置中断向量表。
其实used\_vectors中没有标记为1的,都是设备中断的部分。
也即所有的设备中断的中断处理函数,在中断向量表里面都会设置为从irq\_entries\_start开始,偏移量为i - FIRST\_EXTERNAL\_VECTOR的一项。
看来中断处理函数是定义在irq\_entries\_start这个表里面的,我们在arch\\x86\\entry\\entry\_32.S和arch\\x86\\entry\\entry\_64.S都能找到这个函数表的定义。
这又是汇编语言,不需要完全看懂,但是我们还是能看出来,这里面定义了FIRST\_SYSTEM\_VECTOR - FIRST\_EXTERNAL\_VECTOR项。每一项都是中断处理函数,会跳到common\_interrupt去执行。这里会最终调用do\_IRQ,调用完毕后,就从中断返回。这里我们需要区分返回用户态还是内核态。这里会有一个机会触发抢占,咱们讲进程切换的时候讲过的。
```
ENTRY(irq_entries_start)
vector=FIRST_EXTERNAL_VECTOR
.rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
pushl $(~vector+0x80) /* Note: always in signed byte range */
vector=vector+1
jmp common_interrupt /* 会调用到do_IRQ */
.align 8
.endr
END(irq_entries_start)
common_interrupt:
ASM_CLAC
addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */
interrupt do_IRQ
/* 0(%rsp): old RSP */
ret_from_intr:
......
/* Interrupt came from user space */
GLOBAL(retint_user)
......
/* Returning to kernel space */
retint_kernel:
......
```
这样任何一个中断向量到达任何一个CPU,最终都会走到do\_IRQ。我们来看do\_IRQ的实现。
```
/*
* do_IRQ handles all normal device IRQ's (the special
* SMP cross-CPU interrupts have their own specific
* handlers).
*/
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc * desc;
/* high bit used in ret_from_ code */
unsigned vector = ~regs->orig_ax;
......
desc = __this_cpu_read(vector_irq[vector]);
if (!handle_irq(desc, regs)) {
......
}
......
set_irq_regs(old_regs);
return 1;
}
```
在这里面,从AX寄存器里面拿到了中断向量vector,但是别忘了中断控制器发送给每个CPU的中断向量都是每个CPU局部的,而抽象中断处理层的虚拟中断信号irq以及它对应的中断描述结构irq\_desc是全局的,也即这个CPU的200号的中断向量和另一个CPU的200号中断向量对应的虚拟中断信号irq和中断描述结构irq\_desc可能不一样,这就需要一个映射关系。这个映射关系放在Per CPU变量vector\_irq里面。
```
DECLARE_PER_CPU(vector_irq_t, vector_irq);
```
在系统初始化的时候,我们会调用\_\_assign\_irq\_vector,将虚拟中断信号irq分配到某个CPU上的中断向量。
```
static int __assign_irq_vector(int irq, struct apic_chip_data *d,
const struct cpumask *mask,
struct irq_data *irqdata)
{
static int current_vector = FIRST_EXTERNAL_VECTOR + VECTOR_OFFSET_START;
static int current_offset = VECTOR_OFFSET_START % 16;
int cpu, vector;
......
while (cpu < nr_cpu_ids) {
int new_cpu, offset;
......
vector = current_vector;
offset = current_offset;
next:
vector += 16;
if (vector >= first_system_vector) {
offset = (offset + 1) % 16;
vector = FIRST_EXTERNAL_VECTOR + offset;
}
/* If the search wrapped around, try the next cpu */
if (unlikely(current_vector == vector))
goto next_cpu;
if (test_bit(vector, used_vectors))
goto next;
......
/* Found one! */
current_vector = vector;
current_offset = offset;
/* Schedule the old vector for cleanup on all cpus */
if (d->cfg.vector)
cpumask_copy(d->old_domain, d->domain);
for_each_cpu(new_cpu, vector_searchmask)
per_cpu(vector_irq, new_cpu)[vector] = irq_to_desc(irq);
goto update;
next_cpu:
cpumask_or(searched_cpumask, searched_cpumask, vector_cpumask);
cpumask_andnot(vector_cpumask, mask, searched_cpumask);
cpu = cpumask_first_and(vector_cpumask, cpu_online_mask);
continue;
}
....
```
在这里,一旦找到某个向量,就将CPU的此向量对应的向量描述结构irq\_desc,设置为虚拟中断信号irq对应的向量描述结构irq\_to\_desc(irq)。
这样do\_IRQ会根据中断向量vector得到对应的irq\_desc,然后调用handle\_irq。handle\_irq会调用generic\_handle\_irq\_desc,里面调用irq\_desc的handle\_irq。
```
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}
```
这里的handle\_irq,最终会调用\_\_handle\_irq\_event\_percpu。代码如下:
```
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;
record_irq_time(desc);
for_each_action_of_desc(desc, action) {
irqreturn_t res;
res = action->handler(irq, action->dev_id);
switch (res) {
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action);
case IRQ_HANDLED:
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
```
\_\_handle\_irq\_event\_percpu里面调用了irq\_desc里每个hander,这些hander是我们在所有action列表中注册的,这才是我们设置的那个中断处理函数。如果返回值是IRQ\_HANDLED,就说明处理完毕;如果返回值是IRQ\_WAKE\_THREAD就唤醒线程。
至此,中断的整个过程就结束了。
## 总结时刻
这一节,我们讲了中断的整个处理过程。中断是从外部设备发起的,会形成外部中断。外部中断会到达中断控制器,中断控制器会发送中断向量Interrupt Vector给CPU。
对于每一个CPU,都要求有一个idt\_table,里面存放了不同的中断向量的处理函数。中断向量表中已经填好了前32位,外加一位32位系统调用,其他的都是用于设备中断。
硬件中断的处理函数是do\_IRQ进行统一处理,在这里会让中断向量,通过vector\_irq映射为irq\_desc。
irq\_desc是一个用于描述用户注册的中断处理函数的结构,为了能够根据中断向量得到irq\_desc结构,会把这些结构放在一个基数树里面,方便查找。
irq\_desc里面有一个成员是irqaction,指向设备驱动程序里面注册的中断处理函数。
![](https://static001.geekbang.org/resource/image/26/8f/26bde4fa2279f66098856c5b2b6d308f.png)
## 课堂练习
你知道如何查看每个CPU都收到了哪些中断吗?
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)