尽管有些设备仅通过它们的I/O寄存器就可以得到控制,但现实中的大部分设备却要比这复杂一些。设备需要同外界打交道,但设备又总是比处理器慢,让处理器等等待外部事件的情况总是不能令人满意的,所以我们需要找一种方法可以让设备在产生某个事件时通知处理器。
这种方法就是中断。一个“中断”仅仅是一个信号,当硬件需要处理器关注它时,就发送这个信号。Linux处理中断的方式很大程度上与它在用户空间处理信号是一样的。在大多数情况下,一个驱动程序只需要为它自己设备的中断注册一个处理例程,并且在中断到达时处理器能进行正确的处理。
注意,从本质上讲,中断处理例程和其他代码并发运行,这样,这些处理例程会不可避免地引起并发问题,并竞争数据结构和硬件。
准备并口
和大多数设备一样,在没有设定产生中断前,并口是不会产生中断;并口的标准规定设置端口2的第4位将启用中断报告(允许产生中断而不是产生中断)。short模块在初始化时调用outb来设置这个位。
在中断处于启用状态时,第当引脚10的电平发生从低到高改变时,并口就会产生一个中断,在没有把打印机连到端口上的情况下,强制接口产生中断的最简单方法是连接并口连接器的9号脚和10号引脚。
引脚9是并口数据字节中的最高位。将二进制数写入就会引发几个中断,将ASCII码文本写入端口不会产生中断,因为文本不会用到最高位。
安装中断处理例程
如果要看到产生的中断,那么仅仅向硬件写入是不够的,还必须在系统中安装一个软件处理例程。
中断信号线是非常珍贵且有限的资源。内核维护了一个中断信号线的注册表,有点类似于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,这是要申请的中断号。
handler,这是要安装的中断处理函数指针。
unsigned long flags,这是一个与中断管理有关的位掩码选项。
const char *dev_name,传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者。
void *dev_id,这个指针用于共享的中断信号线。它是唯一的标识符。
可在在flags中设置的位如下所示:
SA_INTERRUPT——当该位被设置时,表明这是一个快速的中断处理例程。快速中断处理例程运行在中断的禁用状态下。
SA_SHIRQ——该位表示中断可以在设备之间共享。
SA_SAMPLE_RANDOM——该位指出产生的中断能对/dev/random设备和/dev/urandom设备使用的熵池有贡献。从这些设备读取,将会返回真正的随机数,从而有助于应用软件选择用于加密的安全密钥。
中断处理例程是在驱动程序初始化时安装还是在设备第一次打开时安装呢?因为中断处理信号线的数量非常有限,我们不想肆意浪费。在设备打开的时候申请中断,则可以共享这些有限的资源。所有调用request_irq的正确位置应该是在设备第一次打开,硬件被告知产生中断之前。调用free_irq的位置是最后一次关闭设备,硬件被告知不再使用中断处理器之后。这样的缺点是我们必须为每个设备维护一个打开计数,这样才知道什么时候可以禁用中断。
尽管这样我们的short还是在装载时请求了它的中断信号线,下面是short请求中断代码:
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 irq %i\n",
short_irq);
short_irq = -1;
}
else { /*真正启用中断——假定这是一个并口*/
outb(0x10, short_base+2);
}
}下面是i386和x86-64体系架构用于查询某中断线是否可用的函数:
int can_request_irq(unsigned int irq, unsigned long flags);
如果能够成功分配给定的中断,则该函数返回非零值。但要注意,在调用can_request_irq和request_irq之前,始终可能发生一些事情来改变现状。
/proc接口
产生的中断报告显示在文件/proc/interrupts中,这可用来查看设备是否按预期工作。下面是它的一个快照
第一列是是IRQ号,其中明显缺少一些中断,这说明该文件只会显示那些已经安装了的中断处理例程的中断,即使在本系统中用过然后又被释放了的。
文件给出了已经发送到系统上每一个CPU的中断数量。正如你所看到的,Linux内核通常会在第一个CPU上处理中断,以便最大化缓存的本地性(某些大型系统会显式地使用中断平衡方案在系统范围内传播中断)。最后两列一个是处理中断的可编程中断控制器信息(通常不关心),另一个是中断处理例程的设备名称(这就是传递给request_irq的dev_name)。
/proc树结构下还有另一个与中断相关的文件,即/proc/stat。它的每行都以一个字符串开始,它是这行的关键字。intr标记正是我们需要的,下面是前一个快照不久后获得的:
第一个数是所有中断的总数,之后依次从中断0开始,如0号中断产生了5154006次,4号中断产生了4907次。它和上一个文件的区别是它会显示那些没有安装的中断号。
两个文件的另一个不同之处是interrupts文件不依赖于体系结构。当前x86体系结构上定义的中断数量是224个,不是16个。这可从include/asm-386/irq.h中得到解释。
自动检测IRQ号
驱动程序初始化时,最迫切的问题就是要解决设备将使用哪条IRQ信号线。尽管程序员可以在装载时指定中断号,但这不是一个好习惯,因为大部分时间用户也不知道这个中断号。因此,中断号的自动检测对于驱动程序可用性来说是一个基本要求。
有时,自动检测依赖于一些设备拥有的默认特性。既然如此,驱动程序可以假定设备使用了这些默认值。这也是short在检测并口时的默认行为,正如short的代码所给出的那样,其实现相当简单:
if(short<0)/*尚未给定,强制使用默认值*/
switch(short_base) {
case 0x378: short_irq = 7; break;
case 0x278: short_irq = 2; break;
case 0x3bc: short_irq = 5; break;
}这段代码根据选定的I/O地址的基地址分配中断号,也允许用户在装载时使用下面的命令来覆盖默认值:
insmod ./short.ko irq=x
有些设备会简单的声明它们要使用的中断。这样,驱动程序就可以通过从设备的某个I/O端口或者PCI配置空间中读出一个状态字为获得中断号。幸运的是,大多数现代硬件以这种方式工作,比如,PCI标准要求外设声明它们打算使用的中断线。
令人遗憾的是,并不是所有的设备都对程序员这么友好,自动检测可能还是需要做一些探测工作。这在技术上很简单:驱动程序通知设备产生中断并观察会发生什么。如果一切正常,那么只有一条中断信号线被激活。
尽管理论上讲起来简单,但实际实现起来就不那么清晰了。现在我们来看看执行该任务的两种方法:调用内核定义的函数,或者实现我们自己的版本。
内核帮助下的探测
Linux内核提供了一个底层设施来探测中断号,它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在非共享中断模式下,并可提供更好的找到配置中断号的方法。这个设施由两个函数组成,在<linux/interrupt.h>中声明。
unsigned long probe_irq_on(void);
int probe_irq_off(unsigned long);如果没有中断发生就返回0,如果产生了多次中断就返回一个负值。short模块演示了如何进行这样的探测。如果指定probe=1选项装载模块,并且并口连接器的引脚9和10相连,就会执行下面的代码进行中断信号线的检测:
int count = 0;
do {
unsigned long mask;mask = probe_irq_on();
outb_p(0x10, short_base+2); /*启用中断报告*/
outb_p(0x00, short_base); /*清除该位*/
outb_p(0xff, short_base); /*设置该位:中断!*/
outb_p(0x00, short_base+2); /*禁用中断报告*/
udelay(5); /*留给中断探测一些时间*/
short_irq = probe_irq_off(mask);if(short_irq == 0) { /*没有找到*/
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);探测是一个很耗时的任务,因此最好的方法就是只在模块初始化的时候探测中断信号线一次,这与是否在设备打开时或者在初始化函数内安装中断处理例程无关。
值得注意的是,在一些平台(PowerPC、M68k、大部分MIPS的实现以及两个SPARC版本)上,探测是没有必要的,因些前面的函数只是一些点位符,有时叫"useless ISA nonsense"。而在其他的平台上探测只是为ISA设备实现的。总之,大多数体系结构 都定义了函数(甚至是空的)来简化现有的设备驱动程序的移植。
DIY探测
探测也可以由驱动自己实现。如果装载时指定probe=2,short模块将对IRQ信号线进行DIY探测。这机制与先前内核的探测是一样的:启用所有未被占用的中断,然后观察会发生什么。在short中我们假定可能的IRQ值是3、5、7、9,这些编号实际上是并口设备允许用户选择的一些值。
下面代码通过测试所有可能的中断并观察将要发生的事情来进行中断探测。
int trials[] = {3, 5, 7, 9, 0};
int tried[] = {0, 0, 0, 0, 0};
int i, count = 0;/* 为所有可能的中断安装探测处理例程
记录那些目前空闲的结果(0为成功,否则为-EBUSY)仅仅是为了释放已获得的资源*/
for(i=0; trials[i]; i++)
tried[i] = request_irq(trials[i], short_probing,
SA_INTERRUPT, "short probe", NULL);
do {
short_irq = 0; /*初始值,什么也未获得*/
outb_p(0x10, short_base+2); /*启用中断报告*/
outb_p(0x00, short_base); /*清除该位*/
outb_p(0xff, short_base); /*设置该位:中断!*/
outb_p(0x00, short_base+2); /*禁用中断报告*/
udelay(5); /*留给中断探测一些时间*//*处理例程已经设置了相应的值*/
if(short_irq == 0) { /*没有找到*/
printk(KERN_INFO "short: no irq reported by probe\n");
}
/*如果已经有多个中断被激活,则结果为负值。
我们应该服务该中断并再次重试
最多重试5次,然后放弃。*/
}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);有时我们无法预知“可能的”中断值,在这种情况下,需要探测所有的空闲中断号,而不仅仅是那些由trials[]数组列出的中断号。为了探测所有的中断,不得不从IRQ0探测到IRQ NR_IRQS-1,NR_IRQS是在头文件<asm/irq.h>中定义的具有平台相关性的常数。
下面是探测处理例程本向,它的任务是根据实际收到的中断号更新short_irq变量,short_irq值为0意味着“什么也没有”,负值意味着“二义性”。
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是要处理的中断号,就足以理解上面的函数了。
快速和慢速处理例程
只有老版本才区分快速和慢速中断。快速中断是那些可以被很快处理的中断,而处理慢速中断则会明显花费更长的时间。当慢速中断正在被处理时,慢速中断要求处理器可以再次启用中断。否则,需要快速处理的任务可能会被延迟过长。也就是说快速中断可以打断慢速中断。
在现代内核中,很多快速中断和慢速中断的区别已经消失了。剩下的只有一个:快速中断执行时,当前处理器上的其他所有中断都被禁止,但其它处理器仍然可以处理中断。
那么应该使用哪种中断处理例程呢?在现代系统中,SA_INTERRUPT只是在少数几种特殊情况(例如定时器中断)下使用。我们不应该使用SA_INTERRUPT标志,除非有足够必要的理由想要在其他中断被禁用的时候运行自己的中断处理例程。
x86平台上中断处理的内幕
下面论述是从是从2.6内核中的文件得出的,虽然基本概念是相同的,但是硬件细节还是与其他平台有所区别。
最底层的中断处理代码可见entry.S文件,该文件是一个汇编语言文件,完成了许多机器级的工作。这个文件利用几个汇编技巧及一些宏,将一段代码用于所有可能的中断。在所有情况下,这段代码将中断编号压入栈,然后跳转到一个公共段,而这个公共段会调用在irq.c中定义的do_IRQ函数。
do_IRQ函数做的第一件事就是应答中断,这样中断处理器就可以继续处理其他的事情了。然后该函数对于给定的IRQ号获得一个自旋锁,这样就阻止了任何其他的CPU处理这个IRQ。接着清除几个状态位(包括IRQ_WAITING),然后寻找这个特定IRQ的处理例程。如果没有例程,就什么也不做;自旋锁被释放,处理什么待处理的软件中断,最后do_IRQ返回。
如果设备有一个已注册的处理例程并且发生中断,则函数handle_IRQ_event会被调用以实际调用处理例程。如果处理例程是慢速的,将重新启用硬件中断,并调用处理例程。然后做一些清理工作,接着运行软件中断,最后返回到常规工作。作为中断的结果(例如,处理例程可以wake_up一个进程),“常规工作”可能已经改变,所以,从中断返回时发生的最后一件事情可能就是一次处理器的重新调度。
IRQ探测是通过为每个缺少中断处理例程的IRQ设置IRQ_WAITING状态位来完成的。当中断产生时,因为没有处理例程,do_IRQ清除该位然后返回。当probe_irq_off被一个驱动程序调用的时候,只需要搜索那些没有设置IRQ_WAITING位的IRQ。
实现中断处理例程
中断处理例程没什么与众不同的——它们也是变通的C程序,唯一特殊的地方就是处理例程是在中断时间内运行的,因此它的行为会受到一些限制。这些限制与我们在内核定时器是看到的一样。
处理例程不能向用户空间发送或着接收数据,因为它不是在任何进程的上下文中执行的 处理例程也不能做任何可能发生休眠的操作,例如调用wait_event、使用不带有GFP_ATOMIC标志的内存分配操作,或者锁住一个信号量 处理例程不能调用schedule函数还是举帧捕捉卡的例子,一个进程通过连续地读该设备来获取一系统图像;在读每一帧数据前,read调用都是阻塞的,每当新的数据帧到达时,中断处理例程就会唤醒此进程。这里假定捕捉卡会中断处理器以便通知每一帧数据的成功到达。
无论是慢速还是快速处理例程,程序员都应该编写执行时间尽可能短的处理例程。如果需要执行一个长时间的计算任务,最好的方法是使用tasklet或者工作队列在更安全的时间内调度计算任务(我们将在顶半和底半描述用这种方法来延迟处理工作)。
在short示例代码中,中断处理例程调用了do_gettimeofday,并输出当前时间到大小为一页的循环缓冲区中,然后唤醒任何一个读取进程,告诉该进程现在有新的数据可以读取。
irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct timeval tv;
int written;do_gettimeofday(&tv);
/*写入一个16字节的记录。假定PAGE_SIZE是16的倍数*/
written = sprintf((char *)short_head, %08u.%06u\n",
(int)(tv.tv_sec%100000000), (int)(tv.tv_usec));
BUG_ON(written != 16);
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /*唤醒任何读取进程*/
return IRQ_HANDLED;
}尽管上述代码很简单,去代表了中断处理例程的典型工作。
static inline void short_incr_bp(volatile unsigned long *index, int delta)
{
unsigned long new = *index+delta;
barrier(); /*禁止对前后两条语句的优化*/
*index = (new >= (short_buffer+PAGE_SIZE)) ? short+buffer : new;
}对barrier的调用将阻止编译器在函数的两行语句之前做任何的优化工作。
处理例程的参数及返回值
虽然short没有对参数进行处理,但还是有三个参数被传给了中断处理例程:irq、dev_id和regs。让我们来看看每个参数的意义。
如果存在任何可以打印到日志的消息时,中断号(int irq)是很有用的。void *dev_id是一种客户数据类型(即驱动程序可用的私有数据),传给request_irq函数的void *参数会在中断发生时作为参数被传回处理例程。通常我们会为dev_id传递一个指向自己设备的数据结构指针,这样,一个管理若干同样设备的驱动程序在不断处理例程中不需要做任何额外的代码,就可以找出哪个设备产生了当前的中断事件。
中断处理例程中参数的典型用法如下:
static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct sample_dev *dev = dev_id;
/*现在,dev指向正确的硬件项*/
/*....*/
}与这个处理例程相关联的典型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;
}最后一个参数struct pt_reg *regs很少使用,它保存了处理进入中断代码之前的处理器上下文快照。该寄存器可被用来监视和调度,对一般的驱动程序任务来说通常不是必需的。
中断处理例程应该返回一个值,用来指明是否真正处理了一个中断。如果处理例程发现其设备的确需要处理,则应该返回IRQ_HANDLED;否则,返回值应该是IRQ_NONE。我们也可以使用下面的宏来产生这个返回值:
IRQ_RETVAL(handled)
如果要处理该中断,则handled应该取非零值。该返回值将被内核使用,以便检测并抑制假的中断。如果设备无法告诉我们是否被真正中断,则应该返回IRQ_HANDLED.
启用和禁用中断
有时设备驱动程序必须在一个时间段内(希望很短)阻塞中断折发出。通常来说,我们必须在拥有自旋锁的时候阻塞中断,以名死锁系统。我们应该尽量少禁用中断,即使在设备驱动程序中也是如此,同时这种技术不应该在驱动程序中作为互斥机制使用。
禁用单个中断
有是(但很少),驱动程序需要禁用某个特定中断线的中断产生。为此,内核提供了三个函数,在<asm/irq.h>中声明。我们不能禁用共享的中断线,而在现代系统上,中断的共享是很常见的。
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);调用这些函数可以在所有处理器上禁用或启用IRQ。对这些函数的调用是可以嵌套的——如果disable_irq被成功调用两次,那么在IRQ真正重新启用之前,则需要执行两次enable_irq调用。从一个中断处理例程中调用这些函数是可以的,但是在处理某个IRQ时再打开它不是一个好习惯。
disable_irq不但会禁止给定的中断,而且也会等待当前正在执行的中断处理例程完成。因此,如果调用disable_irq的线程拥有任何中断处理例程需要的资源(比如自旋锁),则系统会死锁。disable_irq_nosync是立即返回的。因此使用后者会更快,但是可以会让你的驱动程序处理于竞态状态。
但是为什么还要禁用中断呢?还是举个并口的例子,先看看plip网络接口。一个plip设备使用裸的并口传送数据。因为并口连接器上只有5个位可以读,它们被解释为4个数据位和一个时钟/握手信号。当发起者(发送数据包的那个接口)送出一个包的头4个位时,时钟线的电平升高,这将导致接收方接口中断处理器。然后,plip的处理例程就会被调用,以便处理最新到达的数据。
在设备被通知之后,数据的传输将继续进行。这里,plip使用握手信号线和接收方保持同步。如果接口每接收一个字节都要处理两次中断,那么性能显然是不可忍受的。因此,驱动程序在接收数据包的时候要禁用中断,而使用“轮询并延迟”循环来接收数据。
同样地,因为接收方到发送方的握手信号被用来应答数据的接收,所以发送接口也要在发送数据包时禁用它自己的IRQ信号。
禁用所有的中断
如果要禁用所有的中断,可通过下面两个函数之一关闭当前处理器上的所有中断处理,在<asm/system.h>中:
void local_irq_save(unsigned long flags);所当前中断状态保存到flags中,然后禁用当前处理器上的中断发送。
void local_irq_disable(void);不保存状态而关闭本地处理器上的中断发送,只有我们知道中断并未在其他地方被禁用的情况下,才能使用这个版本。可通过下面的函数打开中断:
void local_irq_save(unsigned long flags);恢复之前的状态
void local_irq_disable(void);无条件打开中断在2.6内核中,没有办法全局禁用整个系统上的所有中断。内核开发者认为关闭所有中断的代价太高,因此没有必要提供这种能力。如果使用的老驱动程序调用了类似cli和sti这样的函数,为了该驱动程序能够在2.6下使用,则需要进行修改而使用正确的锁。
顶半部和底半部
中断处理的一个主要问题是怎样在处理例程内完成耗时的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理例程需要尽快结束而不能使中断阻塞的时间过长,这两个需求(工作和速度)彼此冲突,让驱动程序的作者多少有点困扰。
Linux(连同很多其它的系统)通过将中断处理例程分成两部分来解决这个问题。称为“顶半部”的部分是实际响应中断的例程,也就是request_irq注册的中断例程;而所谓的“底半部”是一个被顶半部调度,并在稍后更安全的时间内执行的例程。
“顶半部”处理例程和“底半部”处理例程最大的不同,就是当底半部处理例程执行时,所有的中断都是打开的——这就是所谓的在更安全时间内运行。典型的情况是顶半部保存设备的数据到一个设备特定的缓冲区并调度它的底半部,然后退出:这个操作是非常快的。然后,底半部执行其他必要的工作,例如唤醒进程、启动另外的I/O操作等等。这种方式允许在底半部工作期间,顶半部还可以继续为新的中断服务。
几乎每一个严格的中断处理例程都是以这种方式分成两部分的。例如,当一个网络接口报告有新数据包到达时,处理例程仅仅接收数据并将它推到协议层上,而实际的数据包处理过程是在底半部执行的。
Linux内核有两种不同的机制可以用来实现底半部处理,第一种就是tasklet,它通常是底半部处理的优选机制;因为这种机制非常快,但是所有的tasklet代码必须是原子的。第二种就是工作队列,它可以具有更高的延迟,并且允许休眠。
tasklet
tasklet是一个在软件中断上下文被调度运行的特殊函数,它在由系统决定的安全时刻运行。它可以被多次调度运行,但实际只会运行一次(在本次运行完之前如果再次被一次或多次调度),tasklet可以与其它的tasklet并行地运行在对称多处理器(SMP)系统上。这样,如果驱动程序有多个tasklet,它们必须使用某种锁定机制来避免彼此间的冲突。
tasklet可确保和第一次调度它的函数运行在同一CPU上。因为tasklet在中断处理例程结束前不会开始运行,所以此时的中断处理例程是安全的,但是在task运行时,可能有其它的中断发生,因此在tasklet和中断处理例程之间的锁还是需要的。
驱动程序short如下声明它自己的tasklet:
void short_do_tasklet(unsigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);函数task_schedule用来调度一个tasklet运行。如果指定tasklet=1选项装载short,它就会安装一个不同的中断处理例程,这个处理例程保存数据并如下调度tasklet:
irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_gettimeofday((struct timeval *) tv_head);/*强制转换以免出现“易失性”警告*/
short_incr_tv(&tv_head);
tasklet_schedule(&short_tasklet);
short_wq_count++; /*记录中断的产生*/
return IRQ_HANDLED;
}实际的tasklet例程如下,它将会在系统方便时得到执行:
void short_do_tasklet(unsigned long unused)
{
int savecount = short_wq_count, written;
short_wq_count = 0; /*已经从队列中移除*/
/*
底半部读取由顶半部填充的tv数组,
并向循环缓冲区中打印信息,而缓冲区的数据则由
读取进程获得
*/
/*首先将调用此bh之前发生的中断数量写入*/
written = sprintf((char *)short_head, "bh after %6i\n", savecount);
short_incr_bp(&short_head, written);/*然后写入时间值,每次写入16个字节,
所以它与PAGE_SIZE是对齐的*/
do{
written = sprintf((char *)short_head, "%08u.%06u\n",
(int)(tv_tail->tv_sec%100000000),
(int)(tv_tail->usec));
short_incr_bp(&short_head, written);
short_incr_tv(&tv_tail);
}while(tv_tail != tv_head);
wake_up_interruptible(&short_queue); /*唤醒任何读取进程*/
}除其它动作外,这个tasklet记录了自从它上次被调用以来产生了多少次中断。一个类似于short的设备可以在很短的时间内产生很多次中断,所以在底半部执行前,肯定会有多次中断发生。驱动程序必须一直对这种情况有所准备,并且必须能根据顶半部保留的信息知道有多少工作需要完成。
工作队列
工作队列会在将来的某个时间、在某个特殊的工作者进程上下文中调用一个函数。因为工作队列函数运行在进程上下文中,因此可在必要时休眠。但是我们不能从工作队列向用户空间复制数据,除非使用将在第十五章中描述的高级技术,要知道,工作者进程无法访问其他任何进程的地址空间。
如果在装载short驱动程序时将wq选项置为非0,则该驱动程序将使用工作队列作为其底半部进程。它使用系统共享的工作队列,如果工作队列函数要长时间休眠则应该创建我们自己的专用工作队列。我们需要一个work_struct结构,该结构如下声明并初始化:
static struct work_struct short_wq;
下面是出现在short_init()中的代码:
INIT_WORK(&short_wq, (void (*)(void *))short_do_tasklet, NULL);
在使用工作队列时,short构造了另一个中断处理例程,如下所示:
irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_gettimeofday((struct timeval *) tv_head);
short_incr_tv(&tv_head);
schedule_work(&short_wq);
short_wq_count++; /*记录中断的产生*/
return IRQ_HANDLED;
}该中断处理例程和tasklet版本非常相似,唯一不同的是它调用了schedule_work来安排底半部处理。
中断共享
Linux内核支持所有总线的中断共享,针对2.6内核的设备驱动程序,应该在目标硬件可以支持共享中断操作的情况下处理中断共享。
安装共享的处理例程
共享中断也是通过request_irq安装的,但是有两个不同之处:
请求中断时,必须指定参数flags参数中的SA_SHIRQ位。 dev_id参数必须是唯一的。任何指向模块是址空间的指针都可以用,但dev_id不能设置成NULL。如果注册共享中断时传递了值为NULL的dev_id,现代的内核就会给出警告。当请求一个共享中断时,如果满足了下面条件之一,那么request_irq就会成功:
- 中断信号线是空闲的
- 任何已经注册了该中断信号线的处理例程也标识了IRQ是共享的。
无论何时,当两个或者更多的驱动程序共享一同根中断信号线,而硬件又通过这根信号线中断处理器时,内核会调用每一个为这个中断注册的处理例程,并将它们自己的dev_id传回去。因此,一个共享的处理例程必须能够识别属于自己的中断,并且在自己的设备没有被中断时迅速退出。
对于共享处理例程是没有探测函数可以利用的。
使用共享处理例程的驱动程序需要小心一件事情:不能使用enable_irq和disable_irq。如果使用了,共享中断信号线的其他设备就无法正常工作了;即使在很短时间内禁用中断,也会因为这种延迟而为设备和其用户带来问题。
运行处理例程
如上所述,当内核产生中断时,所有已注册的处理例程都将被调用。
装载short时,如果指定shared=1选项,则将安装下面的处理例程而不是默认的处理例程:
irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int value, written;
struct timeval tv;/*如果不是short产生的,则立即返回*/
value = inb(short_base);
if(!(value & 0x80))
return IRQ_NONE;/*清除中断位*/
outb(value & 0x7f, short_base);/*其余没什么变化*/
do_gettimeofday(&tv);
written = sprintf((char *)short_head, %08u.%06u\n",
(int)(tv.tv_sec%100000000), (int)(tv.tv_usec));
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /*唤醒任何读取进程*/
return IRQ_HANDLED;
}因为并口没有中断挂起位,所以使用了9号引脚(9号和10号引脚是连在一起的)来检查报告的中断是否是送给short的,如果该位为高那是,然后处理例程清除该位。如果与short共享IRQ的其他设备产生了中断,short就知道它自己的信号线没有被激活,所以不会做任何工作。
一个功能完整的驱动程序可能会将任务分成顶半部和底半部,当然这很容易添加,并且对用于实现共享的代码没有任何影响。一个真正的驱动程序或许会使用dev_id参数来判断产生中断的某个或多个设备。
注意,打印机协议不允许共享。
/proc接口和共享的中断
在系统上安装共享的中断处理例程不会对/proc/stat造成影响,它甚至不知道哪些处理例程是共享的。但是,/proc/interrupts会有稍许改变。
所有为同一中断号安装的处理例程会出现在/proc/interrupts文件的同一行上。下面的输出(来自一个x86系统)说明了共享的中断处理例程是怎样显示的:
CPU0
0: 892335412 XT-PIC timer
1: 453971 XT-PIC i8042
2: 0 XT-PIC cascade
5: 0 XT-PIC libata, ehci_hcd
8: 0 XT-PIC rtc
9: 0 XT-PIC acpi
10: 11365067 XT-PIC ide2, uhci_hcd, uhci_hcd, SysKonnect SK-98xx,EMU10K1
11: 4391962 XT-PIC uhci_hcd, uhci_hcd
12: 224 XT-PIC i8042
14: 2787721 XT-PIC ide0
15: 203048 XT-PIC ide1
NMI: 41234
LOC: 892193503
ERR: 102
MIS: 0该系统具有多个共享的中断线。
中断驱动的I/O
数据缓冲区有助于将数据的传送和接收与系统调用write和read分离开来,从而提高系统的整体性能。
一个好的缓冲机制需要采用中断驱动的I/O,这种模式下,一个输入缓冲区在中断时间内被填充,并由读取该设备的进程取走缓冲区内的数据;一个输出缓冲区由写入设备的进程填充,并在中断时间内取走数据。
要正确进行中断驱动的数据传输,则要求硬件应该能够按照下面的语义来产生中断:
对于输入来说,当新的数据已经到达并且处理器准备好接收它时,设备就中断处理器。 对于输出来说,当设备准备好接收新数据或者对成功的数据传送进程应答时,就要发出中断。shortprint模块实现了一个针对并口、面向输出的非常简单的驱动程序。
当用户空间进程向该设备写入数据时,数据会反馈到缓冲区中,但write方法并不实际执行任何的I/O操作,只是将数据写入缓冲区,并不将数据写入硬件。