一、中断
中断使得硬件得以发出通知给处理器。中断随时都可以产生,如键盘敲击就会触发中断,通知操作系统有按键按下。
不同设备对应的中断不同,而每个中断都通过一个唯一的数字标识。这些中断值通常被称为中断请求(IRQ)线。每个 IRQ 线都会关联一个数值量。
异常与中断不同,它在产生时必须考虑与处理器时钟同步,异常也常常被称为同步中断。在处理器执行到错误指令时候(如除数为0),或者是在执行期间出现特殊情况(如缺页),这些异常需要通过内核来处理,处理器就会产生一个异常。中断还可以通过软中断实现系统调用。
二、中断处理程序
在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler) 或中断服务例程(interrupt service routine,ISR)。每种类型的中断都有一个相应的中断处理程序。一个设备的中断处理程序是它设备驱动程序(driver)的一部分——设备驱动程序是用于对设备进行管理的内核代码。
中断处理程序与其他内核函数的真正区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。
中断可能随时发生,因此中断处理程序也就随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快地恢复中断代码的执行。
一般把中断处理切位两个部分:中断处理程序是上半部(top half)——接收到一个中断,上半部立刻开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。
三、注册中断处理程序
中断处理程序是管理硬件的驱动程序的组成部分。如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。
驱动程序可以通过 request_irq() 函数注册一个中断处理程序(声明在 <linux/interrupt.h>),并且激活给定的中断线,以处理中断:
第一个参数 irq 表示要分配的中断号。
第二个参数 handler 是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。
注意 handler 函数的原型,它接受两个参数,并有一个类型为 irqreturn_t 的返回值。
第三个参数 flags 可以为 0,也可能是下列一个或多个标志的位掩码。定义在 <linux/interrupt.h>。其中最重要的几个标志是:
- IRQF_DISABLED——该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其他中断。多数中断处理程序是不会设置该位,这种用法留给希望快速执行的轻量级中断。
- IRQF_TIMER——该标志是特别为系统定时器的中断处理而准备的。
- IRQF_SHARED——此标志标明可以在多个中断处理程序之间共享中断线。
第四个参数 name 是与中断相关的设备的 ASCII 文本表示
第五个参数 dev 用于共享中断线。当一个中断处理程序需要释放时,dev 将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果无需共享中断线则设置为 NULL 即可。内核每次调用中断处理程序时,都会把这个指针传递给它。实践中往往会通过它来传递驱动程序的设备结构。
request_irq() 函数成功执行会返回 0,非 0 值则代表有错误发生。
request_irq() 函数可能会睡眠,因此不能在中断上下文或其他不允许阻塞的代码中调用该函数。因为 kmalloc() 是可睡眠的。
四、卸载中断处理程序
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。上述动作需要调用:
void free_irq(unsigned int irq, void *dev)
如果指定的中断线不是共享的,则删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则删除 dev 所对应的处理程序,并不禁用中断线。
五、编写中断处理程序
以下是一个中断处理程序声明:
static irqreturn_t intr_handler(int irq, void *dev)
中断处理程序的返回值为 irqreturn_t。中断处理程序可能返回两个特殊的值:IRQ_NONE 和 IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源的时候,返回 IRQ_NONE。反之则返回 IRQ_HANDLED。
Linux 的中断处理程序是无需重入的。同一个中断处理程序绝不会被同时调用以处理嵌套的中断。
共享的处理程序的特点有:
- request_irq() 的参数 flags 必须设置 IRQF_SHARED 标志。
- 对于每个注册的中断处理程序来说,dev 参数必须唯一。指向任一设备结构的指针就是唯一的。
- 中断处理程序必须能够区分它的设备是否真的产生了中断。
内核在接收一个中断后,它将依次调用在该中断线上注册的共享的处理程序,所以,一个处理程序必须知道它是否应该为这个中断负责,如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。
六、中断上下文
当执行一个中断处理程序的时候,内核处于中断上下文(interrupt context)中。
进程上下文是一种内核所处的操作模式,此时内核代表进程执行。进程上下文可以睡眠,也可以调用调度程序,因为进程有 task_struct 结构,当进程再次被调度时能恢复进程执行环境。
中断上下文和进程没有什么关联,并且中断上下文是不可睡眠的,因为中断上下文没有某种结构记录它的执行状态,一旦睡眠就无法再被重新唤起了(没有东西来恢复它的执行环境),所以在中断上下文中不可使用信号量,因为信号量会导致睡眠。因为中断打断了其他代码的执行,所以中断上下文的代码应该简洁、迅速,尽量把工作从中断处理程序中分离出来,放到下半部执行。
procfs 是一个虚拟文件系统,它只存在于内核内存,一般安装于 /proc 目录。在 procfs 中读写文件都要调用内核函数。/proc/interrupt 文件存放系统中与中断相关的统计信息。
通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。
禁止当前处理器上的本地中断,随后又激活它们的语句为:
local_irq_disable();
/* 禁止中断 */
local_irq_enable();
x86 上这两个函数是通过单个汇编指令实现的,cli 指令和 sti 指令。
但是上述用法并不安全,万一在调用 local_irq_disable() 之前中断就是关闭的,之后再调用 local_irq_enable()相当于无条件把中断打开了,所以为了更安全的关闭中断,我们使用如下方式:
unsigned long flags;
local_irq_save(flags); /* 禁止中断 */
local_irq_restore(flags)l /* 中断恢复到原来的状态 */