中断是指 CPU 在执行程序的过程中,出现了某些突发事件时 CPU 必须暂停执行当前的程序,转去处理突发事件,处理完毕后 CPU 又返回原程序被中断的位置并继续执行。
根据中断的来源,中断可分为内部中断和外部中断:
根据是否可以屏蔽中断分为可屏蔽中断与不屏蔽中断(NMI):
根据中断入口跳转方法的不同,中断分为向量中断和非向量中断:
为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux 将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottomhalf)。
注意:尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为 Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs),
unsigned long irqflags,
const char * devname,
void *dev_id);
函数返回值: 0 表示成功,或返回一个负的错误码,如 -EBUSY 表示另一个驱动已经占用了你所请求的中断线。
参数详解:
释放IRQ函数,request_irq()相对应的函数为 free_irq(),free_irq()的原型如下:
void free_irq(unsigned int irq,void *dev_id);
free_irq()中参数的定义与 request_irq()相同。
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
disable_irq_nosync()与 disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。注意,这 3 个函数作用于可编程中断控制器,因此,对系统内的所有CPU 都生效。
下列两个函数将屏蔽本 CPU 内的所有中断:
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
前者会将目前的中断状态保留在 flags 中,注意 flags 被直接传递,而不是通过指针传递。后者直接禁止中断。
与上述两个禁止中断对应的恢复中断的方法如下:
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
注意:以上各 local_开头的方法的作用范围是本 CPU 内。
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;
}
有些设备的设计更为先进,会简单的“申明”它们要使用的中断,这样,驱动程序就可以通过从设备的某个I/O端口或者PCI配置空间中读出一个状态值来获取中断号。当目标设备有能力告诉驱动程序他将使用的中断号时,自动检测中断号就只是一位着探测设备而不需要额外的工作来探测中断,虽然大部分硬件都是以这种方式工作,但是有些设备还是需要自动检测:驱动程序通知设备产生中断并观察会发生什么,如果一切正常,那么只有一条中短线被激活。
linux提供了一个底层设施来探测中断号。它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在共享中断的模式下,并可提供更好的找到配置中断号的方法,内核提供的这一设施由两个函数组成,在头文件
<linux/interrupt.h>
unsigned long probe_irq_on(void);
/*返回一个未分配中断的位掩码。驱动必须保留返回的位掩码,并在后边传递给probe_irq_off,在调用它之后,
驱动程序应当至少安排它的设备产生一次中断*/
int probe_irq_off(unsigned long);
/*在请求设备产生一次中断后,驱动调用这个函数,并将probe_irq_on产生的位掩码作为参数传递给probe_irq_off,
probe_irq_off返回在probe_on之后发生的中断号。如果没有中断发生,返回0,如果产生了多次中断,返回一个负值。*/
应当注意在调用 probe_irq_on 之后启用设备上的中断, 并在调用 probe_irq_off 前禁用。此外还必须记住在 probe_irq_off 之后服务设备中待处理的中断。
以下是LDD3中的并口示例代码,(并口的管脚 9 和 10 连接在一起,探测五次失败后放弃):
intcount= 0;
do
{
unsigned long mask;
mask = probe_irq_on();
outb_p(0x10,short_base+2); /* enable reporting */
outb_p(0x00,short_base); /* clear the bit */
outb_p(0xFF,short_base); /* set the bit: interrupt! */
outb_p(0x00,short_base+2); /* disable reporting */
udelay(5); /* give it some time */
short_irq = probe_irq_off(mask);
if (short_irq== 0){/* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
short_irq = -1;
}
} while (short_irq < 0 && count++< 5);
if (short_irq< 0)
printk("short: probe failed %i times, giving up\n",count);
最好只在模块初始化时探测中断线一次,大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。
DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。
下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 :
int trials[]= {3, 5, 7, 9, 0}; //trials 数组列出要尝试的中断, 以 0 作为结尾标志;
int tried[]={0, 0, 0, 0, 0}; //tried 数组用来跟踪哪个中断号已经被这个驱动注册
int i, count = 0;
for (i = 0; trials[i]; i++)
tried[i]= request_irq(trials[i], short_probing,
SA_INTERRUPT, "short probe", NULL);
do
{
short_irq = 0; /* none got, yet */
outb_p(0x10,short_base+2); /* enable */
outb_p(0x00,short_base);
outb_p(0xFF,short_base); /* toggle the bit */
outb_p(0x00,short_base+2); /* disable */
udelay(5); /* give it some time */
/* 等待中断,若在这段时间有中断产生,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; trials[i]; i++)
if (tried[i]== 0)
free_irq(trials[i],NULL);
if (short_irq< 0)
printk("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;/* found */
if (short_irq!= irq) short_irq=-irq;/* ambiguous */
return IRQ_HANDLED;
}
若事先不知道"可能的" IRQ ,就需要探测所有空闲的中断,所以不得不从 IRQ 0 探测到 IRQ NR_IRQS-1
Linux 内核有 3 个不同的机制可用来实现底半部处理:
tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;
工作队列,它可能有更高的延时,但允许休眠;
软中断。
tasklet 的使用较简单,我们只需要定义 tasklet 及其处理函数并将两者关联,例如:
void my_tasklet_func(unsigned long); /*定义一个处理函数*/
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); /*定义一个 tasklet 结构 my_tasklet,与 my_tasklet_func(data)函数相关联*/
代码 DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现了定义名称为my_tasklet 的 tasklet 并将其与 my_tasklet_func()这个函数绑定,而传入这个函数的参数为 data。
在需要调度 tasklet 的时候引用一个 tasklet_schedule()函数就能使系统在适当的时候进行调度运行,如下所示:
tasklet_schedule(&my_tasklet);
tasklet 作为底半部处理中断的设备驱动程序模板:
/*定义 tasklet 和底半部函数并关联*/
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
/*中断处理底半部*/
void xxx_do_tasklet(unsigned long)
{
...
}
/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
...
tasklet_schedule(&xxx_tasklet);
...
}
/*设备驱动模块加载函数*/
int __init xxx_init(void)
{
...
/*申请中断*/
result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "xxx", NULL);
...
}
/*设备驱动模块卸载函数*/
void __exit xxx_exit(void)
{
...
/*释放中断*/
free_irq(xxx_irq, xxx_interrupt);
...
}
上述程序在模块加载函数中申请中断,并在模块卸载函数中释放它。对应于 xxx_irq 的中断处理程序被设置为 xxx_interrupt()函数,在这个函数中,tasklet_schedule(&xxx_tasklet)调度的 tasklet 函数xxx_do_tasklet()在适当的时候得到执行。
工作队列的使用方法和 tasklet 非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; /*定义一个工作队列*/
void my_wq_func(unsigned long); /*定义一个处理函数*/
通过 INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定,如下所示:
INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL); /*初始化工作队列并将其与处理函数绑定*/
与 tasklet_schedule()对应的用于调度工作队列执行的函数为 schedule_work(),如:
schedule_work(&my_wq); /*调度工作队列执行*/
使用工作队列处理中断底半部的设备驱动程序模板:
/*定义工作队列和关联函数*/
struct work_struct xxx_wq;
void xxx_do_work(unsigned long);
/*中断处理底半部*/
void xxx_do_work(unsigned long)
{
...
}
/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
...
schedule_work(&xxx_wq);
...
}
/*设备驱动模块加载函数*/
int xxx_init(void)
{
...
/*申请中断*/
result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "xxx", NULL);
...
/*初始化工作队列*/
INIT_WORK(&xxx_wq, (void (*)(void *)) xxx_do_work, NULL);
...
}
/*设备驱动模块卸载函数*/
void xxx_exit(void)
{
...
/*释放中断*/
free_irq(xxx_irq, xxx_interrupt);
...
}
与tasklet 不同的是,上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码。
尽管 Linux 专家们多建议在设备第一次打开时才申请设备的中断并在最后一次关闭时释放中断以尽量减少中断被这个设备占用的时间,但是,大多数情况下,为求省事,大多数驱动工程师还是将中断申请和释放的工作放在了设备驱动的模块加载和卸载函数中。
通过 request_irq来安装共享中断与非共享中断有2点不同:
请求一个共享的中断时,如果满足下列条件之一,则request_irq成功:
注意:
当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断: