向下之旅(九):中断和中断处理程序(二)
中断上下文
前面所说的进程上下文是一种内核所处的操作模式,此时内核代表进程执行——例如,执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程,此外进程是以进程上下文的形式连接到内核中的,因此在进程上下文中可以睡眠,也可以调用调度程序。
中断上下文与进程没有什么关系,所以中断上下文不可以睡眠,因此不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在你的中断处理程序中使用它。
中断上下文有严格的时间限制,有一点非常重要,中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线上的另一中断处理程序)。所以所有的中断处理程序必须尽可能的迅速,简洁。尽量把工作从中断处理程序中分离出来,放在下半部执行。因为下半部可以再更适合的时间运行。
早期的Linux,中断处理程序并不没有自己的栈,而是共享所中断进程的内核栈。内核栈的大小是两页,即在32位的体系结构中是8KB,在64位的体系结构中是16KB。再2.6早起的内核中,增加了一个选项,把栈的大小从两页减到了一页。这就减轻了内存的压力,为了应对栈大小的减少,中断处理程序拥有了自己的栈,每一个处理器一个,大小为一页。称为中断栈。
中断处理机制的实现
设备产生中断,经总线把电信号发送给中断控制器。如果中断线是激活的(它们是允许被屏蔽的),那么中断控制器就会把中断发往处理器。本质是通过电信号给处理器的特定管脚发送一个信号。除非处理器上禁止该中断,否则处理器会立即停止它正在做的事,关闭中断系统(防止有更高级的中断产生,变为中断嵌套),然后调到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。对于每条中断线,处理器都会调到对应的唯一的位置,这样内核就知道所接收中断的IRQ号了。然后内核调用函数do_IRQ()。从这开始,大多数中断处理代码是用C写的——但它们依然与体系结构有关。
do_IRQ()的声明如下:
unsigned int do_IRQ(struct pt_regs regs)
C的调用惯例是要把函数参数放在栈的顶部,因此pt_regs结构包含原始寄存器的值。中断的值也会被保存。所以,do_IRQ()可以将它提取出来。计算出中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。接下来do_IRQ()需要确保在这条中断线上有一个有效的处理程序。

下面是单处理器PC上输出的信息:

第一列是中断线。第二列是接收中断数目的计数器,事实上,系统中的每个处理器都存在这样的列。可以看到时钟中断已接收3602371次中断,这里声卡(EMU1K1)没有接收一次中断(表示机器启动以来还没有使用它)。第三列是处理这些中断的中断控制器。最后是与这个中断相关的设备的名字。其中4号中断就属于共享中断线(uhci-hcd和eth0都在这条中断线上列出来)。
中断控制
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为了我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。禁止中断也可以禁止内核抢占,但是都没法提供任何保护机制来防止来自其他处理器对共享数据的并发访问。Linux支持多处理器,因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问。而禁止中断提供保护机制,是防止来自其他中断处理程序的并发访问。
禁止和激活中断
用于禁止当前处理器(仅仅是当前处理器)上的本地中断,随后又激活它们的语句为:
local_irq_disable();
local_irq_enable();
但是这种方式会带来潜在的危险:例如,想象一下前面的代码片段是一个大函数的组成部分,这个函数被另外的两个函数调用,其中一个函数禁止中断,另一个函数不禁止中断,此时如果单纯的禁止中断或者激活中断就会产生问题。随着内核的不断增长,要想知道到达这个函数的所有代码路径将变得越来越困难,因此,在禁止中断之前保存中断系统的状态会更加安全一些。相反,在准备激活中断时,只需把中断恢复到它们原来的状态。
unsignen long flags;
local_irq_save(flags); // 禁止中断
local_irq_restore(flags); //恢复之前的状态
这些方法至少部分要以宏的形式实现,因此表面上flags参数(这些参数必须定义为unsigned long类型)是以值传递的。该参数包含具体体系机构的数据,也就是包含中断系统的状态。至少有一种体系结构把栈信息与值相结合(SPARC),因此flags不能传递给另一个函数(特别是它必须驻留在同一栈帧中)。基于这个原因,对local_irq_save()的调用和对local_irq_restore()的调用必须在同一函数中进行。
cli()能够禁止系统中所有处理器的中断,不过不再使用。
禁止指定中断线
在某些情况下,只禁止一条特定的中断线就够了,这就是所谓的屏蔽掉一条中断线。为此,Linux提供了四个接口:

前两个函数禁止中断控制器上指定的中断线,即禁止给定中断向系统中所有处理器的传递。另外,函数只有在当前正在执行的所有处理程序完成后,disable_irq()才能返回。因此,调用者不仅要确保不在指定线上传递新的中断,同时还要确保所有已经开始执行的处理程序已全部退出。函数disable_irq_nosync()不会等待当前中断程序执行完毕。
函数synchronize_irq()等待一个特定的中断处理程序的退出。如果在处理程序正在执行,那么该函数必须退后才能返回。
对这些函数的调用可以嵌套。但是在一条指定的中断线上,对disable_irq()或disable_irq_nosync()的每次调用,都需要相应的调用一次enable_irq()。只有在对enable_irq()完成最后一次调用后,才真正重新激活中断线。例如,如果disable_irq()被调用了两次,那么直到第二次调用enable_irq()后,才能真正的激活中断线。
中断系统的状态
通常有必要了解中断系统的状态(例如中断是禁止的还是激活的),或你当前是否正处于中断上下文的执行状态中。
宏irqs_disable()如果本地处理器上的中断系统是禁止的,则它返回非0,否则,返回0.
在<asm/hardirq.h>中定义的两个宏提供一个用来检查内核的当前上下文接口,它们是:
in_interrupt()
int_irq()
第一个宏最有用:如果内核处于中断上下文汇总,它返回非0。说明内核正执行中断处理程序,或者正在执行下半部处理程序。宏in_irq()只有在内核确实在执行中断处理程序时才返回非0。如果in_interrupt()返回0,则此刻内核处于进程上下文中。

参考自:《Linux Kernel Development》.
浙公网安备 33010602011771号