上一篇博文我们把中断原理和中断处理所需要的数据结构清理了一遍,现在,我们就该看看中断处理的具体动作是怎样的了。
当CPU接收一个中断时,就开始执行相应的中断处理程序代码,前面介绍过了,该代码的地址存放在IDT的相应门中。于是,与其他上下文切换一样,Linux需要保留当前寄存器的内容以便保存和恢复当前指令。
保存寄存器是中断处理程序做的第一件事情。每个IRQ的中断处理程序地址存放于interrupt数组中,即IRQn中断处理程序的地址开始存在于interrupt[n],然后复制到IDT相应表项的中断门中。
interrupt数组是在系统自举阶段,通过文件arch/i386/kernel/entry.S中的几条汇编语言指令建立的,数组包括NR_IRQS个元素,这里NR_IRQS宏产生的数为224或16,当内核支持最新的I/O APIC芯片时(80x86体系结构限制了只能使用256个向量,其中32个留给CPU,因此可用向量空间有224个向量),NR_IRQS宏产生的数为224;而当内核支持旧的8259A可编程控制器芯片时,NR_IRQS宏产生数16。数组中索引为n的元素中存放下面两条汇编语言指令的地址:
pushl $n-256
jmp common_interrupt
结果是把中断号减256的结果保存在栈中。内核用负数表示所有的中断,因为正数用来表示系统调用。当引用这个数时,可以对所有的中断处理程序都执行相同的代码。这段通用代码开始于标签common_interrupt处,包括下面的汇编语言宏和指令:
common_interrupt:
SAVE_ALL
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
SAVE_ALL宏依次展开成下列片段:
cld
push %es
push %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
movl $ _ _USER_DS,%edx
movl %edx,%ds
movl %edx,%es
SAVE_ALL可以在栈中保存中断处理程序可能会使用的所有CPU寄存器,但eflags、cs、eip、ss及esp除外,因为这几个寄存器已经由控制单元自动保存了。然后,这个宏把用户数据段的选择符装到ds和es寄存器。
保存寄存器的值以后,栈顶的地址被存放到eax寄存器中,然后中断处理程序调用do_IRQ()函数。到执行到do_IRQ()的ret指令时(即函数结束时),控制转到ret_from_intr()(从中断和异常返回)。
do_IRQ( ) 函数
调用do_IRQ()函数执行与一个中断相关的所有中断服务例程。该函数声明为:
_ _attribute_ _((regparm(3))) unsigned int do_IRQ(struct pt_regs *regs)
关键字regparm表示函数到eax寄存器中去找到参数regs的值。如上所见,eax指向被SAVE_ALL最后压入栈的那个寄存器在栈中的位置。
do_IRQ( ) 函数执行下面的操作:
1. 执行irq_enter()宏,它使表示中断处理程序嵌套数量的计数器递增。计数器是指当前进程thread_info结构的preempt_count字段。
2. 如果thread_union结构的大小为4KB,函数切换到硬中断请求栈,并执行下面这些特殊的步骤:
a) 执行current_thread_info()函数以获取与内核栈(地址在esp中)相连的thread_info描述符的地址。
b) 把上一步获取的thread_info描述符的地址与存放在hardirq_ctx[smp_processor_id()]中的地址(与本地CPU相关的thread_info描述符的地址)相比较。如果两个地址相等,说明内核已经在使用硬中断请求栈,因此跳转到第3步,这种情况发生在内核处理另外一个中断时又产生了中断请求的时候。
c) 这一步必须切换内核栈。保存当前进程描述符指针,该指针在本地CPU的irq_ctx联合体中的thread_info描述符的task字段中。完成这一步操作就能在内核使用硬中断请求栈时使当前宏按预先的期望工作。
d) 把esp栈指针寄存器的当前值存入本地CPU的irq_ctx联合体的thread_info描述符的previous_esp字段中(仅当为内核oop准备函数调用跟踪时使用该字段)。
e) 把本地CPU硬中断请求栈的栈顶(其值等于hardirq_ctx[smp_processor_id()]加上4096)装人esp寄存器;以前esp的值存入ebx寄存器。
3. 调用__do_IRQ()函数,把指针regs和regs->orig_eax字段中的中断号传递给该函数。
4. 如果在上面的第2e步已经成功地切换到硬中断请求栈,函数把ebx寄存器中的原始栈指针拷贝到esp寄存器,从而回到以前在用的异常栈或软中断请求栈。
5. 执行宏irq_exit(),该宏递减中断计数器并检查是否有可延迟函数正等待执行。
6. 结束:控制转向ret_from_intr()函数。
__do_IRQ( ) 函数
__do_IRQ()函数接受IRQ号(通过eax寄存器)和指向pt_regs结构的指针(通过edx寄存器,用户态寄存器的值已经存在其中)作为它的参数。
spin_lock(&(irq_desc[irq].lock));
irq_desc[irq].handler->ack(irq);
irq_desc[irq].status &= ~(IRQ_REPLAY | IRQ_WAITING);
irq_desc[irq].status |= IRQ_PENDING;
if (!(irq_desc[irq].status & (IRQ_DISABLED | IRQ_INPROGRESS))
&& irq_desc[irq].action) {
irq_desc[irq].status |= IRQ_INPROGRESS;
do {
irq_desc[irq].status &= ~IRQ_PENDING;
spin_unlock(&(irq_desc[irq].lock));
handle_IRQ_event(irq, regs, irq_desc[irq].action);
spin_lock(&(irq_desc[irq].lock));
} while (irq_desc[irq].status & IRQ_PENDING);
irq_desc[irq].status &= ~IRQ_INPROGRESS;
}
irq_desc[irq].handler->end(irq);
spin_unlock(&(irq_desc[irq].lock));
在访问主IRQ描述符之前,内核获得相应的自旋锁,这些内容我们后面同步和互斥的博文中再来讨论,这里暂不细讲。
获得自旋锁后,函数就调用主IRQ描述符的ack方法,把IRQ号传给他。如果使用旧的8259A PIC,查看上一篇博文,我们就知道了是调用相应的mask_and_ack_8259A()函数来应答PIC上的中断,注意禁用这条IRQ线。屏蔽IRQ线是为了确保在这个中断处理程序结束前,CPU不进一步接受这种中断的出现。请记住咯!do_IRQ()函数是以禁止本地中断运行的;事实上,CPU控制单元自动清eflags寄存器的IF标志,因为中断处理程序是通过IDT中断门调用的。不过,我们立即会看到,内核在执行这个中断的中断服务例程之前可能会重新激活本地中断。
然后,__do_IRQ()函数初始化主IRQ描述符的几个标志。设置IRQ_PENDING标志,是因为中断已被应答(在一定程度上),但是还没有真正地处理;也清除IRQ_WAITING和IRQ_REPLAY标志(但我们现在不必关注它们)。
现在,__do_IRQ()检查是否必须真正地处理中断。在三种情况下什么也不做,他们是:IRQ_DISABLED被设置、IRQ_INPROGRESS被设置、irq_desc[irq].action为空。
呵呵,就让我们假定三种情况没有一种成立,因为这是99%的情况,因此中断必须被处理。__do_IRQ()设置IRQ_INPROGRESS标志并开始一个循环。在每次循环中,函数清IRQ_PENDING标志,释放中断自旋锁,并调用handle_IRQ_event()执行中断服务例程。当handle_IRQ_event()终止时,__do_IRQ()再次获得自旋锁,并检查IRQ_PENDING标志的值。如果该标志清0,那么,中断的进一步出现不传递给另一个CPU,因此,循环结束。相反,如果IRQ_PENDING被设置,当这个CPU正在执行handle_IRQ_event()时,另一个CPU已经在为这种中断执行do_IRQ()函数。因此,do_IRQ()执行循环的另一次反复,为新出现的中断提供服务。
我们的__do_IRQ()函数现在准备终止,或者是因为已经执行了中断服务例程,或者是因为无事可做。函数调用主IRQ描述符的end方法。当使用旧的8259A PIC时,相应的end_8259A_irq()函数重新激活IRQ线(除非出现伪中断)。当使用I/O APIC时,end方法应答中断(如果ack方法还没有去做)。
最后,__do_IRQ()释放自旋锁:艰难的工作已经完成!
中断服务例程
如前所述,一个中断服务例程(ISR)实现一种特定设备的操作。当中断处理程序必须执行ISR时,它就调用handle_IRQ_event()函数。这个函数本质上执行如下步骤:
1.如果SA_INTERRUPT标志清0,就用sti汇编语言指令激活本地中断。
2.通过下列代码执行每个中断的中断服务例程:
retval = 0;
do {
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
在循环的开始,action指向irqaction数据结构链表的开始,而irqaction表示接受中断后要采取的操作(这里实在看不明白请参考前一篇博文最后的那个图)。
3.用cli汇编语言指令禁止本地中断。
4.通过返回局部变量retval的值而终止,也就是说,如果没有与中断对应的中断服务例程,返回0;否则返回1(见下面)。
熟悉C语言的哥们肯定都能看出来,这里最重要的是对每个设备执行action->handler()函数,表示对共享此IRQ的所有设备都执行一次。所有的中断服务例程都作用于相同的参数(它们分别又一次通过eax、edx和ecx寄存器来传递):
irq:IRQ号
dev_id:设备标识号
regs:指向内核(异常)栈的pt_regs结构的指针,栈中含有中断发生后随即保存的寄存器。pt_regs结构包含15个字段:
- 开始的9个字段是被SAVE_ALL压入栈中的寄存器的值。
- 第10个字段为IRQ号编码,通过orig_eax字段被引用。
- 其余的字段对应由控制单元自动压入栈中的寄存器的值。
第一个参数允许一个单独的ISR处理几条IRQ线,第二个参数允许一个单独的ISR照顾几个同类型的设备,第三个参数允许ISR访问被中断的内核控制路径的执行上下文。看晕了吧,不是前边再三提醒大家IRQ跟I/O设备是一对多的关系,怎么这里来了一个ISR处理多个IRQ呢,实际上,大多数ISR不使用这些参数,哈哈。
每个中断服务例程在成功处理完中断后都返回1,也就是说,当中断服务例程所处理的硬件设备发出信号时返回1;而共享该IRQ的其他设备对此没有意义,则返回0。
当do_IRQ()函数调用一个ISR时,主IRQ描述符的SA_INTERRUPT标志决定是开中断还是关中断。通过中断调用的ISR可以由一种状态转换成相反的状态。在单处理器系统上,这是通过cli(关中断)和sti(开中断汇编语言指令实现的)。
IRQ线的动态分配
在前面博文中我们看到,有几个向量留给特定的设备,其余的向量都被动态地处理。针对IRQ不够用的问题,除了多个设备共享IRQ方式,还有一种方式允许同一条IRQ线让多个硬件设备使用,即这些设备不允许IRQ共享,必须独占一个IRQ,也可以对其动态分配。技巧就在于使这些硬件设备的活动串行化,以便一次只能有一个设备拥有这个IRQ线。
具体怎么实现呢?在激活一个准备利用IRQ线的设备之前,其相应的驱动程序调用request_irq(),建立一个新的irqaction描述符,并用参数值初始化它。然后调用setup_irq()函数把这个描述符插入到合适的IRQ链表。如果setup_irq()返回一个出错码,设备驱动程序中止操作,这意味着IRQ线已由另一个设备所使用,而这个设备不允许中断共享。当设备操作结束时,驱动程序调用free_irq()函数从IRQ链表中删除这个描述符,并释放相应的内存区。
让我们用一个简单的例子看一下这种方案是怎么工作的。假定一个程序想访问/dev/fd0设备文件对应于系统中的第一个软盘(软盘是不允许IRQ共享的“旧设备”)。程序要做到这点,可以通过直接访问/dev/fd0,也可以通过在系统上安装一个文件系统。通常将IRQ6分配给软盘控制器,给定这个号,软盘驱动程序发出下列请求:
request_irq(6, floppy_interrupt, SA_INTERRUPT|SA_SAMPLE_RANDOM, "floppy", NULL);
我们可以观察到,floppy_interrupt()中断服务例程必须以关中断(设置SA_INTERRUPT)的方式来执行,并且不共享这个IRQ(清SA_SHIRQ标志)。设置SA_SAMPLE_RANDOM标志意味着对软盘的访问是内核用于产生随机数的一个较好的随机事件源。当软盘的操作被终止时(要么终止对/dev/fd0的I/O操作,要么卸载这个文件系统),驱动程序就释放IRQ6:
free_irq(6, NULL);
为了把一个irqaction描述符插入到适当的链表中,内核调用setup_irq()函数,传递给这个函数的参数为irq_nr(即IRQ号)和new(即刚分配的irqaction描述符的地址)。这个函数将:
1. 检查另一个设备是否已经在用irq_nr这个IRQ,如果是,检查两个设备的irqaction描述符中的SA_SHIRQ标志是否都指定了IRQ线能被共享。如果不能使用这个IRQ线,则返回一个出错码。
2. 把*new(由new指向的新irqaction描述符)加到由irq_desc[irq_nr]->action指向的链表的末尾。
3. 如果没有其他设备共享同一个IRQ,清*new的flags字段的IRQ_DISABLED、IRQ_AUTODETECT、IRQ_WAITING和IRQ _INPROGRESS标志,并调用irq_desc[irq_nr]->handler PIC对象的startup方法以确保IRQ信号被激活。
举一个如何使用setup_irq()的例子,它是从系统初始化的代码中抽出的。内核通过执行time_init()函数中的下列指令,初始化间隔定时器设备的irq0描述符。
struct irqaction irq0 =
{timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
setup_irq(0, &irq0);
首先,类型irqaction的irq0变量被初始化:把handler字段设置成timer_interrupt()函数的地址,flags字段设置成SA_INTERRUPT,name字段设置成“timer”,最后一个字段设置成NULL以表示没有用dev_id值。接下来,内核调用setup_irq()把irq0插入到与IRQO相关的irqaction描述符链表中。