|
|
# 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, ¶m);
|
|
|
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)
|
|
|
|