Embedded 版 (精华区)

发信人: Thinkpad (船长), 信区: Embedded_system
标  题: Ch09-中断处理(上)(zz)
发信站: 哈工大紫丁香 (Sun Jun 24 15:23:12 2001) , 转信


  发信人: dot (爱吃萝卜和青菜), 信区: Embedded
标  题: (LDD) Ch09-中断处理(上)
发信站: 武汉白云黄鹤站 (2001年06月16日14:23:52 星期六), 站内信件

第9章 中断处理


       中断是硬件管理的最终资源。众所周知,设备利用中断来通知软件可以对它进行
操作了。Linux为中断处理提供了很好的接口。事实上中断处理的接口如此之好,以至于
编写和安装中断处理程序几乎和编写其它核心函数一样容易。但是由于中断处理程序和
系统的其它部分是异步运行的,使用时要注意一些事项。

 

       本章的示例代码使用并口来产生中断。因此,如果你想运行测试程序,你必须给
你的电烙铁接上电源,即使在上一章的例子程序中你拒绝这样做。

 

       我们用上一章的short模块来示范如何使用中断。这个模块的名字,short,实际
上是指 short int(这是C语言,不是吗?),提醒我们它要对中断(interrupt)进行处理


准备并口
       虽然我在第8章“硬件管理”的“使用并口”一节已经提到,并口很简单,但它
也会触发中断。打印机就是利用这种能力来通知lp驱动程序它已准备好接收缓冲区中的
下一个字符。

 

       在指定接口这样做之前实际上并不会产生中断;并口标准规定设置2号端口的第4
位(0x37a,0x27a或其它某个地址)时启动中断报告。short模块在初始化时调用outb设置
该位。

 

       启动中断报告之后,每当引脚10(所谓的"ACK"位)上的电平从低变高时,并口都
会产生一个中断。强迫接口(没有把打印机连到端口上)产生中断的最简单方法是将并
行插座的引脚9和引脚10相连。为此,你需要一个阳性的25针D型插座和一英寸的电缆线


 

       引脚9是并行数据字节中最重要的一位。如果你往/dev/short0中写入二进制数据
,就可以产生几个中断。然而,往端口中写入ASCII文本将不会产生中断,因为此时不会
设置这个最重要的位。

 

       如果你确实想“看看”产生的中断,那么,仅仅往硬件设备中写是不够的;还必
须在系统中配置一个软件处理程序。目前,Linux-x86和Linux-Alpha只是简单的确认,
忽略任何在预料之外的中断。

安装中断处理程序
       中断信号线是宝贵并且非常有限的资源,当只有15或16根中断信号线时尤其如此
。内核维护了一个类似于I/O端口注册表的中断信号线的注册表。一个模块可以申请一个
中断通道(或中断请求IRQ,即Interrupt ReQuest),并且,处理完以后还可以释放掉
它。在<linux/sched.h>头文件中申明的下列函数实现了这个接口:

 

int request_irq(unsigned int irq, 

void (*handler)(int, void*, struct pt_regs *), 

unsigned long flags, 

const char *device, void *dev_id);

void free_irq(unsigned int irq, void *dev_id);

 

       注意,1.2版定义了不同的原型。相关的移植问题可参见本章稍后的“IRQ处理程
序的版本相关性”一节。

 

       通常,申请中断的函数的返回值为0时表示成功,或者返回一个负的错误码。函
数返回-EBUSY通知另一个设备驱动程序已经使用了要申请的中断信号线的情况并不常见
。函数参数定义如下:

 

unsigned int irq

该参数为中断号。有时从Linux中断号到硬件中断号的映射并不是一对一的。例如,在ar
ch/alpha/kernel/irq.c文件中可以查看到Alpha上的映射。这里,传递给内核函数的参
数是Linux中断号而不是硬件中断号。

 

void (*handler)(int,void *,struct pt_regs *)

       指向要安装的中断处理函数的指针。

 

unsigned long flags

       如你所想,这是一个与中断管理有关的各种选项的字节掩码。

 

const char *device

传递给request_irq的字符串,在/proc/interrupts中用于显示中断的拥有者(参见下一
节)。

 

void *dev_id

这个指针用于共享的中断信号线。它是一个唯一的标志符,更象一个ClientDate(C++中
的this对象)。设备驱动程序可以自由地任意使用dev_id。除非强制使用中断共享,dev
_id通常被置为NULL。在后面的“实现一个处理程序”一节中,我们将看到一个使用dev_
id的实际例子。

 

在flags中可以设置的位是:

 

SA_INTERRUPT

如果设置该位,就指示这是一个“快速”中断处理程序;如果清除这位,那么它就是一
个“慢速”中断处理程序。快速中断处理程序和慢速中断处理程序的概念在下面的“快
速和慢速处理程序”一节中会谈到。

 

SA_SHIRQ

       该位表明中断可以在设备间共享。共享的概念在稍后的“中断共享”一节中介绍


 

SA_SAMPLE_RANDOM

该位表明产生的中断对/dev/random和/dev/urandom设备要使用的熵池(entropy pool)有
贡献。读这些设备返回真正的随机数,它们用来帮助应用软件选取用于加密的安全钥匙
。这些随机数是从一个熵池中取得的,各种随机事件都会对系统的熵池(无序度)有贡献
。如果你希望设备真正随机地产生中断,你应该置上这个标志。而如果你的中断是可预
测的(例如,帧捕捉卡的垂直消隐),那就不值得设置这个标志位-它对系统的熵池没有
任何贡献。更详尽的信息可参见drivers/char/random.c文件中的注释。

 

       中断处理程序可以在驱动程序初始化时或者在设备第一次打开时安装。虽然在in
it_module函数中安装中断处理程序听起来是个好主意,但实际上并非如此。因为中断信
号线数量有限,你不会想浪费它们的。你的计算机拥有的设备通常要比中断信号线多。
如果一个设备模块在初始化就申请了一个中断,会阻碍其它驱动程序使用这个中断,即
便这个设备根本不使用它占用的这个中断。而在打开设备时申请中断,则允许资源有限
的共享。

 

       例如,只要你不同时使用帧捕捉卡和调制解调器这两个设备,它们使用同一个中
断就是可能的。用户经常在系统启动时装载某个特殊设备的模块,即使这个设备很少使
用。数据采集卡可以和第二个串口使用同一个中断。尽管在进行数据采集时避免去连你
的ISP并不是件难事,但在使用调制解调器前不得不先卸载一个模块太令人不愉快了。

 

       调用request_irq的正确位置是在设备第一次打开,硬件被指示产生中断前的时
候。而调用free_irq的位置是设备最后关闭,硬件被通知不要再中断处理器后的时候。
该技术的缺点是你必须为每个设备维护一个记录其打开次数的计数器。而如果你在同一
个模块中控制两个以上的设备,那么仅仅使用模块计数器那还不够。

 

       尽管我已说了这么多,short却是在装载时申请中断信号线的。我这样做是为了
使你在运行测试程序时不必运行其它进程来使设备保持打开的状态。因此,short会象真
正的设备那样,在init_module中而不是short_open中申请中断。

 

       下面这段代码要申请的中断是short_irq。对这个变量的赋值将在后面再给出,
因为它与现在的讨论无关。short_base是使用的并口的I/O基地址;写接口的2号寄存器
打开中断报告。

 

       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);

              }

       }

 

       这段代码显示安装的处理程序是个快速中断处理程序(SA_INTERRUPT),不支持中
断共享(没有设置SA_SHIRQ),并且对系统熵池无贡献(没有设置SA_SAMPLE_RANDOM)。然
后调用outb打开并口的中断报告。

/proc接口
       当处理器被硬件中断时,一个内部计数器会被加1,这为检查设备是否正常工作
提供了一个方法。报告的中断显示在文件/proc/interrupts中。下面是我的486启动一个
半小时(uptime)后该文件的一个快照:

 

 0:     537598   timer

 1:      23070   keyboard

 2:          0   cascade

 3:       7930 +  serial

 5:       4568   NE2000

 7:      15920 + short

13:          0   math error

14:      48163 + ide0

15:       1278 + ide1

 

       第一列是IRQ中断号。你可以从显示中缺少一些中断推知该文件只会显示已经安
装了驱动程序的那些中断。例如,第一个串口(使用中断号4)没有显示,这表明我现在没
有使用调制解调器。实际上,即使我在获取这个快照之前使用过调制解调器,它也不会
出现在这个文件中;串口的行为很良好,当设备关闭时会释放它们的中断处理程序。出
现在各记录中的加号标志该行中断采用了快速中断处理程序。

 

       /proc树中还包含了另一个与中断有关的文件,/proc/stat;有时你可能会发现
一个文件更有用,但有时又更愿意使用另一个。/proc/stat文件记录了关于系统活动的
一些底层的统计信息,包括(但不仅限于)只系统启动以来接收到的中断次数。stat文件
的每一行都以一个字符串开始,它是该行的关键字;intr标记正是我们要找的。下面的
快照是在得到前面那个快照后半分钟获得的:

 

intr 947102  540971  23346 0 8795  4907  4568  0  15920  0 0 0 0 0 0  48317 
1278

 

       第一个数是总的中断次数,而其它每个数都代表一个中断信号,从0号中断开始
。上面的快照显示4号中断被使用了4907次,虽然当前它的处理程序没有安装上。如果你
测试的驱动程序是在每次打开和关闭设备的循环中获取和释放中断的话,那么你会发现/
proc/stat文件要比/proc/interrupts文件更有用。

 

       两个文件另一处不同是interrupts文件与体系结构无关,而stat文件则与体系结
构有关:其字段的个数取决于内核之下的硬件。可以获取的中断个数在Sparc上只有15个
,而在Atari(M68k处理器)上则多达72个。

 

下面的快照给出我的Alpha工作站(共有16个中断,和x86机器一样)上的文件内容:

 

1:       2  keyboard

5:    4641  NE2000

   15:   22909 + 53c7,8xx

 

intr  27555 0 2 0 1 1 4642 0 0 0 0 0 0 0 0 0 22909 

 

       这个输出的最值得注意的地方是不出现时钟中断。在Alpha机器上,时钟中断与
其它中断到达处理器的方式不同,没有分配IRQ中断号。

自动检测中断号
       驱动程序初始化时最迫切的问题之一就是如何决定设备要使用哪条中断信号线。
驱动程序需要该信息以便安装正确的处理程序。虽然程序员可以要求用户在装载是指定
中断号,但这并不好,因为一般用户并不知道中断号,或者是因为他没有配置跳线或者
因为该设备根本就没有跳线。自动检测中断号是对驱动程序使用的基本要求。

 

       有时自动检测依赖于一些设备拥有的较少改变的缺省特性。此时,驱动程序可以
就假定设备使用了这些缺省值。short在检测并口时就正是这么作的。正如short的代码
中所给出的,实现起来相当简明:

 

       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地址来分配中断号,但也允许用户在装载驱动程序时通
过调用insmod short short_irq=x来覆盖缺省值。short_base缺省为0x378,因此short_
irq缺省为7。

 

       有些设备设计得更为先进,会简单地“声明”它们要使用那个中断。此时,驱动
程序可以通过读设备的某个I/O端口的一个状态字节来获得中断号。当目标设备能告述设
备要使用哪个中断时,那么自动检测中断号就是探测设备,不需要额外工作来探测中断


 

       值得注意的是,现代的设备能提供自己的中断配置信息。PCI标准通过要求外围
设备声明要使用的中断信号线的方法来解决这个问题。关于PCI标准的讨论可参见第15章
“外设总线概貌”。

 

       遗憾的是,不是所有设备都对程序员友好,自动检测可能还是需要一些探测的。
技术很简单:驱动程序告诉设备产生中断,然后观察会发生些什么。如果一切正常,那
么只有一条中断信号线被激活了。

 

       尽管探测在理论上很简单,实际的实现则并不那么简明。下面我们看看执行该任
务的两种方法:调用内核定义的帮助函数和实现我们自己的版本。

核心帮助下的检测
       主流的内核版本都提供探测中断号的底层工具。这种工具包括两个函数,都在头
文件<linux/interrupt.h>中声明(该头文件也描述了探测的机制):

 

unsigned long probe_irq_on(void);

这个函数返回尚未分配的中断的位掩码。驱动程序必须保留返回的位掩码以便随后能将
它传递给probe_irq_off函数。调用该函数后,驱动程序要安排相应设备至少产生一次中
断。

 

int probe_irq_off(unsigned long);

在设备已经申请了中断之后,驱动程序要调用这个函数,传递给它的参数是先前调用pro
be_irq_on返回的位掩码。probe_irq_off返回“启动探测”后发出的中断次数。如果没
有发生任何中断,就返回0(因此无法探测0号中断,但在能支持的所有体系结构上也没有
什么定制设备能使用它)。如果产生了多次中断(二义性检测),probe_irq_off将返回一
个负值。

 

       程序员要注意在调用probe_irq_on后启动设备,并在调用probe_irq_off后关闭
它。此外,在调用probe_irq_off之后,不要忘了处理你的设备尚未处理的那些中断。

 

       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); /* 关闭中断报告 */

short_irq=probe_irq_off(mask);

 

       if (short_irq==0){ /* 没有探测到中断报告?*/

              printk(KERN_INFO "short: no irq reported by probe\n");

              short_irq=-1;

       }

       /*

* 如果激活了一个以上的中断,结果就是负的。我们将为中断提供服务(除非是lpt

* 端口)并且再次进行循环。最多循环5次,然后放弃

*/

       } while (short_irq<0 && count++<5);

       if (short_irq<0)

              printk("short: probe failed %i times, giving up\n",count);

 

       探测很耗时。尽管short的探测很快,但象探测帧捕捉卡,就至少需要延迟20ms(
相对处理器时间就太长了),而探测其它设备可能会更花时间。因此,最好就只在模块初
始化时探测中断信号线一次,不管你是在打开设备时(你应该这样做)或者在init_module
中(你无论如何不应该这样做)安装你的中断处理程序的。

 

       值得注意的是,在Sparc和M68k上,中断探测全无必要,因此也不必实现。探测
是种“黑客”行为,象PCI这样的成熟的体系结构会提供所有必要的信息。实际上,M68k
和Sparc的内核开放给模块桩(stub)的探测函数总是返回0——每种体系结构都必须定义
这些函数,因为它们是由体系结构无关的源文件来开放的。所有其它的体系结构都允许
使用上面给出的探测技术。

 

       probe_irq_on和probe_irq_off的问题是早期的内核版本并不开放这两个函数。
因此,如果你希望写的模块能移植到1.2版的内核,你必须自己做中断探测。

DIY(Do It Yourself自己做)检测
       探测也可以有驱动程序自己较容易地实现。如果装载是指定probe=2,short模块
将对中断信号线进行DIY检测。

 

       实现机制和前面讨论的内核帮助下的检测是一样的:启动所有未被占用的中断,
然后等着看会发生些什么。但我们可以利用拥有的对设备的一些知识。通常一个设备可
以配置成使用3或4个中断号中的一个;只需要探测这些中断号,这使我们不必测试所有
可能的中断号就可以检测到正确的中断号。

 

       在short的实现中假定可能的中断号只有3,5,7和9。这些数值实际上是一些并
口允许你选取的值的范围。

 

       下面的代码通过测试所有“可能的”中断和会观察发生什么来进行中断探测。tr
ials数组列出所有要尝试的中断号,0是该列表的结束标志;trials数组用于记录实际上
哪个处理程序被驱动程序注册了。

 

       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(0x10, short_base+2); /* 关闭 */

 

       /* 处理程序已经设置了这个值 */

       if  (short_irq==0) { /*

              printk(KERN_INFO "short: no irq reported by probe\n");

       }

/*

* 如果激活了一个以上的中断,结果就是负的。我们将为中断提供服务(除非是lpt

* 端口)并且再次进行循环。最多这样做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[]。为了探测所有的中断,你不得不从0号中断探测到NR_IRQS-1号中断,NR_IR
QS是在头文件<asm/irq.h>中定义的与平台无关的常数。

 

现在缺的就是探测处理程序自己了。该处理程序的功能就是根据实际接收到的中断号来
更新short_irq变量。short_irq值为0意味着“什么也没有”,而负值意味着存在“二义
性”。我选取这些值是为了和probe_irq_off保持一致,并可以在short.c中使用同样的
代码来调用任何一种探测方法。

 

void 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; /* 有二义性 */

}

 

处理程序的参数稍后会介绍。知道参数irq是要处理的中断号就足以理解上面的函数了。


快速和慢速中断处理
       你已经看到,我为short的中断处理程序设置了SA_INTERRUPT标志位,因此是请
求安装一个快速中断处理程序。现在到解释什么是“快速”和“慢速”的时候了。实际
上,不是所有的体系结构都支持快速和慢速中断处理程序两种实现的。例如,Alpha和Sp
arc的移植版本,快速和慢速处理程序是一样处理的。2.1.37版和其后的Intel移植版本
也消除了两者的差别,因为现代处理器的可以获得的处理能力使得我们不必再区分出快
速和慢速两种中断。

 

       这两种中断处理程序的主要差别就在于,快速中断处理程序保证中断的原子处理
,而慢速中断处理程序则不保证(这种差别在最新的中断处理的实现也保留了)。也就是
说,“开启中断”处理器标志位(IF)在运行快速中断处理程序时是关闭的,因此在服务
该中断时不允许被中断。而调用慢速中断处理时,内核启动微处理器的中断报告,因此
在运行慢速中断处理程序时其它中断仍可以得到服务。

 

       在调用实际的中断处理程序之前,不管是快速还是慢速中断处理程序,内核都要
执行一项任务,关闭刚才发出报告的那个中断信号线。这对程序员是个好消息-中断服
务例程不必是可重入的。但另一方面,即使是慢速中断处理程序也要实现得运行的尽可
能快,以免丢失后面到达的中断。

 

       当处理程序还在处理上一个中断时,如果设备又发出新的中断,新的中断会永远
丢失。中断控制器并不缓存被屏蔽的中断,但是处理器会进行缓存-一旦发出sti指令,
待处理的中断就会得到服务。sti函数是“置中断标志位”处理器指令(是在第2章“编写
和运行模块”的“ISA内存”一节引入的)。

 

       总结快速和慢速两种执行环境如下:

 

l      快速中断处理程序运行时微处理器关闭了中断报告,中断控制器禁止了被服务这
个中断。但处理程序可以通过调用sti来启动处理器的中断报告。

 

l      慢速处理程序运行时启动了处理器的中断报告,但中断控制器也禁止了被服务这
个中断。

 

但快速和慢速中断处理程序还有另一处不同:内核带来的额外开销。慢速中断处理程序
之所以慢是因为内核带来的一些管理开销造成的。这意味着较频繁的中断最好由快速中
断处理程序为之提供服务。至于short,当把大文件拷贝到/dev/short0时每秒会产生上
千次中断。因此我选择使用了一个快速中断处理程序来控制添加给系统的开销。这种分
别在更新的2.1版的内核中已经得到统一;这个开销现在加到了所有的中断处理程序上。


 

帧捕捉卡是使用慢速中断处理程序的一个好的候选者。它每秒只中断处理器50到60次,
选择使用慢速处理程序将帧数据从接口卡拷贝到物理内存就不会阻塞住其它的系统中断
,例如那些由串口或定时器服务产生的中断。

x86平台上中断处理的内幕
       下面的描述是根据2.0.x版本的内核中的两个文件arch/i386/kernel/irq.c和inc
lude/asm-i386/irq.h推断的;虽然基本概念是相同的,但是具体的硬件细节与平台有关
,并且在2.1开发版本中有些修改。

 

       最底层的中断处理是在头文件irq.h中的声明为宏的一些汇编代码,这些宏在文
件irq.h中被扩展。为每个中断声明了三种处理函数:慢速,快速和伪(bad)处理函数。

 

       “伪”处理程序,它最小,是当没有为中断安装C语言的处理程序时的汇编入口
点。它将中断转交给适当的PIC(Programmable Interrupt Controller,可编程的中断控
制器)设备*的同时禁止它,以避免由于伪中断而进一步浪费处理器时间。在驱动程序处
理完中断信号后调用free_irq时又会重新安装伪处理程序。伪处理程序不会将/proc/sta
t中的计数器加1。

 

       值得注意的是,在x86和Alpha上的自动探测都是依赖于伪处理程序的这种行为。
probe_irq_on启动所有的伪中断,而不安装处理程序;probe_irq_off只是简单地检查自
调用probe_irq_on以来那些中断被禁止了。如果你想验证这一点,可以在装载short时指
定probe=1(内核帮助下的检测),此时可观察到中断计数器没有加1,而如果装载时指定p
robe=2(DIY检测)则会将它们加1。

 

       慢速中断的汇编入口点会将所有寄存器保存到堆栈中,并将数据段(DS和ES处理
器寄存器)指向核心地址空间(处理器已经设置了CS寄存器)。然后代码将将中断转交给PI
C,禁止在相同的中断信号线上触发新的中断,并发出一条sti指令(set interrupt 
flag,置中断标志位)。注意处理器在对中断进行服务时会自动清除该标志位。接着慢速
中断处理程序就将中断号和指向处理器寄存器的一个指针传递给do_IRQ,这是一个C函数
,由它来调用相应的C语言处理程序。驱动程序传递给中断处理程序参数struct 
pt_regs *是一个指向存放着各个寄存器的堆栈的指针。

 

       do_IRQ结束后,会发出cli指令,打开PIC中指定的中断,并调用ret_from_sys_c
all。最后这个入口点(arch/i386/kernel/entry.S)从堆栈中恢复所有的寄存器,处理所
有待处理的下半部处理程序(参见本章的“下半部”一节),并且,如果需要的话,重新
调度处理器。

 

       快入口点不同的是,在跳转到C代码之前并不调用sti指令,并且在调用do_fast_
IRQ前并不保存所有的机器寄存器。当驱动程序中的处理程序被调用时,regs参数是NULL
(空指针,因为寄存器没有保存到堆栈中)并且中断仍被屏蔽。

 

       最后,快速中断处理程序会重新打开8259芯片上的所有中断,恢复先前保存的所
有寄存器,并且不经过ret_from_sys_call就返回了。待处理的下半部处理程序也不运行


 

       2.1.34前的所有内核版本中,这两种处理程序在将控制转移给C代码前都会将int
r_count变量加1(参见第6章“时间流”的“任务队列的特性”一节)。

实现中断处理程序
       至此,我们学习了如何注册一个中断处理程序,但还并没有真正编写这样的一个
处理程序。实际上,处理程序并没有什么特别的-就是普通的C代码。

 

       唯一特别的地方就是处理程序是在中断时间内运行的,因此它的行为要受些限制
。这些限制和我们在任务队列中看到的差不多。处理程序不能向用户空间发送或接受数
据,因为它不在任何进程的上下文中执行。快速中断处理程序,可以认为是原子地执行
的,当访问共享的数据项时并不需要避免竞争条件。而慢速处理程序不是原子的,因为
在运行慢速处理程序时也能为其它处理程序提供服务。

 

       中断处理程序的功能就是将有关中断接收的信息反馈给设备,并根据要服务的中
断的不同含义相应地对数据进行读写。第一步通常要先清除接口卡上的一个位;大部分
硬件设备在它们的“中断待处理”位被清除前是不会产生任何中断的。一些设备就不需
要这一步,因为它们没有“中断待处理”位;这样的设备比较少,但并口却是其中之一
。因此,short不需要清除这样的位。

 

       中断处理程序的典型任务是唤醒在设备上睡眠的那些进程——如果中断向这些进
程发出了信号,指示它们等待的事件已经发生,比如,新数据到达了。

 

       还举老的帧捕获卡的例子,进程可以通过连续地对设备读来获取一系列的图像;
每读一帧后read调用都被阻塞,而新的帧一到达中断处理程序都会唤醒该进程。这假定
了捕获卡会中断处理器来发出信号通知每一帧的成功到达。

 

       不论是快速还是慢速中断处理程序,程序员都要注意处理例程的执行时间必须尽
可能短。如果要进行长时间的计算,最好的方法是使用任务队列,将计算调度到安全时
间内进行(参见第6章的“任务队列”一节)。这也是需要下半部处理的一个原因(参见本
章稍后的“下半部”)。

 

       short中的范例代码使用中断来调用do_gettimeofday并把当前时间打印到大小为
一页的循环缓冲区。然后它唤醒所有的读进程(实际上由于short使用快速中断处理程序
,这些读进程只会在下一个慢速中断处理程序结束时或下一个时钟滴答时醒来)。

 

void short_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    struct timeval tv;

    do_gettimeofday(&tv);

 

    /* 写一个16个字节的记录。假设 PAGE_SIZE是16的倍数 */

    short_head += sprintf((char *)short_head,"%08u.%06u\n",

                          (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));

    if (short_head == short_buffer + PAGE_SIZE)

        short_head = short_buffer; /* 绕回来 */

 

    wake_up_interruptible(&short_queue); /* 唤醒所有的读进程 */

}

 

       这段代码,尽管简单,却给出了一个中断处理程序的典型工作流程。

 

       用来读取在中断时间里填满的缓冲区的节点是/dev/shortint。这是唯一的没有
在第8章中介绍的short设备节点。/dev/shortint内部的实现为中断产生和报告作了特别
的处理。每向设备写入一个字节都会产生一个中断;而读设备时则给出每次中断报告的
时间。

 

       如果你将并口插座的第9和第10引脚相连,那么拉高并行数据字节的最高位就可
以产生中断。这可以通过向/dev/short0写二进制数据或者向/dv/shortint*写入任意数
据来实现。

 

       下面的代码是/dev/shortint的read和write的实现:

 

read_write_t short_i_read (struct inode *inode, struct file *filp,

                char *buf, count_t count)

{

    int count0;

 

    while (short_head == short_tail) {

        interruptible_sleep_on(&short_queue);

        if (current->signal & ~current->blocked) /* 有信号到达 */

          return -ERESTARTSYS; /* 通知fs层去处理它 */

        /* 否则,再次循环 */

    }

    /* count0 是可以读进来的数据字节个数 */

    count0 = short_head - short_tail;

    if (count0 < 0) /* wrapped */

        count0 = short_buffer + PAGE_SIZE - short_tail;

    if (count0 < count) count = count0;

 

    memcpy_tofs(buf, (char *)short_tail, count);

    short_tail += count;

    if (short_tail == short_buffer + PAGE_SIZE)

        short_tail = short_buffer;

    return count;

}

 

read_write_t short_i_write (struct inode *inode, struct file *filp,

                const char *buf, count_t count)

{

    int written = 0, odd = filp->f_pos & 1;

    unsigned port = short_base; /* 输出到并口数据锁存器 */

 

    while (written < count)

        outb(0xff * ((++written + odd) & 1), port);

 

    filp->f_pos += count;

    return written;

}

使用参数
       虽然short中不对参数进行处理,但还是有三个参数被传给了中断处理函数:irq
,dev_id和regs。下面我们看看每个参数的意义。

 

       当用一个处理程序来同时对若干个设备进行处理并且使用不同的中断信号线,那
么中断号(int irq)就很有用了。例如,立体视频系统就使用了两个中断来支持两个帧捕
捉卡。驱动程序必须能检测两个设备,并且安装一个处理程序来对两个中断进行处理。
驱动程序就可以使用irq参数来通知处理程序是哪个设备发出了中断。

 

       例如,如果驱动程序声明了一个设备结构的数组hwinfo,每个元素都有一个irq
域,那么下面的代码可以在中断到达时选取出正确的设备。这段代码的设备前缀是cx。

 

static void cx_interrupt(int irq)

{

       /* “Cxg_Board”是硬件信息的数据类型 */

       Cxg_Board *board; int i;

       

       for (i=0, board=hwinfo; i>cxg_boards; board++,i++)

              if  (board->irq==irq)

                     break;

 

       /* 现在'board' 指向了正确的硬件描述 */

       /* .... */

}

 

       第二个参数,void *dev_id,是一种ClientData;是传递给request_irq函数的
一个void *类型的指针,并且当中断发生时这个设备ID还会作为参数传回给处理程序。
参数dev_id是在1.3.70版的Linux中引入以处理共享中断,但即使不共享它也很有用。

       假定我们例子中的设备是象下面这样注册它的中断的(这里board->irq是要申请
的中断,board是ClientData)

 

static void cx_open(struct inode *inode, struct file *filp)

{

       Cxg_Board *board=hwinfo+MINOR(inode->i_rdev);

       Request_irq(board->irq, cx_interrupt, 0, "cx100", board /* dev_id 
*/);

       /* .... */

       return 0;

}

 

       这样处理程序的代码就可以缩减如下:

 

static void cx_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

       Cxg_Board *board=dev_id;

 

       /* 现在'board' 指向了正确的硬件项 */

       /* .... */

}

 

       最后一个参数,struct pt_regs *regs,很少使用。它存放着在处理器进入中断
代码前的一个处理器上下文的快照。这些寄存器可用于监控和调试,实际上show_regs函
数(它是按下RightAlt-PrScr键时由键盘中断启动的调试函数-第4章“调试技术”的“
系统挂起”一节)就是使用它们来实现监控和调试的。

打开和禁止中断
       有时驱动程序要打开和禁止它相应IRQ信号的中断报告。内核为此提供了两个函
数,都在头文件<asm/irq.h>中声明:

 

void disable_irq(int irq);

void enable_irq(int irq);

 

       调用其中任一函数都会更新PIC中对指定的irq的掩码。

 

       实际上,当中断被禁止后,那么即使硬件急需处理,处理器也得不到报告。例如
,“x86上中断处理的内幕”一节中就介绍了“伪”处理程序在x86上的实现就禁止了它
收到的所有中断。

 

       但是,为什么我们要禁止中断呢?还是举并口的例子,我们看看plip(并行IP)网
络接口。plip设备使用裸的(bare-bones)并口来传输数据。因为只能从并口读出5个位,
它们就被解释为四个数据位和一个时钟/握手信号。当发起者(即发送数据包的那个接口)
送出数据包的第一个位时,时钟信号会升高,接收方接口就会中断处理器。然后plip处
理程序被调用来处理新到达的数据。

 

       在设备被激活后,开始数据传输,使用握手信号将新数据按时钟周期传送给接收
接口(这可能不是最好的实现方法,但只有这样才能和其它使用并口的数据包驱动程序兼
容)。如果接收接口为接收每个字节(8个位)都要处理两次中断,那性能必然不可忍受。
因此驱动程序在接收数据包时要禁止中断。

 

       同样的,因为从接收方到发送方的握手信号用于确认数据的接收,发送接口也要
在发送数据包时禁止它的中断信号。

 

       但要注意的是,因为处理程序本身无法打开和禁止中断信号。存在这个限制是因
为,如上所述,内核在调用处理程序前会禁止中断,而在处理程序结束后又会重新打开
它。但打开和禁止中断仍可以做到,只要在下半部处理程序中作就可以了(参见下一节)


 

       最后值得注意的是,在Sparc实现中,disable_irq和enable_irq都被定义为指针
而不是函数。这个小技巧允许内核在启动检测你是在运行哪种Sparc时对指针进行相应的
赋值(Sun4c和Sun4m的中断硬件不相同)。而所有的Linux系统上,不管使不使用这种小技
巧,函数在C语言中的语义都相同,这就避免了编写那些冗长无味的条件编译代码。

下半部
中断处理的一个主要问题是如何在处理程序中完成比较耗时的任务。Linux解决这个问题
的方法是将中断处理程序划分成两个部分:所谓的“上半部”是你通过request_irq函数
注册的处理例程,而“下半部”(bottom half,简称为“bh”)则是由上半部调度到以后
在更安全的时间内执行的那部分例程。

 

但是下半部有什么用呢?

 

上半部和下半部处理程序最大的不同就在于在执行bh是所有的中断都是打开的---所以
说它是在“更安全”时间内运行。典型的情况是,上半部处理程序将设备数据存放进一
个设备指定的缓冲区,再标记它的下半部,然后退出;这样处理得就非常快。由bh将新
到的数据再分派给各个进程,必要时再唤醒它们。这种设置允许上半部处理程序在下半
部还在运行时就能为新的中断提供服务。但另一方面,在上半部处理程序结束前如果有
新的数据到了,由于中断控制器禁止了中断信号,这些数据仍会丢失掉。

 

所有实际的中断处理程序都作了这样的划分。例如,当网络接口卡报告新的数据包到达
了,处理程序只是取得数据并将它推进协议层中;对数据包的实际处理是在下半部中完
成的。

 

这使我们想起了任务队列;实际上,任务队列就是从下半部的一个较老的实现演变而来
的。甚至1.0版的内核也有下半部,而任务队列则还未引入。

 

与动态的任务队列不同,下半部的个数有限,并由内核预定义了;这和老的内核定时器
有些类似。下半部的静态特性并不是个问题,因为有些下半部可以通过运行任务队列演
变为动态对象。在头文件<linux/interrupt.h>中,你可以看到下半部的一张列表;它们
的最有意思的一部分将在下面讨论。

下半部的设计
下半部由一个函数指针数组和一个位掩码组成-这就是为什么它们不超过32个的原因。
当内核准备处理异步事件时,它就调用do_bottom_half。我们已经在前面看到,从系统
调用返回和退出慢速处理程序时,内核都是这样做的;而这两类事件都发生得很频繁。
而决定使用掩码主要出于性能的考虑:检查掩码只要一条机器指令,开销最小。

 

当某段代码需要调度运行下半部处理时,只要调用mark_bh即可,该函数设置了掩码变量
的一个位以将相应的函数进入执行队列。下半部可以由中断处理程序或其它函数来调度
。执行下半部时,它会自动去除标记。

 

标记下半部的函数是在头文件<linux/interrupt.h>中定义的:

 

       void mark_bh(int nr);

 

       这里,nr是激活的bh的“数目”。这个数是在头文件<linux/interupt.h>中定义
的一个符号常数,它标记位掩码中要设置哪个位。每个下半部bh相应的处理函数由拥有
它的那个驱动程序提供。例如,当调用mark_bh(KEYBOARD_BH)时,要调度执行的函数是k
bd_bh,它是键盘驱动程序的一部分。

 

       因为下半部是静态对象,模块化的驱动程序无法注册自己的下半部。目前还不支
持下半部的动态分配,可能将来也不会支持,因为此时可以使用立即队列。

 

       本节其余部分将列出一些有意思的下半部:

 

IMMEDIATE_BH

对设备驱动程序来说这是最重要的bh。被调度执行的函数处理任务队列tq_immediate。
没有下半部的驱动程序(例如一个定制模块)可以通过使用立即队列来取得和立即bh同样
的效果。将任务等记到队列中后,驱动程序必须标记bh以使得它的代码真正得到执行;
具体做法可参见第6章的“立即队列”一节。

 

TQUEUE_BH

如果任务等记在tq_timer队列中,那么每次时钟滴答都会激活这个bh。实际上,驱动程
序可以使用tq_timer来实现自己的下半部;定时器队列是在第6章(“定时器队列”一节
中)引入的一种下半部,但并不必为它调用mark_bh。TQUEUE_BH总是在IMMEDIATE_BH后执
行的。

 

NET_BH

网络驱动程序通过标记这个队列来将事件通知上面的网络层。bh本身是网络层的一部分
,模块无法访问。我们将在第14章“网络设备驱动程序”的“中断驱动的操作”一节中
熟悉它的使用。

 

CONSOLE_BH

控制台是在下半部中进行终端tty切换的。这个操作要包含进程控制。例如,在X 
Window系统和字符模式间切换就是由X 服务器控制的。而且,如果键盘驱动程序请求控
制台的切换,那么控制台切换不能在中断时进行。也不能在进程向控制台写的时候进行
。使用bh就能满足这些要求,因为驱动程序可以任意禁止下半部;如果发生了前面情况
,在写控制台时禁止console_bh即可*。

 

TIMER_BH

这个bh由do_timer函数标记;do_timer函数管理着时钟滴答。这个bh要执行的函数正是
驱动内核定时器的那个函数。因此不使用add_timer的驱动程序是无法使用这种功能的。


 

       其余的下半部是有特定的内核驱动程序使用的。没有为模块提供入口点,即使有
入口也没什么意义。

 

       bh一旦被激活,当在return_from_sys_call中调用do_bottom_half(kernel/soft
irq.c)时它就会得到执行。当进程退出系统调用或慢速中断处理程序退出时都会执行ret
urn_from_sys_call过程。快速中断处理程序退出时就不会执行下半部;如果驱动程序需
要快速执行它的下半部。它必须注册一个慢速处理程序。

 

       时钟滴答总要执行ret_from_sys_call的;因此,如果快速中断处理程序标记了
一个bh,实际的处理函数最多10ms后就会被执行(Alpha上则小于1ms,它时钟滴答的频率
是1024Hz)。

 

       下半部运行后,如果设置了need_resched变量,就会调用调度器;各种wake_up
函数都会设置这个变量。因此,上半部可以将任何与被唤醒的进程有关的任务放到下半
部去做-这些任务马上就会被调度。例如,当telnet数据包到达网络时就是这样的。net
_bh唤醒telnetd,并且调度器马上给它处理器时间,因此没有额外的延迟。

编写下半部
       下半部代码是在安全时间内运行的——比上半部处理程序运行时安全。但是,也
有些注意事项,因为bh还是在“中断时间”内处理的。intr_count不为0,因为下半部是
在进程上下文之外执行的。因此,第6章的“任务队列的特性”一节中列出的各种限制也
适用于在下半部中执行的代码。

 

       下半部的主要问题是它们通常要与上半部中断处理程序共享数据结构,因此要避
免竞争条件。这意味着要暂时禁止中断或者使用锁的技术。

 

       从前面“下半部的设计”一节中给出的下半部列表可以明显看出,新编写的实现
了下半部的驱动程序应该通过使用立即队列来将它的代码挂在IMMEDIATE_BH上。如果你
的驱动程序很关键,那么甚至可以拥有从内核里分配的bh号。但这样的驱动程序比较少
,因此我就不详细介绍了。共有三个函数可用于管理自己私有的下半部:init_bh,enab
le_bh和disable_bh。如果你有兴趣的话,可以在内核源码找到它们。

 

       实际上,使用立即队列和管理自己拥有的下半部并无区别-立即队列也是一种下
半部。当标记了IMMEDIATE_BH后,处理下半部的函数实际上就是去处理立即队列。如果
你的中断处理函数将它的bh处理函数排进tq_immediate队列并且标记了下半部,那么队
列中的这个任务会正确地被执行。因为所有最新的内核都可以将相同的任务多次排队而
不破坏任务队列,因此每次运行上半部处理函数时都可以将下半部排队。稍后我们会看
到这种做法。

 

       需要特殊配置的驱动程序——需要多个下半部或不能简单地用tq_immediate来设
置——可以使用定制的任务队列。中断处理函数将任务排进自己的队列中,当它准备运
行这些任务时,就将一个简单的对队列进行处理的函数插入立即队列。详情可参见第6章
的“运行自己的任务队列”一节。

 

       下面让我们看看short的实现。装载时如果指定bh=1,那么模块就会安装一个使
用了下半部的中断处理函数。

 

       short是这样对中断处理进行划分的:上半部(中断处理函数)将当前时间保存到
一个循环缓冲区中并调度下半部。而bh将累积的各个时间值打印到一个字符缓冲区,然
后唤醒所有的读进程。

 

       最后上半部非常简单:

 

void short_bh_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    do_gettimeofday(tv_head);

    tv_head++;

 

    if (tv_head == (tv_data + NR_TIMEVAL) )

        tv_head = tv_data; /* wrap */

 

    /* 将bh排队。即使被多次排队也没有关系 */

    queue_task_irq_off(&short_task, &tq_immediate);

    mark_bh(IMMEDIATE_BH);

 

    short_bh_count++; /* 记录一个新的中断到了 */

}

 

       正如我们所料,这段代码调用queue_task而不会检查任务是否已被排进队列。但
在Linux 1.2中不能这么做,并且如果你是用1.2版的头文件来编译short,那么它会使用
不同的处理函数,仅当short_bh_count为0时该函数才会将任务排队。

 

然后,下半部处理剩下的工作。它也记录下在调度下半部前上半部被激活的次数(saveco
unt)。如果上半部是一个“慢速”处理函数,那么这个数总为1,因为如上所述,当慢速
处理函数退出时,总会运行待处理的下半部。

 

void short_bottom_half(void *unused)

{

    int savecount = short_bh_count;

    short_bh_count = 0; /* 我们已经从队列中删去*/

    /*

    * 下半部读入由上半部填充的tv数组,并将它打印入循环的字符缓冲区,该缓冲区


    * 由读进程处理的

    */

 

    /* 首先写入在这个bh 前发生的中断的次数*/

 

    short_head += sprintf((char *)short_head,"bh after %6i\n",savecount);

    if (short_head == short_buffer + PAGE_SIZE)

    short_head = short_buffer; /* 绕回来 */

 

    /*

    *然后,写入时间值。每次写16个字节。因此与PAGE_SIZE是对齐的

    */

 

    do {

        short_head += sprintf((char *)short_head,"%08u.%06u\n",

                              (int)(tv_tail->tv_sec % 100000000),

                              (int)(tv_tail->tv_usec));

        if (short_head == short_buffer + PAGE_SIZE)

            short_head = short_buffer; /* 绕回来 */

 

        tv_tail++;

        if (tv_tail == (tv_data + NR_TIMEVAL) )

            tv_tail = tv_data; /* 绕回来 */

 

    } while (tv_tail != tv_head);

 

    wake_up_interruptible(&short_queue); /* 唤醒所有读进程 */

}

 

       在我的老式的计算上机运行时给出的时间值表明,使用下半部,两个中断间的时
间间隔从53ms减少到了27ms,因为上半部处理函数作的工作更少些。但处理中断的总的
工作量不变,更快的上半部的优点是禁止中断的时间较短。但这对short不是个问题,因
为只有在中断处理函数结束后才会重新调用产生中断的write函数(因为short采用的是快
速中断处理函数),但对真正的硬件中断来说,这个时间还是很有关系的。

 

下面是当装载short时指定bh=1你可能看到的输出结果:

 

morgana%echo 1122334455 > /dev/shortint; cat /dev/shortint

bh after       5

50588804.876653

50588804.876693

50588804.876720

50588804.876747

50588804.876774

 




--

※ 来源:.哈工大紫丁香 http://bbs.hit.edu.cn [FROM: 202.118.239.147]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:405.491毫秒