0%

《趣谈linux操作系统》小结(二十八) - 字符设备中断

字符设备中断

前面在学习文件读写的时候,如果是读取设备上的内容, 那么在读取完毕的时候,设备会发送一个中断告知CPU内容读取完毕。同样的,字符设备也是有中断来通知CPU处理事件, 比如鼠标设备就是通过中断来告知系统自己的位置和按键信息,传递给驱动程序。

注册中断处理函数

处理中断,需要一个中断的处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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){
...
}

鼠标的中断处理函数就是logibm_interrupt。通过request_irq函数注册中断处理函数。

中断处理函数的返回值表示中断处理的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


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),
};

这里使用void指针参数, 可以达到同一个函数处理不同设备的中断事件,根据中断号可以区分是什么设备,然后强转为特定设备的结构,这种实现是很普遍的用在通过流程的入口函数的。

这里的返回值有三种:IRQ_NONE 表示不是我的中断,不归我管;IRQ_HANDLED 表示处理完了的中断;IRQ_WAKE_THREAD 表示有一个进程正在等待这个中断,中断处理完了,应该唤醒它。

鼠标的中断处理相对比较简单,没有耗时长的操作,整体也不是很复杂。在中断处理的过程中,这个中断信号是被关闭的, 如果连续来几个相同的中断,后续的中断可能不被即时响应,显得系统很慢。对于其他复杂的中断,可能需要分成关键处理部分和延迟处理部分,在中断处理函数中,仅仅处理关键部分,完成了就将中断信号打开,使得新的中断可以进来,需要比较长时间处理的部分,也即延迟部分,往往通过工作队列等方式慢慢处理。

request_irq函数注册中断处理函数。对于每一个中断,都有一个对中断的描述结构 struct irq_desc。它有一个重要的成员变量是 struct irqaction,用于表示处理这个中断的动作。处理动作可以有多个,通过 irqaction的next串起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

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;
};

这个desc结构可以通过中断号调用irq_to_desc函数来查找。当然这里的中断号也只是一个内核逻辑的中断编号,不同的设备实际发出的中断号可能不一样,这个有一个中断控制器控制。

request_threaded_irq 函数分配了一个 struct irqaction,并且初始化它,接着调用 __setup_irq。在这个函数里面,如果 struct irq_desc 里面已经有 struct irqaction 了,我们就将新的 struct irqaction 挂在链表的末端。如果设定了以单独的线程运行中断处理函数,setup_irq_thread 就会创建这个内核线程,wake_up_process 会唤醒它。

注册处理函数其实也不复杂,只是查找一下中断描述符,然后建立action链表。

触发中断处理

中断触发有几个阶段

  • 外部设备给中断控制器发送物理中断信号
  • 中断控制器将物理中断信号转换成为中断向量 interrupt vector,发给各个 CPU。
  • 每个 CPU 都会有一个中断向量表,根据 interrupt vector 调用一个 IRQ 处理函数。
  • 在 IRQ 处理函数中,将 interrupt vector 转化为抽象中断层的中断信号 irq,调用中断信号 irq 对应的中断描述结构里面的 irq_handler_t。

CPU 能够处理的中断总共 256 个,用宏 NR_VECTOR 或者 FIRST_SYSTEM_VECTOR 表示。

CPU 硬件要求每一个 CPU 都有一个中断向量表,通过 load_idt 加载,里面记录着每一个中断对应的处理方法,这个中断向量表定义在文件 arch/x86/kernel/traps.c 中。

1
gate_desc idt_table[NR_VECTORS] __page_aligned_bss;

0-31是系统异常的中断,在内核初始化的时候 trap_init函数设置了中断函数。在trap_init还设置了127系统调用的中断处理函数。同时给used_vectors全局变量对应的位置位。其他设备的中断初始化调用的是init_IRQ函数。

任何硬件中断向量处理最终都会调用到do_IRQ函数。do_IRQ就通过每个CPU对应的中断向量表获取最终全局的中断desc,然后调用对应的处理函数进行处理。

图片替换文本

行动,才不会被动!

欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。