中断处理
可以让设备在产生某个事件时通知处理器的方法就是中断。一个“中断”仅是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。 Linux 处理中断的方式非常类似在用户空间处理信号的方式。 大多数情况下,一个驱动只需要为它的设备的中断注册一个处理例程,并当中断到来时进行正确的处理。本质上来讲,中断处理例程和其他的代码并行运行。因此,它们不可避免地引起并发问题,并竞争数据结构和硬件。 透彻地理解并发控制技术对中断来讲非常重要。
安装中断处理例程
内核维护了一个中断信号线的注册表,该注册表类似于I/O端口的注册表。模块在使用前要先请求一个中断通道(或则中断请求IRQ),然后在使用后释放该通道。在许多场合下,模块也希望可以和其他的驱动程序共享中断信号线。
在<linux/sched.h>头文件中定义了以下两个接口。
int request_irq(unsigned int irq,irqreturn_t (*handler)(int,void *,struct pt_regs *),unsigned long flags,
const char *dev_name,void *dev_id);
void free_irq(unsigned int irq,void *dev_id);
通常从request_irq函数返回给请求函数的值为0时表示申请成功,为负时表示错误码。函数返回-EBUSY表示已经有另外一个驱动程序占用了该中断号信号线。
unsigned int irq:是要申请的中断号。
irqreturn_t (*handler)(int,void *,struct pt_regs *):这是要安装的中断处理函数指针。
unsigned long flags:与中断管理有关的位掩码。
const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者。
void *dev_id:这个指针用于共享的中断信号线,它是唯一的标识符,在中断信号线空闲时可以使用它,驱动程序也可以使用他指向驱动程序自己的私有数据区(用来识别哪个设备产生了中断)。在没有强制使用共享方式时,dev_id可以被设置为NULL,总之用它来指向设备的数据结构是一个比较好的思路。
flag中设置的位如下所示:
SA_INTERRUPT:当该位被设置时,表明这是一个“快速”的中断处理例程,快速处理例程运行在中断禁用的状态下。
SA_SHIRQ:该位表示中断可以在设备之间共享。
SA_SAMPLE_RANDOM:该位表示产生的中断能对 /dev/random 和 /dev/urandom 使用的熵池(entropy pool)有贡献。 读取这些设备会返回真正的随机数,从而有助于应用程序软件选择用于加密的安全密钥。 若设备以真正随机的周期产生中断,就应当设置这个标志。若设备中断是可预测的,这个标志不值得设置。可能被攻击者影响的设备不应当设置这个标志。更多信息看 drivers/char/random.c 的注释。
中断处理例程可在驱动程序初始化时或者设备第一次打开时安装。因为中断信号的数量是非常有限的,所以推荐在设备打开时安装中断而不是在初始化时安装,这样可以共享这些有限的资源。
调用request_irq的正确位置应该是在设备第一次打开、硬件被告知产生中断之前。调用free_irq的位置是最后一次关闭设备、硬件被告知不再用中断处理器之后。这样我们必须为每个设备维护一个打开计数。
if(short_irq>=0){ //short_irq是我们要申请的中断号
result = request_irq(short_irq,short_interrupt,
SA_INTERRUPT,”short”,NULL); 是一个快速处理例程,不支持中断共享
if(result){
printk(KERN_INFO “short:can’t get assigned irq %i/n”,short_irq);
short_irq = -1; //中断号致负,表明申请失败
}
}
if(short_irq >=0){
result = request_irq(short_irq,short_interrupt,
SA_INTERRUPT,”short”,NULL);
if(result){
printk(KERN_INFO “short:can’t get assigned a irq %i/n”,short_irq);
short_irq = -1;
}else{
outb(0x10,short_base +2);
}
}
在i386和x86体系结构中定义如下函数:
int can_request_irq(unsigned int irq,unsigned long flags);
如果能够成功分配给定的中断,则该函数返回非零值,但要注意在request_irq和can_request_irq之间,可能会发生一些事情来改变现状。
/porc接口
当硬件的中断到达处理器时,一个内部计数递增,这位检查设备是否按预期工作提供了一种方法,显示的中断报告在文件/proc/interrupts中。另一个有用的文件时/proc/stat。关于文件中个字段的意义,可以参与资料,两者的区别在于interrupts文件不用来于体系结构,而stat文件则是依赖的:字段的数量依赖于内核之下的硬件
<asm/irq.h>中。ARM的定义为:
#define NR_IRQS128
自动检测IRQ号
驱动初始化时最迫切的问题之一是决定设备要使用的IRQ 线,驱动需要信息来正确安装处理例程。自动检测中断号对驱动的可用性来说是一个基本需求。有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:
if(short_irq < 0){
switch(short_base){
case 0x378:
short_irq = 7;
break;
case 0x278:
short_irq = 2;
break;
case 0x3bc:
short_irq = 5;
break;
}
}
当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。
但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。这个工作技术上非常简单: 驱动告知设备产生中断并且观察发生了什么。如果一切顺利,则只有一个中断信号线被激活。尽管探测在理论上简单,但实现可能不简单。有 2 种方法来进行探测中断: 调用内核定义的辅助函数和DIY探测。
内核辅助下的探测
linux内核提供了一个底层设施来探测中断号,它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在共享中断的模式下,并提供更好的找到配置中断号的方法。内核的这一设施由两个函数组成,在头文件<linux/iterrupt.h>中声明。
unsigned long probe_irq_on(void);
该函数返回一个未分配中断的位掩码。驱动程序必须保存返回的为掩码,并且将它传递给后面的probe_irq_off函数,调用该函数之后,驱动程序要安排设备至少产生一次中断。
unsigned long probe_irq_off(unsigned long);
在请求设备产生中断之后,驱动程序调用这个函数,并将前面probe_irq_on返回的位掩码作为参数传递给它。probe_irq_off返回”probe_irq_on”之后发生的中断编号,如果没有中断发生,就返回0(因此,IRQ 0不能被探测到,但在任何已支持的体系结构上,没有任何设备能够使用IRQ 0)。如果产生多次中断,probe_irq_off就返回一个负值(出现二意性)。
要注意在调用probe_irq_on之后启用设备上的中断,并在调用probe_irq_off之前禁用中断。在probe_irq_off之后,需要处理设备上代处理的中断。
int count = 0 ;
do{
unsigned long mask; 定义保存probe_irq_on返回掩码的变量
maske = probe_irq_on();
outb(0x10,short_base+2); 启用中断,short_base+2的第四个引脚
outb(0x00,short_base); 清除short_base的位
outb(0xFF,short_base); 设置该位,进行中断,写数据到数据端口
outb(0x00,short_base+2); 调用probe_irq_off前禁用中断
udelay(5); 留给中断探测一段时间
short_irq = probe_irq_off(mask);
if(short_irq == 0){ 这里对probe_irq_off的返回值进行判断,为零没有产生中断
printk(KERN_INFO “short_;no irq reported by probe/n”);
short_irq = -1;
}
/*如果已经产生多个中断,则结果为负值。
*我们应该服务该中断并多次再试。
*最多重试5次,然后放弃
*/
}while(short_irq < 0 &&count++ <5)
if(short_irq < 0){
printk(“short:probe failed %i times,giving up/n”,count);
}
注意在调用prpbe_irq_off 之前调用udelay的使用。探测是一个耗时的任务。因此,最好的方法是在模块初始化的时候探测中断信号线一次,这与时候设备打开时(推荐)或则在初始化函数内(不推荐)安排中断处理函数无关。
DIY探测
启用所有未被占用的中断,然后观察会发生什么。但是,我们要充分发挥对有关设备的了解。通常,设备可以使用3或4个IRQ号中的一个来进行配置,探测这些IRQ号,使我们能够不必测试所有可能的IRQ就检测到正确的IRQ号。
下面的代码通过测试所有“可能”的中断并观察将要发生的事情来进行中断探测。trials数组列出了以0作为结束标志的需要测试的IRQ,tried数组用来记录哪个处理例程被驱动程序注册了。
int trials[] = {3,5,7,9,0};
int tried[] = {0,0,0,0,0};
int i,count = 0;
为所有可能的中断线安装探测处理例程。
for(i=0;trials[i];i++){
tried[i] = request_irq(trials[i],short_probing,SA_INTERRUPTIBLE,”short probe”,NULL);
}
do{
short_irq = 0;
outb(ox10,short_base+2);
outb(0x00,short_base);
outb(0xff,short_base);
outb(0x00,short_base+2);
udelay(5);//这里等待中断的发生,如果发生了中断会在handler中重新设置short_irq的值
if(short_irq == 0){
printk(KERN_INFO “short:no irq reported by probe./n”);
}
}while(short_irq <= 0 && count++ < 5)
for(i=0;tried[i];i++){
if(tried[i] == 0)
free_irq(trials[i],NULL);
}
if(short_irq < 0){
printk(KERN_INFO “short:probe failed %i times,giving up/n”,count);
}
handler函数实现如下:
irqreturn_t short_probing(int irq,void *dev_id,struct pt_regs *regs)
{
if(short_irq == 0) short_irq = irq;
if(short_irq != irq) short_irq = -irq;
return IRQ_HANDLED;
}
有时,我们无法预知“可能的IRQ值。在这种情况下,需要探测所有的空闲设备号,而不仅仅是那些由trials数组列出的中断号。为了探测所有中断,不得不从IRQ 0 探测到IRQ NR_IRQS-1NR_IRQS是在头文件<asm/irq.h>中定义的与平台相关的常数。
快速和慢速处理例程
快速中断是那些能够很快处理的中断,而处理慢速中断会花费更长的时间。在处理慢速中断时处理器重新使能中断,避免快速中断被延时过长。在现代内核中,快速和慢速中断的区别已经消失,剩下的只有一个:快速中断(使用 SA_INTERRUPT )执行时禁止所有在当前处理器上的其他中断。注意:其他的处理器仍然能够处理中断。
除非你充足的理由在禁止其他中断情况下来运行中断处理例程,否则不应当使用SA_INTERRUPT.
实现中断处理例程
中断处理程序的一些限制:1.处理例程不能向用户空间发送或者接受数据,因为它不是在任何进程的上下文中执行的。2.处理例程也不能做任何可能发生休眠的操作,例如调用:wait_event、使用不带GFP_ATOMIC标志的内存分配操作,或则锁住一个信号量等等。3.最后处理例程不能调用schedule函数。
中断处理例程的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的中断的不同含义对数据进行相应的读或写。中断处理例程的第一步通常要清除接口卡上的一个位,大多数硬件设备在他们的”interruptible-pending”位被清除之前不会产生其它的中断。这也要根据硬件的工作原理决定, 这一步也可能需要在最后做而不是开始; 这里没有通用的规则。一些设备不需要这步, 因为它们没有一个”中断挂起”位; 这样的设备是少数。
中断处理例程的一个典型任务是:如果中断通知进程所等待的事件已经发生,比如新的数据已经到达,就会唤醒在该设备上休眠的进程。
例如在帧捕捉卡的例子,一个进程通过连续的读该设备来获取一系列图像;在读每一帧数据前,read调用都是阻塞的,每当新的数据帧到达时,中断处理例程就会唤醒该进程。
不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。 如果需要进行长时间计算, 最好的方法是使用 tasklet 或者 workqueue 在一个更安全的时间来调度计算任务(参见“顶半和底半”)。
irqreturn_t short_interrupt(int irq,void *dev_id,struct pt_reg *regs)
{
struct timeval tv;
int written;
do_gettimeofday(tv);
written = sprintf((char *)short_head,”%08u.%06u/n”,(int)(tv.tv_sec % 100000000),(int)(tv.tv_usec));
sprintf是一个格式化函数,原型为:int sprintf(char *string,char *format,arg_list);作用是将参数格式化的输出到目的字符串中(第一个参数为目的字符串),返回值表示有多少个字符被输入到目标字符串。
BUG_ON(written != 16);
short_incr_bp(&short_head,written);
wake_up_interruptible(&short_queue);
return IRQ_HANDLED;
}
static inline short_incr_bp(volatile unsigned long *index,int delta)
{
unsigned long new = *index + delta;
barrier(); //禁止对前后两条语句进行优化。
*index = (new > (short_buffer + PAGE_SIZE))?short_buffer:new);
}
处理例程的参数及返回值
中断处理例程中用到3个参数,int irq、void *dev_id、struct pt_reg *regs:
1.如果存在任何可以打印到日志的消息时,中断号(int irq)是很有用的。
2.void *dev_id是一种客户数据类型(即驱动程序可用的私有数据)。传递给request_irq函数的void *参数会在中断发生时作为参数被传回处理例程(void *dev_id参数和request_irq函数中的*dev_id是相同的)。通常我们会为dev_id传递一个指向自己设备的数据结构指针。这样一个管理若干相同设备的驱动在中断处理例程中不需要任何额外的代码,就可以找出哪个设备产生了当前的中断事件。
3.最后一个参数struct pt_reg *regs很少使用,它用来保存处理器进入中断代码之前的处理器上下文快照,该寄存器可被用来监视和调试,对一般的设备驱动程序任务来说通常不是必须的。
static irqreturn_t sample_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
struct sample_dev *dev = dev_id;
…
}
处理例程对应的典型open代码:
static void sample_open(struct inode *inode,struct file *filp)
{
struct sample_dev *dev = hwinfo +MINOR(inode->i_rdev);
request_irq(dev->irq,sample_interrupt,
0/* flags */,”sample”,dev/* dev_id */);
…
return 0;
}
中断处理例程应该返回一个值,用来指明是否真正处理了一个中断。如果处理例程发现设备确实需要处理, 应当返回 IRQ_HANDLED; 否则返回值 IRQ_NONE。
启用和禁用中断
有时设备驱动程序必须在一个时间段内(希望较短)阻塞中断的发出。通常来说我们必须在拥有自旋锁的情况下阻塞中断,以免死锁系统。在涉及自旋锁的情况下,有多种方法可禁用中断。但是我们应注意尽量少禁用中断,即使在设备驱动程序中也是如此。
禁用单个中断:有时(但很少),驱动程序需要禁用某个特定中断线的中断产生。内核提供了三个函数<asm/irq.h>。需要注意的是我们不能禁用共享的中断线,而在现代系统中,中断的共享是很常见的。
void disable_irq(int irq);//不但会禁止给定的中断,而且也会等待当前正在执行的中断处理例程完成。要注意的是,如果调用disable_irq的线程拥有任何中断处理例程所需要的资源,则系统会死锁。
void disable_irq_nosync(int irq);//和disable_irq不同, disable_irq_nosync是立即返回的。因此使用后则会更快,但是很可能让驱动程序处于竟态状态。
void enable_irq(int irq);
调用任一函数可能更新在可编程控制器(PIC)中的特定 irq 的掩码, 从而禁止或使能所有处理器特定的 IRQ。这些函数的调用能够嵌套,即如果 disable_irq 被连续调用 2 次,则需要 2 个 enable_irq 重新使能 IRQ 。可以在中断处理例程中调用这些函数,但在处理某个IRQ时再打开它是不好的做法。
禁用所有中断:在<asm/system.h> 中,定义了两个函数:
void local_irq_save(unsigned long flags);把当前中断状态保存到flags中,然后禁用当前处理器上的中断发送。注意flags是被直接传递。
void local_irq_disable(void);不保存状态而关闭本地处理器上的中断发送。不推荐使用。
如果调用链中的多个函数需要禁用中断,则应该使用local_irq_save。
打开中断:void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
在目前的2.6内核中,没有办法全局禁用整个系统上的所有中断。
鸟儿爱美,不仅需要羽毛之美,还需要鸣声婉转之美;