LDD-Interrupt Handling

写在前面:中断处理程序和其他代码同时运行,在处理中断时要注意相关数据结构和硬件的并发访问控制。
 
Installing an Interrupt Handler

中断连接线(interrupt lines)数量有限,是珍贵的资源,驱动程序要在使用之前请求系统分配中断连接线,使用完后还要释放。相关的函数定义在linux/interrupt.h中:
1 int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), 
2                       unsigned long flags, const char *dev_name, void *dev_id);
3 void free_irq(unsigned int irq, void *dev_id);        
函数执行成功时返回0,失败返回负数;如果返回-EBUSY,表明已经有驱动使用该中断号。中断连接线用整数来表示,称为中断号,即irq参数;handler参数为中断处理程序;flags参数指明中断管理的相关标志;dev_name是出现在/proc/interrupts中的设备名,中断号的拥有者;dev_id是共享中断连接线需要的参数。
中断相关的标志如下:
SA_INTERRUPT:表明是一个"fast"中断处理程序——CPU执行此中断处理函数会关闭中断
SA_SHIRQ:表明中断可以和其他设备共享
SA_SAMPLE_RANDOM:若设备会随机产生中断设置此标志(drivers/char/random.c中的注释说明设置此标志的设备会用来产生随机数(熵池,entropy pool),随机数会用来加密);如果设备可能被攻击,不能设置此标志位
 
中断处理函数可以在驱动初始化或者第一次打开设备时设置,似乎前者是更好的途径。事实却不尽然,尤其是设备没有设置共享标志时。由于中断连接线数量有限,如果在驱动初始化时就分配中断号,可能有很多驱动程序因获取中断号失败而无法执行。因此,调用request_irq的正确时机是第一次打开设备之时;调用free_irq的正确时机是最后一次关闭设备之时。
判断中断号是否可用:
1 int can_request_irq(unsigned int irq, unsigned long flags);
可用返回0;但是在can_request_irq和request_irq之间可能会发生许多事情。
 
硬件中断到达处理器时,内部的计数器会增加,统计结果呈现在/proc/interrupts文件中。要注意的是这个文件中只会列出生成文件时系统中正在使用的中断——即使是之前曾经使用,现在没有使用的中断也不会列出。文件的第一列是中断号;接着是每个CPU的统计信息;倒数第二列是处理中断的可编程中断控制器(PIC);最后一列是注册处理函数的设备名。/proc/stat中同样包含中断的统计信息。
 
内核提供了探测中断号的方法:
1 unsigned long probe_irq_on(void);
2 int probe_irq_off(unsigned long);

probe_irq_on会返回一个未分配的中断的掩码,驱动程序需要保留返回的掩码,作为probe_irq_off的参数。调用probe_irq_on函数后,驱动程序需要让设备至少产生一个中断。设备产生中断后,驱动程序调用probe_irq_off函数,得到probe_irq_on之后产生中断的中断号。如果没有中断产生,返回0;如果有多个中断产生,返回负数。

 
快中断可以在很短的时间内处理,慢中断需要花费很长的时间,因此在处理慢中断时将中断重新打开能够提升系统的性能,而处理快中断时会将当前的处理器的其他中断都关闭(其他处理器仍然可以处理中断,不同的CPU不可能同时处理同一个中断——可能和计算机系统内的中断处理器的级联有关?)。通常情况下,只有计时器中断会设置为SA_INTERRUPT类型,除非你十分确定中断处理函数需要在其他中断关闭的情况下执行,否则不要设置SA_INTERRUPT标志。
 
Implementing a Handler

中断处理函数无法和用户空间交互数据——中断处理函数不在进程上下文中执行;也无法调用可能休眠的函数,例如wait_event,schedule,从GFP_ATOMIC以外的区域分配内存。
中断处理函数一般执行两步操作:给设备反馈;与设备交互数据。典型的中断处理函数执行流程如下:
  1. 修改设备的寄存器状态,如清空设备的中断pend位
  2. 从设备读取数据,唤醒等待数据的进程
有些中断处理函数在执行时,需要关闭其他中断——驱动程序不应该擅自执行该操作。关中断的函数定义在asm/irq.h中:
1 void disable_irq(int irq);
2 void disable_irq_nosync(int irq);
3 void enable_irq(int irq);
disable_irq不但会关闭给定的中断号,还会等待正在执行的中断处理函数完成。如果调用disable_irq函数的线程持有中断处理函数所需的资源,系统可能会死锁。disable_irq_nosync会立即返回。
 
如果要一次性关闭当前CPU的所有中断,可以通过定义在asm/system.h中的以下函数:
1 void local_irq_save(unsigned long flags);
2 void local_irq_disable(void);
local_irq_save会将当前的中断状态保存在flags中,然后关闭当前处理器的中断;local_irq_disable会直接关闭中断,不会保存当前中断状态。恢复中断的函数如下:
1 void local_irq_restore(unsgned long flags);
2 void local_irq_enable(void);
 
Top and Bottom Halves

通常情况下,中断处理函数需要执行大量的操作处理中断,这与中断处理函数需要快速完成防止长时间阻塞中断的需求相违背。为了解决这个问题,Linux将中断处理函数分为上下两段——上半段是通过request_irq函数注册的和中断相对应的例程,下半段在一个更安全的时间由上半段调用。上下两段例程的主要区别是执行下半段例程的时候所有中断处于打开状态。典型上半段例程将设备的数据保存到缓冲区中,然后调用下半段例程,退出;下半段例程执行后续的耗时的操作。内核提供了两种机制来实现上下两段的中断处理函数:tasklet和workqueue;前者必须是原子的,后者可以休眠。
 
Interrupt Sharing

共享的中断同样通过request_irq函数申请,但是需要设置SA_SHIRQ标志位,指定独特的dev_id参数。内核会维护一个共享的中断号的处理函数的列表,dev_id用来选择不同的中断处理函数。如果请求的中断号处于空闲状态,或者已经注册的中断处理函数都指明这个中断号是共享的,request_irq函数就能成功返回。
中断产生时,CPU会调用所有注册的中断处理函数,并且传入对应的dev_id参数。中断处理函数需要判断属于自己的设备是否发生了中断,如果没有需要快速返回IRQ_NONE。
释放中断号时,同样调用free_irq函数,传入dev_id参数。
共享的中断处理函数最好不要调用enable_irq和disable_irq,否则可能引起混乱。
 
共享中断会影响/proc/interrupts,每个中断号的所有中断处理函数都列在文件的最后一列。
 
Interrupt-Driven I/O

中断驱动的I/O和缓冲区紧密相关:中断产生时将设备的数据保存在输入缓冲区中,供进程读取;进程将需要输出的数据保存在输出缓冲区,在中断时清空。
 
posted @ 2018-11-20 16:52  glob  阅读(426)  评论(0编辑  收藏  举报