7.2 中断处理程序
在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序或中断服务例程。产生中断的每个设备都有一个相应的中断处理程序。例如,由一个函数专门处理来自系统时钟的中断,而另外一个函数专门处理由键盘产生的中断。一个设备的中断处理程序是它设备驱动程序的一部分——设备驱动程序是用于对设备进行管理的内核代码。
在Linux中,中断处理程序是普通的C函数。只不过这些函数必须按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息,在其他方面,它们与一般的函数没有差别。中断处理程序与其他内核函数的真正区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于称之为中断上下文的特殊上下文中。需要指出的是,中断上下文偶尔也称为原子上下文,该上下文中的执行代码不可阻塞。
中断可能随时发生,因此中断处理程序随时可能执行。所以必须保证中断处理程序能够快速执行,才能保证尽可能快地恢复中断代码的执行。因此,尽管对硬件而言,操作系统能迅速对其中断进行服务非常重要;当然对系统的其他部分而言,让中断处理程序在尽可能短的时间内完成运行也同样重要。
最起码,中断处理程序要负责通知硬件设备中断已被接收,但是中断处理程序还要完成大量其他的工具。例如,可以考虑一下网络设备的中断处理程序面临的挑战。该处理程序除了要对硬件应答,还要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。
7.3 上半部与下半部的对比
又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的显然有所抵触。鉴于两个目的之间存在此消彼长的矛盾关系,一般把中断处理切为两个部分或两半。中断处理程序是上半部——接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部去。在合适的时机,下半部会被开中断执行。Linux提供了实现下半部的各种机制。
考察一下上半部和下半部分割的例子,以网卡作为实例。当网卡接收来自网络的数据包时,需要通知内核数据包到了。网卡需要立即完成这件事,从而优化网络的吞吐量和传输周期,以避免超时。因此,网卡立即发出中断,有最新数据包了。内核通过执行网卡已注册的中断处理程序来做出应答。
中断开始执行,通知硬件,拷贝最新的网络数据包到内存,然后读取网卡更多的数据包。这些都是重要、紧迫而又与硬件相关的工作。内核通常需要快速的拷贝网络数据包到系统内存,因为网卡上接收网络数据包的缓存大小固定,而且相比系统内存也要小得多。所以上述拷贝动作一旦被延迟,必然造成缓存溢出——进入的网络包占满了网卡的缓存,后续的入包只能被丢弃。当网络数据包被拷贝到系统内存后,中断的任务算是完成了,这时它将控制权交还给系统被中断前原先运行的程序。处理和操作数据包的其他工作在随后的下半部中进行。
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是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。
typedef irqreturn_t (*irq_handler_t)(int, void *);
注意handler函数的原型,它接受两个参数,并有一个类型为irqreturn_t 的返回值。
7.4.1 中断处理程序标志
第三个参数flags可以为0,也可能是下列一个或多个标志的位掩码。其定义在文件linux/interrupt.h。在这些标志中最重要的是:
IRQF_DISABLED——该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其他中断。如果不设置,中断处理程序可以与除本身外的其他任何中断同时运行。多数中断处理程序是不会去设置该位的,因为禁止所有中断是一种野蛮行为。这种用法留给希望快速执行的轻量级中断。这一标志是SA_INTERRUPT标志的当前表现形式,在过去的中断用以区分快速和慢速中断。
IRQF_TIMER——该标志是特别为系统定时器的中断处理而准备的。
IRQF_SHARED——此标志标明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。
第四个参数name是与中断相关的设备的ASCII文本表示。例如,PC机上键盘中断对应的这个值为“keyboard”。这些名字会被/proc/irq和/proc/interrupts文件使用,以便与用户通信。
第五个参数dev用于共享中断线。当一个中断处理程序需要释放时,dev将提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序。如果无须共享中断线,那么将该参数赋值为NULL就可以了,但是,如果中断线是被共享的,那么就必须传递唯一的信息。另外,内核每次调用中断处理程序时,都会把这个指针传递给它。实践中会通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内被用到。
request_irq()成功执行会返回0。如果返回非0值,表示有错误发生,在这种情况下,指定的中断处理程序不会被注册。最常见的错误是-EBUSY,它表示给定的中断线已经在使用。
注意:request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。在睡眠不安全的上下文中调用request_irq()函数,是一种常见错误。造成这种错误的部分原因是为什么request_irq()会引起阻塞。在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir()就是用来创建这个新的procfs项的。proc_mkdir()通过调用函数proc_create()对这个新的procfs项进行设置,而proc_create()会调用函数kmalloc()来请求分配内存。备注:函数kmalloc()是可以睡眠的。
7.4.2 一个中断例子
在一个驱动程序中请求一个中断线,并在通过request_irq()注册中断处理程序:
request_irq():
if(request_irq(irqn, my_interrupt, IRQF_SHARED, "my_device", my_dev)){
printk(KERN_ERR "my_device: cannot register IRQ %d\n", irqn);
return -EIO;
}
在这个例子中,irqn是请求的中断号;my_interrupt是中断处理程序;通过标志设置中断号可以共享;设备命名为“my_device”;最后是传递my_dev变量给dev形参。如果请求失败,那么这段代码将打印出一个错误并返回。如果调用返回0,则说明中断处理程序已经成功注册。此后,处理程序就会在响应该中断时被调用。有一点很重要,初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。
7.4.3 释放中断处理程序
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断号。上述动作需要调用:
void free_irq(unsigned int irq, void *dev);
如果指定的中断号不是共享的,那么,该函数删除处理程序的同时将禁用这个中断号。如果中断号是共享的,则仅删除dev所对应的处理程序,而这个中断号本身只有在删除了最后一个处理程序时才会被禁用。可以看出为什么唯一的dev如此重要。对于共享的中断号,需要一个唯一的信息来区分其上面的多个处理程序,并让free_irq()仅仅删除指定的处理程序。不管在哪种情况下,如果dev非空,它都必须与需要删除的处理程序相匹配。必须从进程上下文中调用free_irq()。
中断处理函数的注册和注销。
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);
}
void free_irq(unsigned int, void *);