对中断的一点思考
杨小华(normalnotebook@126.com)
对于X86的单处理器机器,一般采用可编程中断控制器8259A做为中断控制电路。传统的PIC(Programmable Interrupt Controller)是由两片8259A风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达8个不同的IRQ输入线。因为从PIC的INT输出线连接到主PIC的IRQ2引脚,所以可用IRQ线的个数限制为15,如图1所示。
图 1 8259A级联原理图(此图摘自《Linux内核完全注释》)
“中断屏蔽寄存器”(Interrupt Mask Register,简称IMR)用于屏蔽8259A的中断信号输入,每一位对应一个输入。当IMR中的bit[i](0≤i≤7)位被置1时,相对应的中断信号输入线IRi上的中断信号将被8259A所屏蔽,也即IRi被禁止。
当外设产生中断信号时(由低到高的跳变信号,80x86系统中的8259A是边缘触发的,Edge Triggered),中断信号被输入到“中断请求寄存器”(Interrupt Request Register,简称IRR),并同时看看IMR中的相应位是否已被设置。如果没有被设置,则IRR中的相应位被设置为1,表示外设产生一个中断请求,等待CPU服务。
然后,8259A的优先级仲裁部分从IRR中选出一个优先级最高中断请求。优先级仲裁之后,8259A就通过其INT引脚向CPU发出中断信号,以通知CPU有外设请求中断服务。CPU在其当前指令执行完后就通过他的INTA引脚给8259A发出中断应答信号,以告诉8259A,CPU已经检测到有中断信号产生。
8259A在收到CPU的INTA信号后,将优先级最高的那个中断请求在ISR寄存器(In-Service Register,简称ISR)中对应的bit置1,表示该中断请求已得到CPU的服务,同时IRR寄存器中的相应位被清零重置。
然后,CPU再向8259A发出一个INTA脉冲信号,8259A在收到CPU的第二个INTA信号后,将中断请求对应的中断向量放到数据总线上,以供CPU读取。CPU读到中断向量后,就可以装入执行相应的中断处理程序。
如果8259A工作在AEOI(Auto End Of Interrupt,简称AEOI)模式下,则当他收到CPU的第二个INTA信号时,它就自动重置ISR寄存器中的相应位。否则,ISR寄存器中的相应位就一直保持为1,直到8259A显示地收到来自于CPU的EOI命令。
打住,各位看官读到这里,能回答如下问题吗?
1. 在执行中断处理程序时,中断一直是关闭着的吗?
2. 在执行中断处理程序时,本条中断线上的中断是否会被屏蔽?
3. 如果该条中断线被屏蔽了,那么是否一直要到该中断返回(即执行iter指令)时才开启?
4. 禁止中断后,异常还会执行吗?timer还会被执行吗?
如果不能回答这些问题,请继续欣赏。如果你能回答,请关闭本文档,努力工作吧,或拿起一本英语书看看,这年头不好混,多看看英语吧 !:)
当中断发生,CPU在穿越中断门时会关闭本处理器上所有的中断。此时中断执行路线是:common_interrupt->do_IRQ()->__do_IRQ()->handle_IRQ_event()->具体的中断处理程序。大家都知道中断类型包括三种:
标志
含义
SA_INTERRUPT
当该位被设置时,表明这是一个快速的中断处理程序。在本地处理器上,快速中断处理程序在禁止所有中断的情况下运行。除了时钟中断外,绝大多数中断都不使用该标志。
SA_SHIRQ
该位表示中断可以在设备之间共享。
SA_SAMPLE_RANDOM
该位指出产生的中断能对/dev/random设备和/dev/urandom设备使用的熵池有贡献。
表 1中断类型标志位及其含义表
如果相应的中断处理程序在注册时,即调用request_irq()函数进行中断处理程序注册时,会传递这三种中类型中的一个或数个。如果没有指定SA_INTERRUPT该类型。则在handle_IRQ_event()函数中会第一次开中断;如果指定了该参数,则会在关闭中断的情况下执行中断处理程序。
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
……//调用中断处理程序
local_irq_disable();
return retval;
}
如果此时开中断了,本条IRQ线上的中断也打开了吗?我要告诉你的是,在执行到这里的时候,本条线上的中断已经被屏蔽了,但也不是问题3中所说的一直到iret时才打开。
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
irq_desc_t *desc = irq_desc + irq;
……
desc->handler->ack(irq);
……
action_ret = handle_IRQ_event(irq, regs, desc->action);
……
desc->handler->end(irq);
……
}
desc->handler->ack(irq);最终会调用mask_and_ack_8259A()(为什么会调用该函数,请查看源代码或相关书籍)。mask_and_ack_8259A的功能是:因为中断处理器在将中断请求“上报”到CPU后,期待CPU给它一个确认(ACK),也就是给8259A芯片一个应答信号,表示“我已经在处理”,应答时应该遵循这样的顺序:首先,屏蔽相应的IRQ;然后,发送EOI(End of Interrupt)命令。此外,如果IRQ来自于从8259A,还必须先向从 8259A发送EOI命令,再向主8259A发送EOI命令。如果IRQ来自于主 8259A,则仅仅向主8259A发送EOI命令就可以了。当一个中断服务结束后,CPU可利用中断结束命令EOI通知8259A,以便复位ISR中的相应位。因此当调用handle_IRQ_event()时,即使开中断,该条中断线的中断也是关闭的。一直到调用desc->handler->end(irq);时才清除中断屏蔽。
细心的读者可能还有一个问题,为什么在handle_IRQ_event()返回时,还要关闭本地所有的中断(即代码中的local_irq_disable();)。因为在中断返回时,将会进行退栈清理性的工作,如果此时响应中断,鬼知道后果是什么?嘿嘿, Linus Torvalds肯定知道结果。因为他比鬼还厉害 :)
虽然,中断关闭了,但异常并没有关闭。关中断只是关掉了外部中断,cli只是设置EFLAGS寄存器的IF位,如果该位被清除,则表示CPU会禁止外部中断传递信号给INTR引脚,但对于CPU内部异常和不可屏蔽中断(NMI)并不起作用。由于timer是外部中断,所以在禁中断后,timer中断将不在起作用,一直到开中断。
1. 如果在desc->handler->ack(irq);和desc->handler->end(irq);之间,该条中断线上再次发生中断,该中断是否会被丢失?
2. 在中断处理快结束时,会执行软中断。可在执行do_softirq()时,又会执行
if (in_interrupt())
return;
难道软中断不在中断中吗?岂不是执行到这里都返回了?
如果你又知道这些问题的答案,那我只能自认倒霉了,难道你就是传说中的hacker!!!:).
对于第一个问题,我也不能给出明确的答案。我只是把所收集的资料写出来。至于对不对,有大家自己去判断。
中断不会丢失,会被挂起(pending),保存在IPR(Interrupt Pending Register)中,等到中断允许位打开后再执行相应的中断处理程序。(但我查了8259A的寄存器,好像没有这个寄存器。)
在《源代码情景分析》一书中p216第10行提到:“这样,就把本来可能发生在同一通道(甚至可能来自同一中断源)的中断嵌套化解为一个循环”。本人认为这种嵌套仅仅针对的是SMP的情况。因为对单CPU来说,ack操作已经将本条中断线给屏蔽了,根本不可能再响应了。
如果哪位这里有比较好的权威性的答案,请记得发封邮件给我,先谢过了。
在do_IRQ()中,将会调用irq_enter()()和irq_exit()两个函数。
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
……
irq_enter();
……
__do_IRQ(irq, regs);
……
irq_exit();
……
}
在执行irq_enter()时,将会调用(preempt_count() += HARDIRQ_OFFSET);在执行irq_exit()函数时,又会:
void irq_exit(void)
{
preempt_count()-= IRQ_EXIT_OFFSET;// # define IRQ_EXIT_OFFSET HARDIRQ_OFFSET
if (!in_interrupt() && local_softirq_pending())
do_softirq();
preempt_enable_no_resched();
}
而in_interrupt()函数只是对preempt_count的某些位进行测试。所以在执行do_softirq()时,preempt_count经过一次加减运算后,将值还原了。所以只要不是中断嵌套的话,in_interrupt()会为假。所以执行到这里时,根本不会返回,除非中断嵌套。
我们也该休息一下了,广告之后马上回来,进入下一个环节--有关调度的问题。
我曾经在一个培训资料上看到如下的结论:
实时应用中,中断的发生不但要求迅速的中断服务,还要求迅速的调度有关的进程进入运行,在用户空间中对事件处理。 可是,如果这样的中断发生在内核时,本次中断返回是不会引起调度的,而要到最初使CPU从用户空间进入内核的那次系统调用或中断(或异常)返回时才会发生调度。倘若内核中的这段代码恰好需要较长时间来完成的话,或者连续又发生几次中断的话,就可能将调度过分的推迟。
当中断发生在内核时,本次中断返回时是有可能引起调度的,原因是,只要need_resched被置位并且preempt_count=0,如果这这两个条件都满足,那么就会发生schedule()操作,
我认为写这段话的作者,当时可能研究的是2.4的内核,因为那时的内核是不可抢占的。当返回到用户态时,只需要对need_resched进行检查。
另外一个问题:
当idle运行时,发生外部中断A,中断处理程序A将一个进程P1唤醒,并设置了调度标志need_resched,在中断处理程序A还没有结束前,又有一高优先级的外部中断B发生,响应B,当中断处理程序B结束后,内核进行调度,选择中断A所唤醒的进程P1运行,此后,产生许多可运行的进程,致使idle不能很快再运行。这不就存在着很大的问题,甚者会导致该条线上的中断永远都不能响应。
的确不错,当调用wake_up_process()时,如果被唤醒进程的优先级比当前进程的优先级高,那么将设置need_resched位。但这个问题忽略了一个很重要的问题,那就是中断B返回时,当返回内核态时(我想不可能返回到用户态吧),将会对need_resched和preempt_count进行检查,如果这两者都满足,才会进行调度。从上面分析可知,当发生中断嵌套时,preempt_count此时等于一个很大的值,虽然B在执行时,preempt_count经过一次加和减的操作,但A还是将该值设置成禁止抢占的,所以中断B返回时,根本不可能发生调度,而是会执行中断上下文A。
说明:有些结论来自论坛上网友的回答,向他们表示感谢。
友情鸣谢:
[1] www.linuxforum.net
[2] Daniel P.Bovel & Marco Cesati 《深入理解Linux内核》 中国电力出版社,2004
[3] 毛德操,胡希明 《Linux内核源代码情景分析》 浙江大学出版社,2001
[4] Robert Love 《Linux内核设计与实现》 机械工业出版社 ,2004