第七章 中断和中断处理
7.1 中断
中断使得硬件得以发出通知给处理器。例如,在你敲击键盘的时候,键盘控制器(控制键盘的硬件设备)会发送一个中断,通知操作系统有键按下。中断本质上是一种特殊的电信号,由硬件设备发向处理器。处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由操作系统负责处理这些新到来的数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步——换句话说就是中断随时可以产生。因此,内核随时可能因为新到来的中断而被打断。
从物理学的角度看,中断是一种电信号,由硬件设备生成,并直接送入中断控制器的输入引脚中——中断控制器是个简单的电子芯片,其作用是将多路中断管线,采用复用技术只通过一个和处理器相连接的管线与处理器通信。当接收到一个中断后,中断控制器会给处理器发送一个电信号。处理器一经检测到此信号,便中断自己的当前工作转而处理中断。此后,处理器会通知操作系统已经产生中断,这样,操作系统就可以对这个中断进行适当地处理了。
不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志。因此,来自键盘的中断就有别于来自硬盘的中断,从而使得操作系统能够对中断进行区分,并知道哪个硬件设备产生了哪个中断。这样,操作系统才能给不同的中断提供对应的中断处理程序。
这些中断值通常被称为中断请求(IRQ)线。每个IRQ线都会被关联一个数值量——例如,在经典的PC机上,IRQ0是时钟中断,而IRQ1是键盘中断。
异常
在操作系统中,讨论中断就不能不提及异常。异常与中断不同,它在产生时必须考虑与处理器时钟同步。实际上,异常也常常称为同步中断。在处理器执行到由于编程失误而导致的错误指令(如被0除)的时候,或者是在执行期间出现特殊情况(如缺页),必须靠内核来处理的时候,处理器就会产生一个异常。因为许多处理器体系结构处理异常与处理中断的方式类似,因此,内核对它们的处理也很类似。本章对中断(由硬件产生的异步中断)的讨论,大部分也适合于异常(由处理器本身产生的同步中断)。
你已经熟悉一种异常:在第6章中你已看到,在x86体系结构上如何通过软中断实现系统调用,那就是陷入内核,然后引起一种特殊的异常——系统调用处理程序异常。你会看到,中断的工作方式与之类似,其差异只在于中断是由硬件而不是软件引起的。
7.2 中断处理程序
在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务例程(interrupt service routine,ISR)。产生中断的每个设备都有一个相应的中断处理程序。一个设备的中断处理程序是它设备驱动程序(driver)的一部分——设备驱动程序是用于对设备进行管理的内核代码。
中断处理程序是被内核调用来响应中断的,而他们运行于我们称之为中断上下文的特殊上下文中(关于中断上下文,我们将在后面讨论)。
7.3 上半部与下半部的对比
又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的显然有所抵触。鉴于两个目的之间存在此消彼长的矛盾关系,所以我们一般把中断处理切为两个部分或两半。中断处理程序是上半部(top half)——接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后,在合适的时机,下半部会被开中断执行。Linux提供了实现下半部的各种机制,第8章会讨论这些机制。
7.4 注册中断处理程序
中断处理程序是管理硬件的驱动程序的组成部分。每一设备都有相关的驱动程序,如果设备使用中断(大部分设备如此),那么相应的驱动程序就注册一个中断处理程序。
驱动程序可以通过 request_irq() 函数注册一个中断处理程序(它被声明在文件<Linux/interrupt.h中),并且激活给定的中断线,以处理中断:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
第一个参数irq表示要分配的中断号。对某些设备,如传统PC设备上的系统时钟或键盘,这个值通常是预先确定的。而对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以通过编程动态确定。
第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。
第三个参数flags可以为0,也可能是下列一个或多个标志的位掩码。其定义在文件<Linux/interrupt.h>。在这些标志中最重要的是:
- IRQF_DISABLED——该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其他中断。如果不设置,中断处理程序可以与除本身外的其他任何中断同时运行。多数中断处理程序是不会去设置该位的,因为禁止所有中断是一种野蛮行为。
- IRQF_SAMPLE_RANDOM——此标志表明这个设备产生的中断对内核熵池(entropy pool)有贡献。内核熵池负责提供从各种随机事件导出的真正的随机数。如果指定了该标志,那么来自该设备的中断间隔时间就会作为熵填充到熵池。
- IRQF_TIMER——该标志是特别为系统定时器的中断处理而准备的。
- IRQF_SHARED——此标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。
第四个参数name是与中断相关的设备的ASCII文本表示。例如,PC机上键盘中断对应的这个值为“keyboard”。这些名字会被/proc/irq和/proc/interrupts文件使用,以便与用户通信。
第五个参数dev用于共享中断线。当一个中断处理程序需要释放时(稍后讨论),dev将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序。如果无须共享中断线,那么将该参数赋为空值(NULL)就可以了,但是,如果中断线是被共享的,那么就必须传递唯一的信息。另外,内核每次调用中断处理程序时,都会把这个指针传递给它e。实践中往往会通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内被用到。
request_irq()成功执行会返回0。如果返回非0值,就表示有错误发生,在这种情况下,指定的中断处理程序不会被注册。最常见的错误是-EBUSY,它表示给定的中断线已经在使用(或者当前用户或者你没有指定IRQF_SHARED)。
注意,request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。天真地在睡眠不安全的上下文中调用requestirq()函数,是一种常见错误。
request_irq()—>proc_mkdir()—>proc_create()—>kmalloc()可以睡眠
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。上述动作需要调用:
void free_irq(unsigned int irq,void *dev)
如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用。由此可以看出为什么唯一的dev如此重要。对于共享的中断线,需要一个唯一的信息来区分其上面的多个处理程序,并让free_irq()仅仅删除指定的处理程序。不管在哪种情况下(共享或不共享),如果dev非空,它都必须与需要删除的处理程序相匹配。必须从进程上下文中调用free_irq()。
7.5 编写中断处理程序
以下是一个中断处理程序声明:
static irqreturn_t intr_handler(int irq,void *dev)
注意,它的类型与request_irq()参数中handler所要求的参数类型相匹配。第一个参数irq就是这个处理程序要响应的中断的中断号。
第二个参数dev是一个通用指针,它与在中断处理程序注册时传递给 request_irq()的参数dev必须一致。可以用来区分共享同一中断处理程序的多个设备。另外dev也可能指向中断处理程序使用的一个数据结构。因为对每个设备而言,设备结构都是唯一的,而且可能在中断处理程序中也用得到,因此,它也通常被看做dev。
中断处理程序的返回值是一个特殊类型:irqreturn_t。中断处理程序可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用,且确实是它所对应的设备产生了中断时,返回IRQ_HANDLED。
重入和中断处理程序
Linux中的中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断。这极大地简化了中断处理程序的编写。
共享的中断处理程序
共享的处理程序与非共享的处理程序在注册和运行方式上比较相似,但差异主要有以下三处:
- request_irq()的参数flags必须设置IRQF_SHARED标志。
- 对于每个注册的中断处理程序来说,dev参数必须唯一。指向任一设备结构的指针就可以满足这一要求;通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到它。不能给共享的处理程序传递NULL值。
- 中断处理程序必须能够区分它的设备是否真的产生了中断。这既需要硬件的支持,也需要处理程序中有相关的处理逻辑。如果硬件不支持这一功能,那中断处理程序肯定会束手无策,它根本没法知道到底是与它对应的设备发出了这个中断,还是共享这条中断线的其他设备发出了这个中断。
所有共享中断线的驱动程序都必须满足以上要求。只要有任何一个设备没有按规则进行共享,那么中断线就无法共享了。指定IRQF_SHARED标志以调用requestirq()时,只有在以下两种情况下才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了IRQF_SHARED。
内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,一个处理程序必须知道它是否应该为这个中断负责。如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进行检查。毫无疑问,大多数硬件都提供这种功能。
7.6 中断上下文
当执行一个中断处理程序,内核处于中断上下文(interrupt context)中。
中断上下文不可以睡眠,否则又怎能再对它重新调度呢?因此,不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在你的中断处理程序中使用它——这是对什么样的函数可以在中断处理程序中使用的限制。
中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线上的另一中断处理程序)。正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速、简洁。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。
中断处理程序栈的设置是一个配置选项。曾经,中断处理程序并不具有自己的栈。相反,它们共享所中断进程的内核栈。内核栈的大小是两页,具体地说,在32位体系结构上是8KB,在64位体系结构上是16KB。
现在,中断处理程序拥有了自己的栈,每个处理器一个,大小为一页。这个栈就称为中断栈,尽管中断栈的大小是原先共享栈的一半,但平均可用栈空间大得多,因为中断处理程序把这一整页占为己有。
7.7 中断处理机制的实现
设备产生中断,通过总线把电信号发送给中断控制器。如果中断线是激活的(它们是允许被屏蔽的),那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,否则,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。
7.8 中断控制
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构相关的,可以在<asm/system.h>和<asm/irq.h>中找到。