<audio id="audio" title="33 | 字符设备（下）：如何建立直销模式？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/51/33307f82ecdb7940a6a0003abd72ca51.mp3"></audio>

上一节，我们讲了一个设备能够被打开、能够读写，主流的功能基本就完成了。我们讲输入输出设备的时候说到，如果一个设备有事情需要通知操作系统，会通过中断和设备驱动程序进行交互，今天我们就来解析中断处理机制。

鼠标就是通过中断，将自己的位置和按键信息，传递给设备驱动程序。

```
static int logibm_open(struct input_dev *dev)
{
	if (request_irq(logibm_irq, logibm_interrupt, 0, &quot;logibm&quot;, NULL)) {
		printk(KERN_ERR &quot;logibm.c: Can't allocate irq %d\n&quot;, 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) &amp; 0xf);
	outb(LOGIBM_READ_X_HIGH, LOGIBM_CONTROL_PORT);
	dx |= (inb(LOGIBM_DATA_PORT) &amp; 0xf) &lt;&lt; 4;
	outb(LOGIBM_READ_Y_LOW, LOGIBM_CONTROL_PORT);
	dy = (inb(LOGIBM_DATA_PORT) &amp; 0xf);
	outb(LOGIBM_READ_Y_HIGH, LOGIBM_CONTROL_PORT);
	buttons = inb(LOGIBM_DATA_PORT);
	dy |= (buttons &amp; 0xf) &lt;&lt; 4;
	buttons = ~buttons &gt;&gt; 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 &amp; 1);
	input_report_key(logibm_dev, BTN_MIDDLE, buttons &amp; 2);
	input_report_key(logibm_dev, BTN_LEFT,   buttons &amp; 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 &lt;&lt; 0),
	IRQ_HANDLED		= (1 &lt;&lt; 0),
	IRQ_WAKE_THREAD		= (1 &lt;&lt; 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-&gt;handler = handler;
	action-&gt;thread_fn = thread_fn;
	action-&gt;flags = irqflags;
	action-&gt;name = devname;
	action-&gt;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(&amp;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 &lt; 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-&gt;irq = irq;
......
	/*
	 * Create a handler thread when a thread function is supplied
	 * and the interrupt does not nest into another interrupt
	 * thread.
	 */
	if (new-&gt;thread_fn &amp;&amp; !nested) {
		ret = setup_irq_thread(new, irq, false);
	}
......
	old_ptr = &amp;desc-&gt;action;
	old = *old_ptr;
	if (old) {
		/* add new interrupt at end of irq queue */
		do {
			thread_mask |= old-&gt;thread_mask;
			old_ptr = &amp;old-&gt;next;
			old = *old_ptr;
		} while (old);
	}
......
	*old_ptr = new;
......
	if (new-&gt;thread)
		wake_up_process(new-&gt;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, &quot;irq/%d-%s&quot;, irq, new-&gt;name);
	sched_setscheduler_nocheck(t, SCHED_FIFO, &amp;param);
	get_task_struct(t);
	new-&gt;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。

<img src="https://static001.geekbang.org/resource/image/dd/13/dd492efdcf956cb22ce3d51592cdc113.png" alt="">

在这里，我们不解析硬件的部分，我们从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 &lt; 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
	 * &quot;sidt&quot; 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(&amp;s, type, (unsigned long)addr, dpl, ist, seg);
	write_idt_entry(idt_table, gate, &amp;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 &lt; 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-&gt;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 &lt; nr_cpu_ids) {
		int new_cpu, offset;
......
		vector = current_vector;
		offset = current_offset;
next:
		vector += 16;
		if (vector &gt;= 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-&gt;cfg.vector)
			cpumask_copy(d-&gt;old_domain, d-&gt;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-&gt;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-&gt;irq_data.irq;
	struct irqaction *action;


	record_irq_time(desc);


	for_each_action_of_desc(desc, action) {
		irqreturn_t res;
		res = action-&gt;handler(irq, action-&gt;dev_id);
		switch (res) {
		case IRQ_WAKE_THREAD:
			__irq_wake_thread(desc, action);
		case IRQ_HANDLED:
			*flags |= action-&gt;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，指向设备驱动程序里面注册的中断处理函数。

<img src="https://static001.geekbang.org/resource/image/26/8f/26bde4fa2279f66098856c5b2b6d308f.png" alt="">

## 课堂练习

你知道如何查看每个CPU都收到了哪些中断吗？

欢迎留言和我分享你的疑惑和见解 ，也欢迎可以收藏本节内容，反复研读。你也可以把今天的内容分享给你的朋友，和他一起学习和进步。

<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
