关于LINUX中的中断处理

时间:2020-12-05 14:34:34

一)、基本概念
1、安装中断处理程序
系统中中断信号线很有限,有时只有15或16根。内核维护了一个类似于I/O端口注册表的中断信号线的注册表。一个模块可以申请一个中断请求IRQ,处理完以后也可以释放掉它。相关函数:
头文件
 
原型  1)int request_irq(unsigned int irq, void (*handler)(int, void*, struct pt_regs *),
unsigned long flags, const char *device, void *dev_id);
2)void free_irq(unsigned int irq, void *dev_id);
功能 1)申请中断
2)释放中断
返回值 申请中断的函数的返回值为0时表示成功,或者返回一个负的错误码。函数返回-EBUSY通知另一个设备驱动程序已经使用了要申请的中断信号线,这种情况并不常见
参数表 unsigned int irq
中断号。某些平台上Linux中断号到硬件中断号的映射并不是一对一的。
void (*handler)(int,void *,struct pt_regs *)
指向要安装的中断处理函数的指针。
unsigned long flags
与中断管理有关的各种选项的字节掩码。
const char *device
传递给request_irq的字符串,在/proc/interrupts中用于显示中断的拥有者。
void *dev_id
共享中断信号线时用于区别的唯一的标志符,类似于C++中的this指针。设备驱动程序可以*地任意使用dev_id。除非强制使用中断共享,dev_id通常被置为NULL。
unsigned long flags
标志位,在flags中可以设置的位是:
SA_INTERRUPT
设置该位表示这是一个“快速”中断处理程序;否则就是一个“慢速”中断处理程序。
SA_SHIRQ
该位表明中断可以在设备间共享。
SA_SAMPLE_RANDOM
该位表明用于中断作于/dev/random和/dev/urandom设备使用熵池(entropy pool)的时候。可以读这些设备返回的真正的随机数,用来帮助应用软件选取用于加密的安全钥匙。这些随机数是从一个熵池中取得的,各种随机事件都会对系统的熵池(无序度)有贡献。需要设备真正随机地产生中断时才需要置上这个标志。

<!--[if !supportEmptyParas]--> <!--[endif]-->
中断处理程序可以在驱动程序初始化时或者在设备第一次打开时安装。在init_module函数中申请了一个中断、安装了中断处理程序,会阻碍其它驱动程序使用这个中断,可能形成浪费。
所以应该在打开设备时调用request_irq申请中断,在关闭设备时调用free_irq释放中断将允许资源有限的共享。该技术的缺点是你必须为每个设备维护一个记录其打开次数的计数器。如果在同一个模块中控制两个以上的设备,那么仅仅使用模块计数器那还不够。
下面这段代码要申请的中断是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打开并口的中断报告。
2、自动检测中断号
驱动程序初始化时需要确设备要使用哪条中断信号线。驱动程序需要以此来安装正确的处理程序。
有时自动检测依赖于设备使用的缺省值。例如:
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标准就要求外围设备声明要使用的中断信号线。
有些设备需要自动检测:驱动程序使用设备产生中断,然后观察哪一条中断信号线被激活了。
自动检测函数在实现时有两种方法:调用内核定义的帮助函数和实现我们自己的版本。
1)核心帮助下的检测
主流的内核版本都提供两个函数用于探测中断号。都在头文件 中声明:
unsigned long probe_irq_on(void);
函数返回未被分配的中断的位掩码。这个位掩码需要保留并需要它传递给probe_irq_off函数。
int probe_irq_off(unsigned long);
这个函数在申请了中断后使用,参数是上一函数获得的中断位掩码;函数将使设备产生中断,并返回“启动探测”后发出的中断次数。没有中断就返回0。产生了多次中断将返回一个负值。
处理时要在调用probe_irq_on后启动设备,并在调用probe_irq_off后关闭。此外,在调用probe_irq_off之后,不要忘了处理你的设备尚未处理的那些中断。如下例:
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);
探测很耗时,最好就只在模块初始化时探测中断信号线一次。
2)DIY(Do It Yourself自己做)检测
探测也可以有驱动程序自己较容易地实现。
实现机制和内核帮助下的检测是一样的,一般情况下,有些中断号已经被占用,只需要探测其它一些中断号。
例:下面的代码实现自动测试。trials数组列出所有要尝试的中断号,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);
在事先不知道哪些中断号已经被占用时。就需要探测所有空闲的中断,即从0号中断到NR_IRQS-1号中断,NR_IRQS是在头文件中定义的与平台无关的常数。
例:处理程序的功能是根据实际接收到的中断号来更新short_irq变量。0表示无,负数表示二义检测。
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; /* 有二义性 */
}
3、快速和慢速中断处理
快速中断处理程序在处理时设置了处理器标志位(IF),表示不允许被中断,这保证了中断的原子处理,而调用慢速中断处理时,其它中断仍可以得到服务。
但在中断处理前,不管是快速还是慢速中断处理程序,内核都要关闭刚才发出报告的那个中断信号线。当处理程序还在处理上一个中断时,如果设备又发出新的中断,新的中断将会丢失。中断控制器并不缓存被屏蔽的中断,但是处理器会进行缓存,快速中断处理程序运行时会关闭微处理器的中断报告,中断控制器禁止了被服务这个中断。中断处理程序在处理后可以通过调用sti来启动处理器的中断报告,微处理器就会处理被缓存的中断。sti函数是“置中断标志位”处理器指令。慢速处理程序运行时是启动了处理器的中断报告的,但中断控制器也禁止了正在被服务这个中断。
两种中断处理给内核带来的额外开销也不同。慢速中断处理程序会给内核带来的一些管理开销。因此此较频繁(每秒大于100次)的中断最好由快速中断处理程序为之提供服务。
4、x86平台上中断处理的内幕
最底层的中断处理是在头文件irq.h中的声明为宏的一些汇编代码,这些宏在文件irq.h中被扩展。为每个中断声明了三种处理函数:慢速,快速和伪处理函数。
“伪”处理程序最小,是在没有为中断安装C语言的处理程序时的汇编入口点。它将中断转交给PIC(可编程的中断控制器)设备的同时禁止它。在驱动程序处理完中断信号后调用free_irq时又会重新安装伪处理程序。伪处理程序不会将/proc/stat中的计数器加1。
在x86上的自动探测依赖于伪处理程序的这种行为。probe_irq_on启动所有的伪中断,而不安装处理程序;probe_irq_off只是简单地检查自调用probe_irq_on以来那些中断被禁止了。
慢速中断的汇编入口点会将所有寄存器保存到堆栈中,并将数据段(DS和ES处理器寄存器)指向核心地址空间(处理器已经设置了CS寄存器)。然后代码将将中断转交给PIC,禁止在相同的中断信号线上触发新的中断,并发出一条sti指令(set interrupt flag,置中断标志位)。处理器在对中断进行服务时会自动清除该标志位。接着慢速中断处理程序就将中断号和指向处理器寄存器的一个指针传递给do_IRQ,这是一个C函数,由它来调用相应的C语言处理程序。驱动程序传递给中断处理程序参数struct pt_regs *是一个指向存放着各个寄存器的堆栈的指针。
do_IRQ结束后,会发出cli指令,打开PIC中指定的中断,并调用ret_from_sys_call。最后这个入口点(arch/i386/kernel/entry.S)从堆栈中恢复所有的寄存器,处理所有待处理的下半部处理程序,如果需要的话,重新调度处理器。
快入口点不同的是,在跳转到C代码之前并不调用sti指令,并且在调用do_fast_IRQ前并不保存所有的机器寄存器。当驱动程序中的处理程序被调用时,regs参数是NULL(空指针,因为寄存器没有保存到堆栈中)并且中断仍被屏蔽。最后,快速中断处理程序会重新打开8259芯片上的所有中断,恢复先前保存的所有寄存器,并且不经过ret_from_sys_call就返回了。待处理的下半部处理程序也不运行。
5、实现中断处理程序
处理程序是在中断时间内运行的,它不在任何进程的上下文中执行,就不能向用户空间发送或接受数据。快速中断处理程序,是原子地执行的,当访问共享的数据项时并不需要避免竞争条件。而慢速处理程序不是原子的,在运行慢速处理程序时也能为其它处理程序提供服务。
中断处理程序的功能就是将有关中断接收的信息反馈给设备,并根据要服务的中断的不同含义相应地对数据进行读写。对于大部分硬件设备第一步通常要先清除接口卡上“中断待处理”位,这样硬件在该位被清除前就不会产生任何中断。而没有“中断待处理”位的设备不需要这一步,如并口。
典型中断处理程序是唤醒在设备上睡眠的那些进程,比如,新数据到达了。
老式的帧捕获卡,进程可以通过连续地对设备读来获取一系列的图像;每读一帧后read调用都被阻塞,而新的帧一到达后中断处理程序都会唤醒该进程。。
不论是快速还是慢速中断处理程序,处理例程的执行时间必须尽可能短。如果要进行长时间的计算,最好使用任务队列。
short范例使用中断来调用do_gettimeofday并把当前时间打印到大小为一页的循环缓冲区。然后它唤醒所有的读进程。
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,它内部的实现为中断产生和报告作了特别的处理。每向设备写入一个字节都会产生一个中断;而读设备时则给出每次中断报告的时间。
如果你将并口插座的第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;
}
上面的函数中有三个参数被传给了中断处理函数:irq,dev_id和regs。
当用一个处理程序来同时对若干个设备进行处理并且使用不同的中断信号线,那么中断号(int 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是可以用来处理共享中断,但即使不共享它也很有用。
假定我们例子中的设备是象下面这样注册它的中断的(这里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键时由键盘中断启动的调试函数就是使用它们来实现监控和调试的。
6、打开和禁止中断
有时驱动程序要打开和禁止它相应IRQ信号的中断报告。内核为此提供了两个函数,都在头文件中声明:
void disable_irq(int irq);
void enable_irq(int irq);
调用其中任一函数都会更新PIC中对指定的irq的掩码。
当中断被禁止后,处理器将得不到中断报告。
但要注意的是,因为处理程序本身无法打开和禁止中断信号。内核在调用处理程序前会禁止中断,而在处理程序结束后又会重新打开它。但打开和禁止中断仍可以做到,只要在下半部处理程序中作就可以了。
7、使用/proc查看
当处理器被硬件中断时,一个内部计数器会被加1,这为检查设备是否正常工作提供了一个方法。报告的中断显示在文件/proc/interrupts中。下面是该文件的一个快照:
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中断号。该文件只显示已经安装了驱动程序的那些中断。出现在各记录中的加号标志该行中断采用了快速中断处理程序。
/proc树中另一个与中断有关的文件是/proc/stat;这个文件记录了系统活动的一些底层的统计信息,包括系统启动以来接收到的中断次数。
stat文件的每一行都以一个字符串表示的关键字开始;其中intr标记表示中断相关记录,例如:
intr 947102 540971 23346 0 8795 4907 4568 0 15920 0 0 0 0 0 0 48317 1278
第一个数(947102)是总的中断次数,后面16个数字表示0~15共16个中断各自的使用次数。
interrupts文件与体系结构无关,而stat文件则与体系结构有关:其字段的个数取决于内核之下的硬件。比如在Atari(M68k处理器)上则中断号可以多达72个。
<!--[if !supportEmptyParas]--> <!--[endif]-->
(二)关于下半部
Linux将中断处理程序分成两部分:“上半部”即request_irq函数注册的处理例程,“下半部”则是由上半部调度到以后在更安全的时间内执行的那部分例程,这种机制有助于处理程序中比较耗时的任务。
两部分处理程序最大的不同就在于在执行bh时所有的中断都是打开的。典型的情况是,上半部处理程序将设备数据存放进一个设备指定的缓冲区,再标记它的下半部,然后退出;这样处理得就非常快。由bh将新到的数据再分派给各个进程,必要时再唤醒它们。这种设置允许上半部处理程序在下半部还在运行时就能为新的中断提供服务。但是,如果在上半部处理程序结束前有新的数据到了,由于中断控制器禁止了中断信号,这些数据仍会丢失。
所有实际的中断处理程序都作了这样的划分。如,当网络接口卡报告新的数据包到达了,处理程序只是取得数据并将它推进协议层中;对数据包的实际处理是在下半部中完成的。
实际上,任务队列就是从下半部的一个较老的实现演变而来的。与动态的任务队列不同,下半部的个数有限,并由内核预定义了;下半部的静态特性并不是个问题,因为有些下半部可以通过运行任务队列演变为动态对象。在头文件 中,你可以看到下半部的一张列表;
1、下半部的设计
下半部由一个函数指针数组和一个位掩码组成,它们不超过32个。当内核准备处理异步事件时,它就调用do_bottom_half,如从系统调用返回和退出慢速处理程序时;而这两类事件都发生得很频繁。而使用掩码主要出于性能的考虑。
当代码需要调度运行下半部处理时,只要调用mark_bh,该函数设置了掩码变量的一个位以将相应的函数进入执行队列。下半部可以由中断处理程序或其它函数来调度。执行下半部时,它会自动去除标记。
标记下半部的函数是在头文件 中定义的:
void mark_bh(int nr);
nr是激活的bh的“数目”。这个数是在头文件 中定义的一个符号常数,它标记位掩码中要设置哪个位。每个下半部bh相应的处理函数由拥有它的那个驱动程序提供。例如,当调用mark_bh(KEYBOARD_BH)时,要调度执行的函数是kbd_bh,它是键盘驱动程序的一部分。
因为下半部是静态对象,模块化的驱动程序无法注册自己的下半部。但此时可以使用立即队列。
一些比较常见的下半部:
IMMEDIATE_BH
对设备驱动程序来说这是最重要的bh。被调度执行的函数处理任务队列tq_immediate。没有下半部的驱动程序可以通过使用立即队列来取得和tq_immediate同样的效果。将任务等记到队列中后,驱动程序必须标记bh以使得它的代码真正得到执行;
TQUEUE_BH
如果任务登记在tq_timer队列中,每次时钟都会激活这个bh。驱动程序也可以使用tq_timer来实现自己的下半部;但不必为定时器队列调用mark_bh。TQUEUE_BH总是在IMMEDIATE_BH后执行的。
NET_BH
网络驱动程序通过标记这个队列来将事件通知上面的网络层。bh本身是网络层的一部分,模块无法访问。
CONSOLE_BH
控制台是在下半部中进行终端tty切换的。这个操作要包含进程控制。例如,在X Window系统和字符模式间切换就是由X 服务器控制的。而且,如果键盘驱动程序请求控制台的切换,那么控制台切换不能在中断时进行。也不能在进程向控制台写的时候进行。使用bh就能满足这些要求,因为驱动程序可以任意禁止下半部;如果发生了前面情况,在写控制台时禁止console_bh即可。
TIMER_BH
这个bh由do_timer函数标记;do_timer函数管理着时钟滴答。这个bh要执行的函数正是驱动内核定时器的那个函数。不使用add_timer的驱动程序是无法使用这种功能的。
<!--[if !supportEmptyParas]--> <!--[endif]-->
其余的下半部是有特定的内核驱动程序使用的。bh一旦被激活,当在return_from_sys_call中调用do_bottom_half(kernel/softirq.c)时它就会得到执行。当进程退出系统调用或慢速中断处理程序退出时都会执行return_from_sys_call过程。快速中断处理程序退出时就不会执行下半部;
时钟滴答总要执行ret_from_sys_call的;如果快速中断处理程序标记了一个bh,实际的bh处理函数最多10ms后就会被执行。
下半部运行后,如果设置了need_resched变量,就会调用调度器;各种wake_up函数都会设置这个变量。因此,上半部可以将任何与被唤醒的进程有关的任务放到下半部去做,并通过设置need_resched调度这些任务。
2、编写下半部
下半部代码是在安全时间内运行的。但是,bh还是在“中断时间”内处理的。intr_count不为0,因为下半部是在进程上下文之外执行的。因此,针对“任务队列”的各种限制也适用于在下半部中执行的代码。
下半部通过暂时禁止中断或使用锁来与上半部中断处理程序共享数据结构,且避免竞争条件。
新编写的实现了下半部的驱动程序应该通过使用立即队列来将它的代码挂在IMMEDIATE_BH上。共有三个函数可用于管理自己私有的下半部:init_bh,enable_bh和disable_bh。
实际上,立即队列也是一种下半部。当标记了IMMEDIATE_BH后,处理下半部的函数实际上就是去处理立即队列。如果你的中断处理函数将它的bh处理函数排进tq_immediate队列并且标记了下半部,那么队列中的这个任务会正确地被执行。内核都可以将相同的任务多次排队而不破坏任务队列,每次运行上半部处理函数时都可以将下半部排队。
一些需要特殊配置的驱动程序需要多个下半部或不能简单地用tq_immediate来设置,就可以使用定制的任务队列。中断处理函数将任务排进自己的队列中,并准备运行这些任务时,就将一个简单的对队列进行处理的函数插入立即队列。
例子:装载时如果指定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却不会检查任务是否已被排进队列。
然后,下半部记录下在调度下半部前上半部被激活的次数(savecount)。如果上半部是一个“慢速”处理函数,那么这个数总为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); /* 唤醒所有读进程 */
}
使用下半部,两个中断间的时间间将减少,但处理中断的总的工作量不变,更快的上半部的优点是禁止中断的时间较短,但对真正的硬件中断来说,这个时间还是很有关系的。
下面是当装载short时指定bh=1你可能看到的输出结果:
morgana%echo 1122334455 > /dev/shortint; cat /dev/shortint
bh after 5
50588804.876653
50588804.876693
50588804.876720
50588804.876747
50588804.876774
<!--[if !supportEmptyParas]--> <!--[endif]-->
(三)共享中断
在PC机上不能将不同的设备挂到同一个中断信号线上。但Linux可以共享中断,Linux软件对共享的支持是为PCI设备做的,也可用于ISA卡。
1、安装共享的处理程序
和其它中断一样,要与它共享的中断也是通过request_irq函数来安装的,但它们有两处不同:
<!--[if !supportLists]-->l<!--[endif]-->申请共享中断时,必须在flags参数中指定SA_SHIRQ位
<!--[if !supportLists]-->l<!--[endif]-->dev_id参数必须是唯一的。任何指向模块的地址空间的指针都可以,当然dev_id一定不能设为NULL。
内核为每个中断维护了一张共享处理函数的列表,并且这些处理函数的dev_id各不相同。如果两个驱动程序都将dev_id注册为NULL,那么在卸载时就会混淆,且当中断到达时内核就会出现oops消息。
满足这些条件之后,如果中断信号线空闲或者满足下面两个条件,request_irq就会成功:
<!--[if !supportLists]-->l<!--[endif]-->前面注册的处理函数的flags参数指定了SA_SHIRQ位。
<!--[if !supportLists]-->l<!--[endif]-->两个处理函数同为快速/慢速处理函数,快速和慢速处理函数处于不同的环境,不能互相混淆。
当两个或两个以上的驱动程序共享同一根中断信号线,而硬件又通过这根信号线中断了处理器时,内核激活这个中断注册的所有处理函数,并将自己的dev_id传递给它们。因此,共享处理函数必须能够识别出它对应于哪个中断。
内核没有共享中断的探测函数。仅当使用的中断信号线空闲时,标准的探测机制才能奏效;DIY探测时驱动程序必须为所有可能的中断信号线申请共享处理函数,然后观察中断在何处报告。这和前面的DIY探测之间的差别在于,此时探测处理函数必须检查是否真的发生了中断。
释放处理函数同样是通过执行release_irq来实现的。这里dev_id参数用于从该中断的共享处理函数列表中正确地选出要释放的那个处理函数。
使用共享处理程序的驱动程序时不能使用enable_irq和disable_irq。因为使用后共享中断信号线的其它设备就无法正常工作了。
2、运行处理函数
当内核接收到中断时,所有注册过的处理函数都会被激活。共享中断处理程序必须能将需要处理的中断和其它设备产生的中断区分开来。
例:装载short时指定shared=1将安装下面的处理程序而不是缺省的处理程序:
void short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int value;
struct timeval tv;
/* 如果不是short,立即返回 */
value = inb(short_base);
if (!(value & 0x80)) return;
/* 清除中断位 */
outb(value & 0x7F, short_base);
/* 其余不变 */
do_gettimeofday(&tv);
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); /* 唤醒所有读进程 */
}
并口没有 “待处理的中断”位可供检查,为此处理函数使用了ACK位。如果该位为高,报告的中断就是送给short的,然后处理函数通过将并口的数据端口的高位清零来清除中断位。如果与short共享同一中断的设备产生了一个中断,short会知道它的信号线并未激活。
3、/proc接口
系统中安装的共享中断处理程序不会影响/proc/stat文件,但是,会影响/proc/interrupts文件。
为同一个中断号安装的处理程序会出现在/proc/interrupts文件的同一行上。例如下面的快照是将short和帧捕捉卡装载为共享中断处理程序之后:
0: 1153617 timer
1: 13637 keyboard
2: 0 cascade
3: 14697 + serial
5: 190762 NE2000
7: 2094 + short, + cx100
13: 0 math error
14: 47995 + ide0
15: 12207 + ide1
这里共享中断信号是IRQ7号中断;激活的处理程序列在同一行,用逗号隔开。
4、缓冲与中断驱动的I/O
有时驱动程序的写函数必须实现缓冲。数据缓冲可以将数据的发送和接收与write及read系统调用分离开来,提高系统的整体性能。
“中断驱动的I/O”使写设备的进程填充一个输入缓冲区并在中断时间内由读设备的进程将其取空;或由读进程读空数据然后在中断时间内由写进程填充输入缓冲区。
这种机制要正确运行,就要求硬件必须按下面的语义产生中断:
<!--[if !supportLists]-->l<!--[endif]-->对输入而言,当新数据到达,系统处理器准备读取它时,设备就中断处理器。实际执行的动作取决于设备是否使用了I/O端口,内存映射或者DMA。
<!--[if !supportLists]-->l<!--[endif]-->对输出而言,当设备准备好接收新数据或对成功的数据传输进行确认时都会发出中断。内存映射和能进行DMA的设备通常是通过产生中断来通知系统它们的对缓冲区的处理已经结束。
<!--[if !supportLists]-->5、<!--[endif]-->竞争条件与处理
当操作不是原子地执行时或可能有代码会被同时执行时,会发生资源竞争,会导致当变量或其它数据项在中断时间内被修改时,由于竞争条件的存在,驱动程序的操作就有可能造成它们的不一致。但在执行时仍会假定数据会保持一致性。典型的竞争条件会在三种情况下发生:在函数内隐式地调用schedule、阻塞操作、由中断代码或系统调用访问共享数据。
最好处理方法是不允许并发访问。一般用于避免竞争条件的技术是在驱动程序的方法中实现的,但中断处理函数并不需要特别的处理,因为相对设备驱动,它的操作是原子性的。
最常用的防止数据被并发地访问的方法有:
<!--[if !supportLists]-->l<!--[endif]-->使用循环缓冲区和避免使用共享变量。
<!--[if !supportLists]-->l<!--[endif]-->在访问共享变量的方法里暂时禁止中断。
<!--[if !supportLists]-->l<!--[endif]-->使用锁变量,它是原子地增加和减少的。
可能在中断时间内被修改了的变量时可以声明为volatile的,来阻止编译器对该值的访问进行优化(例如,它阻止编译器在整个函数的运行期内将这个值放进一个寄存器中)。但是,使用volatile变量后,编译器产生的代码会很糟糕;也可以使用cli和sti,Linux实现这些函数时使用了gcc的制导来保证在中断标志位被修改之前处理器处于安全状态。
1)使用循环缓冲区
循环缓冲区使一个进程将数据放进缓冲区中,另一个则将它取出来。有多种情况,如读进程等待读取在中断时间里生产的数据;或,下半部读取上半部产生的数据。
head和tail指针用于对循环缓冲区进行寻址。head是数据的写入位置,由数据的写进程更新。数据从tail处读出,它是由读进程更新。如果数据是在中断时间内写的,那么必须将head定义成volatile的或者在进入竞争条件前将中断禁止。
如果缓冲区满了,有多种方式可以处理,简单地丢弃数据(如果并不检查溢出,如果head超过了tail,那么整个缓冲区中的数据都丢失了)、丢弃最后那个数据项、覆盖缓冲区的tail、、阻塞写进程、分配一个临时的附加的缓冲区作为主力缓冲区的候补等。选择解决方案取决于数据的重要性。
虽然循环缓冲区看来解决了并发访问的问题,但当read函数进入睡眠时可能出现竞争条件。如:
while (short_head==short_tail) {
interruptible_sleep_on(&short_queue);
/* ... */
}
新数据有可能在while条件被测试是否为真后和进程进入睡眠前到达。中断中携带的信息就无法被进程及时读取;即使此时head != tail,进程也将进入睡眠,直到下一项数据到达时它才会被唤醒。
2)禁止中断
通常调用cli禁止处理器的中断报告以获得对共享数据独占访问。当数据在中断时间内要被修改并且是被生存于正常的计算流中的函数修改时,那么随后的函数在访问这些数据前就必须先禁止中断。
这种情况下,竞争条件会发生在读共享数据项的指令和使用刚获得与数据有关的信息的指令之间。例如,如果链接表在中断时间内被修改过了,那么下面的循环在读这个表时就可能会失败。
for (ptr=listHead; ptr; ptr=ptr->next)
/* do somthing */;
在ptr已经被读取后但在使用它之前,一个中断可能会改变了ptr的值。
以下代码在整个关键循环期间将中断禁止:
unsigned long flags;
save_flags(flags);
cli();
/* 临界区代码 */
restore_flags(flags);
在驱动程序的方法中,可以认为当进程进入系统调用时中断会被打开,就可以用简单的cli/sti对来替代save_flags/restore_flags。但是有时候无法确定中断标志位(IF) 当前的值,就不得不使用更安全的save_flags/restore_flags解决方法。
3)使用锁变量
当两个无关的实体(比如象中断处理程序和read系统调用,或者是SMP对称多处理器计算机中的两个处理器)需要并发地对共享的数据项进行访问时,它们必须先申请锁。如果得不到锁,它就必须等待。
Linux内核开放了两套函数来对锁进行处理:位操作和对“原子性”数据类型的访问。
3-1)位操作
可能我们要在进程可能正在访问时,使有单个位的锁变量或者要在中断时间内更新设备状态位,内核为此提供了一套原子地修改和测试位的函数用于这一操作。
原子性的位操作通过单条机器指令来完成,运行的很快,这些函数与体系结构相关,在头文件中声明。即使在SMP机器上它们也能保证是原子的,
但是这些函数的数据类型也是体系结构相关的。nr参数和返回值在Alpha上是unsigned long类型,而在其它体系结构上是int类型。
set_bit(nr, void *addr);
这个函数用于设置addr指向的数据项的第nr个位。该函数作用在一个unsigned long上,即使addr指向void。返回的是该位原先的取值,0或非零。
clear_bit(nr, void *addr);
这个函数用于清除addr指向的unsigned long数据中的指定位。它的语义和set_bit类似。
change_bit(nr, void *addr);
这个函数用于切换指定位,其它方面和前面的set_bit和clear_bit函数类似。
test_bit(nr, void *addr);
这个函数不必是原子的伪操作;它只是简单地返回该位当前的值。
要访问共享数据项的代码段可以使用set_bit或clear_bit来试着原子地获取锁。通常是象下面的代码段这样实现的;假定锁位于地址addr的第nr位上。并且假定当锁空闲时该位为0,锁忙时该位非零。
/* 试着设置锁 */
while (set_bit(nr,addr)!=0)
wait_for_a_while();
/* 做你的工作 */
/* 释放锁,并检查... */
if (clear_bit(nr,addr)==0)
something_wnt_wrong(); /* 已经被释放了:出错 */
这种访问共享数据的方式的毛病是竞争双方都必须要等待。如果其中一方是中断处理程序,那么这一点就较难保证了。
3-2)原子性的整数操作
。中定义了一种新的数据类型atomic_t,只能通过原子操作来访问它。这些函数比位操作功能更强大。
atomic_t目前在所有支持的体系结构上都被定义为int。下面的操作能保证SMP机器上的所有处理器是原子地对它进行访问。这些操作都非常快,因为它们都尽可能编译成单条的机器指令。
void atomic_add(atomic_t i, atomic_t *v);
将v指向的原子变量加上i。返回值是void类型。网络部分的代码使用这个函数来更新套接字在内存使用上的统计信息。
void atomic_sub(atomic_t i, atomic_t *v);
从*v里减去i。
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
对原子变量加减1。
int atomic_dec_and_test(atomic_t *v);
该函数用于跟踪引用计数。仅当变量*v在减1后取值为0时返回值为0。
如果将原子数据项传递给了一个要求参数类型为整型的函数,编译时就会得到警告。可以读取原子数据项的当前值并将它强制转换成其它数据类型。
4)无竞争地进入睡眠
有种特别的竞争条件发生在检查进入睡眠的条件和对sleep_on的实际调用之间。如下面的测试代码:
while (short_head==short_tail){
interruptible_sleep_on(&short_queue);
/* ... */
}
如果要安全地进行比较和进入睡眠,你必须先禁止中断报告,然后测试条件并进入睡眠。比较中被测试的变量不能被修改。内核允许进程在发出cli指令后就进入睡眠。而在将进程插入它的等待队列之后,在调用shcedule之前,内核只要简单地重新打开中断报告。
这里例子使用了while循环来处理信号。如果有阻塞的信号向进程发出报告,interruptible_sleep_on就返回,再次进行while语句中的测试。
下面是一种可能的实现:
while (short_head==short_tail){
cli();
if (short_head==short_tail)
interuptible_sleep_on(&short_queue);
sti();
/* ... 信号解码 .... */
}
如果中断是在cli后发生的,那么这个中断在当前进程进入睡眠前都会处于待处理状态。而当中断最终报告给处理器时,进程已经进入了睡眠,可以被安全地唤醒。
在这个例子中,可以使用cli/sti是因为代码存在于read方法内的;否则我们必须使用更为安全的save_flags,和restore_flags函数。
如果在进入睡眠之前你不想禁止中断,有一种方法,其基本想法是,进程可以把自己排进等待队列,声明自己的状态为睡眠状态,然后执行它的测试代码。
典型的实现如下:
struct wait_queue wait = {current, NULL};
add_wait(&short_queue, &wait);
current->state=TASK_INTERRUPTIBLE;
while (short_head==short_tail){
schedule();
/* ... 信号解码 ... */
}
remove_wait_queue(&short_queue, &wait);
这段代码看起来有点象将sleep_on的内部实现展开了。显式地声明了wait变量,因为需要用它来使进程进入睡眠;
其中current->state字段是给调度器用的提示。调度器被激活后,它将通过观察所有进程的state字段来决定接着作些什么。所有进程都可以任意修改自己的state字段,但在调度器运行之前这种改变还不会生效。
#include
TASK_RUNNING
TASK_INTERRUPTIBLE
TASK_UNINTERRUPTIBLE
这些符号名是current->state常用的取值。
void add_wait_queue(struct wait_queue ** p, struct wait_queue *wait)
void remove_wait_queue(struct wait_queue ** p, struct wait_queue *wait)
void __add_wait_queue(struct wait_queue ** p, struct wait_queue *wait)
void __ remove_wait_queue(struct wait_queue ** p, struct wait_queue *wait)
这些函数用于从等待队列中插入和删除进程。wait参数必须指向进程堆栈所在的页(临时变量)。以下划线开头的函数运行的更快些,但它们在禁止中断后才能被调用。
当中断到达时处理程序将调用wake_up_interruptible(&short_queue);对Linux而言,这意味着“将state置为TASK_RUNNING”。因此,如果在while条件和schedule调用间有中断报告的话,该任务的state字段将会又被标记为TASK_RUNNING的,因此不会丢失数据。
而如果进程仍是“可中断的”(TASK_INTERRUPTIBLE),schedule将保持它的睡眠状态。
wake_up系统调用并不会将进程从等待队列中删去。是由sleep_on来对等待队列进行进程的添加和删除的。因此程序代码必须显式地调用add_wait_queue和remove_wait_queue。  

相关文章