/*Interrupt, trap and exception handling in Windows nt把硬件中断映射到software interrupt request level上了,实现了很好的隔离和跨平台特性。对驱动程序员来说,除了在获取资源列表和调用IoConnectInterrupt之外,几乎接触不到硬 件中断。所有和中断硬件(主要是pic)打交道的代码都集中在hal的一个角落里,hal的大部分以及全部的kernel只和一个虚拟的中断控制器打交 道。在这个虚拟的中断控制器里,nt定义了32个软件中断级别,当硬件中断发生的时候,hal将硬件中断映射成这32个软中断之一,并更新虚拟中断控制器 的内部状态保持和硬件中断控制器同步。从kernel往上到执行体以及驱动程序,所有的代码也都是和这个虚拟出来的中断控制器打交道,最大量使用的操作是,ke、ex或者driver通过操纵irql来控制当前活动的优先级,使得低优先级的活动不至于干扰当前计算任务,同时保持对高优先级计算任务的及时 响应。于是,nt可以将不同的计算任务分配到不同的优先级上,从而为高效使用cpu资源提供了相当的灵活性。
nt做了大量的工作,保证这个虚拟中断控制器(pic)像硬件pic一样精确而可靠的工作,这个虚拟pic的支持数据结构都位于PCR,也就是 processor control region中,PCR是hal和kernel共享的一个非常重要的数据结构。(pcr这个结构很好,实际上它就是硬件抽象层抽象出来的真实cpu)
现在流行可编程设备,像8259之类的简单器件都可以从网上下载到free的ip了,要了解它的工作原理或者把它变成软件比从前容易多了。在当年,不要说实现一个虚拟的pic,就是能有这个想法的,大概也得算是软硬通吃的牛人了。
作为一个比较粗糙的总结,我认为,理解了下面的功能,基本上就能掌握nt的虚拟中断控制器的实现原理了。这个总结不是很好,在那些熟悉硬件的看来,只能说是贻笑大方了。
*) interrupt request register:记录尚未被cpu处理的中断,以便cpu在适当的时候查询。
*) interrupt mask:屏蔽不重要的中断,在nt中通过irql实现
*) interrupt request recognizing:nt在适当的时候会检查IRR,如果发现有比当前irql高的未处理中断,则会立刻启动该中断的处理程序,这个检查的过程我称为 recognizing。recognizing发生在很多场合,KeLowerIrql 是最经常的一个,HalRequestSoftwareInterrupt也是一个。
*) interrupt routing /& dispatching:相当于硬件的中断向量表,nt也有个软件中断派发表,SWInterruptTableHandlerTable,后面将要看 到,在向x86上,这个派发表和idt协同工作实现中断的派发。
*) interrupt acknowledgement:虚拟pic的状态也可以看成是硬件pic的状态的cache。如同memory cache一样,虚拟pic的状态和硬件pic也不总是一致。事实上,在提升irql的时候,nt采用lazy irql技术将对硬件imr的编程推迟到当被屏蔽的中断到来的时候,这个策略和memory cache的lazy
write如出一辙。另外一个可以和这个类比的策略是nt的虚拟内存的保留->提交两阶段分配。我记得在有个程序员的网站上看到,这种将代码推迟到必要的时候才执行的思想,是优化代码的常见方法。
这种将硬件中断虚拟化的设计思路很有大师气派,也确实达到了很好的可移植性,可惜的是nt在这个映射的过程中,牺牲了对硬件中断优先级的控制,后来成了nt在向实时系统转化的时候的最大障碍(另外一个障碍是umode线程只能在passive level上执行)。
为了对nt处理中断的过程有更深刻的了解,必须先认识这个游戏中的几个重要角色。
中断处理演员表:
macros:
ENTER_INTERRUPT/ENTER_TRAP/ENTER_SYSCALL:它们负责构建中断、陷阱、系统调用的stack frame。
DISPATCH_USER_APC/EXIT_ALL/INTERRUPT_EXIT:负责打扫战场。
functions:
KiBeginSystemInterrupt和KiEndSystemInterrupt:这哥俩儿一个提升irql,一个降低irql。
KiExceptionExit/Kei386EoiHelper:也是打扫战场的。
除了clock、profile等nt内置的中断处理程序,硬件中断的处理函数都是动态生成的,所以用!idt调试命令看不到相关符号信息。这种动态生成的代码实际上是一个thunk,可以参见前面关于thunk的那个文章,源代码:KeInitializeInterrupt。x86的hal中以及 kernel的x86部分,很多代码是Shie-Lin Tzong(shielint)写的,中文名叫宗世麟,台大资讯系82届的前辈,不知道现在在哪里发财,哈哈。
中断、陷阱和例外的处理流程非常类似,先看interrupt的处理过程:
*) ENTER_INTERRUPT
+) Build the frame and set registers needed by a interrupt.
*) HalBeginSystemInterrupt
+) This routine is used to dismiss the specified vector number.It is called before any interrupt service routine code is executed.
+) 检测虚假中断(spurious interrupt),如果正常返回,则中断屏蔽被打开,返回值为true,如果是虚假中断(包括delayed interrupt)则中断屏蔽保持关闭状态,同时返回值为false。
+) 这个函数同时负责提升irql,也是执行lazy irql的地方,如果当前irql比当前中断的irql高,则将重新设置pic,并把当前中断记录在pcr,这样会生成一个delayed interrupt,同时将当前中断作为spurious interrupt直接返回。
*) 实际的处理代码
*) INTERRUPT_EXIT
+) cli - 注意HalpEndxxxInterrupt都在中断关闭状态下执行。
+) HalEndSystemInterrupt
x 恢复irql
x 处理delayed interrupt
+) Kei386EoiHelper
x DISPATCH_USER_APC - 执行apc
x EXIT_ALL - 重新加载被中断的上下文
问答:
Q:什么是spurious interrupt?
A: 在设备发出中断请求到cpu响应该中断期间,如果设备撤销了该中断请求,pic的interrupt pending register和interrupt status register会发生不一致,中断响应代码检查interrupt status的时候会发现没有设备中断,这个就称为spurious interrupt。有点像顽皮的小孩乱按门铃,等你出去的时候却看不到敲门的人。
Q:什么是delayed interrupt?
A: 在lazy irql期间,如果没有中断到达,则nt就会摆谱儿不去更新硬件pic的状态。直到低优先级的中断溜进来,nt才忙不迭的堵后门去设置pic的imr,但 这个已经来了的中断怎么办呢?不能就这么丢了啊,nt于是把这个中断记录在pcr的irr里,这样就形成一个所谓delayed interrupt,也有称postponed interrupt。nt将在随后适当的时候用软件模拟的方式重新产生这个中断事件。需要注意的事,apc和dpc也可能产生delayed interrupt,这个事实应该比硬件中断产生的delayed interrupt更好理解,在中断处理函数里调用KeInsertQueueDpc,显然这个dpc不能立刻开始执行,因此只能作为delayed interrupt。由于delayed interrupt是通过软件方式模拟的,所以硬件中断就这样摇身一变成了软件中断,并且将和apc、dpc一样通过软件中断派发表派发(而不是 idt)。
Q:nt在什么时候执行delayed interrupt?
A:显然在当降低irql的时候检查是比较合适的,nt降低irql的途径主要是通过三个函数:
HalpEndSystemInterrupt
HalpEndSoftwareInterrupt
KfLowerIrql
在 其中会针对每个比当前irql高的delayed interrupt事件,通过SWInterruptHandlerTable派发,对apc和dpc两个软件中断来说,会直接派发到 HalpApcInterrupt和 HalpDispatchInterrupt;而对于其它也就是硬件中断产生的delayed interrupt,实际上会被派发到一个stub函数,这个函数里只包含一个int xx指令,其中xx=interrupt vector + PRIMARY_VECTOR_BASE(0x30)。这样我们顺便知道idt中从0x30到0x3f的作用了----用来仿真一个delayed hardware interrupt。这个话题延伸开去,就是idt的布局问题了,也是一个比较有意思的话题,不过牵涉到的硬件部分暂时不是很熟悉,暂且打住。(按:惊!)
Q:KfLowerIrql和HalEnd{System|Software}Interrupt有何区别?
A:从语义上看,这两 个函数完全是一回事,看它们的注释就知道,三者都是降低irql,同时检查是否有需要派发的软件中断(包括delayed interrupt)。但读代码的时候会注意到,KfLowerIrql只处理一个pending software interrupt,而后两者则会循环处理每个pending software interrupt。这个原因似乎是因为只要KfLowerIrql模拟产生一个软件中断事件,那么那个事件的中断处理函数最终肯定会调用 HalpEndxxxInterrupt,因此会在那里处理所有的中断,这样在KfLowerIrql里就没有必要循环了。但这个分析我自己很不满意,仔细查看HalpEndSystemInterrupt和HalpEndSoftwareInterrupt发现有些细小的区别,不目前不是很清楚是什么原因。
Q:为什么需要在HalpEndxxxInterrupt检测delayed interrupt的重入?
A:硬件中断的堆栈帧太大 了(查看ENTER_INTERRUPT可知,有将近100个字节),所以不得不采取一定的措施,不然kmode的 12k堆栈很快就报销了。至于apc和dpc中断,则可以复用当前中断的堆栈帧,也是一种优化。注:为什么硬件中断和软件中断有这个区别,目前还没有想明白。
Q:为什么delayed hardware interrupt必须派发到用int指令来仿真的stub上,而apc和dpc中断则可以直接派发到函数地址上?
A: 稍微动动脑筋就可以想到,如果不通过int指令(进而通过idt派发),那么派发到哪儿呢?也就是 SWInterruptHandlerTable表里填什么呢?因为硬件中断处理函数的地址随系统运行可能会变化(尤其在PnP环境下),所以,干脆用 int指令仿真得了。反正在IoConnectInterrupt的时候会将正确的中断处理函数地址填到idt里头去。这就是最开始我们说 SWInterruptHandlerTable和idt一起完成中断派发的意思了。
Q:simulated software interrupt和真实的interrupt有什么区别?
A: 如果我们把中断也看成一种函数调用的话,那么真实的中断响应函数的原型是和硬件平台相关的,例如x86下面,中断、陷阱、例外的入口堆栈组成都是不一样的。而simulated software interrupt则是平台无关的,统统都是:void (*)(void),正因为如此,我们看到在HalpApcInterrupt入口处为了使用ENTER_INTERRUPT,不得不按照中断入口的标 准,伪造了一个带eflags的堆栈帧,因为ENTER_INTERRUPT是针对中断入口编写的。
其它一些遗留问题,盼望各位一起分析。
Q:中断处理函数为什么可以重入?例如硬件中断处理函数,进入的时候肯定是处在该中断对应的irql上,而nt核心态编程的守则又规定程序不能在不先提升的情况下自行降低irql,这样新的中断应该是不可能获得执行才对啊?
A: 正在研究,似乎是因为delayed interrupt的缘故。(这个原因是,硬件的中断是没有条件的,加上本身nt采用lazy方式来与硬件pic同步,这个低优先级来了才屏蔽,来之前是不屏蔽的,所以它来了,屏蔽了就不来了,而来了就得执行,具体指不执行要看eflags寄存器,这是硬件决定的,但是os内核却真的不想让他执行,怎么 办,pending,延迟,也即是delayed interrupt)
*/
windows看来将硬件中断尽量往它自己的软件中断靠拢, 并且将虚拟中断作为真实中断的cache,在适当的时机进行同步,它自己的软件中断机制涉及pcr结构的虚拟irr,而irql实际就是虚拟的tpr寄存 器,在实现同步的时候有点小小的技巧,就是用了lazy模式,这样就不会丢失那个要来的但是优先级比较低的中断(等到将来的时间重新用int指令分发)。 看看linux的代码,它并没有使用tpr(tpr到底干什么用?简单的说tpr就是将低于当前tpr寄存器值得中断全部屏蔽掉),这就是说,linux 并不屏蔽任何优先级的中断,即使有一个比它正在处理的中断优先级低的中断到来,它还是会去处理的,从这个意义上说,linux根本没有中断优先级的概念, 它的原则就是:该来的就让它来,一切随缘,绝对不刻意阻止。看看代码:(来自2.6.27内核)
static void mask_and_ack_8259A(unsigned int irq)
{
unsigned int irqmask = 1
unsigned long flags;
spin_lock_irqsave(&i8259A_lock, flags);
if (cached_irq_mask & irqmask)
goto spurious_8259A_irq;
cached_irq_mask |= irqmask;
handle_real_irq:
if (irq & 8) {
inb(PIC_SLAVE_IMR); /* DUMMY - (do we need this?) */
outb(cached_slave_mask, PIC_SLAVE_IMR);
/* 'Specific EOI' to slave */
outb(0x60+(irq&7), PIC_SLAVE_CMD);
/* 'Specific EOI' to master-IRQ2 */
outb(0x60+PIC_CASCADE_IR, PIC_MASTER_CMD);
} else {
inb(PIC_MASTER_IMR); /* DUMMY - (do we need this?) */
outb(cached_master_mask, PIC_MASTER_IMR);
outb(0x60+irq, PIC_MASTER_CMD); /* 'Specific EOI to master */
}
spin_unlock_irqrestore(&i8259A_lock, flags);
return;
spurious_8259A_irq:
...
看到了吧,根本没有设置什么tpr寄存器,仅仅保证当前中断不会重入就完事了,linux凭什么敢这么干?屏的就是它强大的软中断机制,它的软中断机制真的很强大吗?事实上真的很强大,softirq在硬件中断完了后紧接着执行,但是如果softirq太多怎么办(事实上真的很多,因为它除了当前中 断外并不屏蔽任何中断),太多了的话对于windows就没有办法了,毕竟它不能总在运行dpc过程(它在任意上下文,会造成用户线程饥饿),对于 linux,专门有一个ksoftirqd内核线程,这个线程拥有线程上下文,可随意延迟。windows实现的比较商业化,它尽量和硬件统一,硬件提供 什么功能,它就虚拟什么功能,而linux则实现比较灵活,它可能根本不买硬件的账,再一个是为了移植性,试想万一对应硬件平台没有实现中断优先级怎么 办?
在windows对于虚拟中断的实现里面每当一个中断来临时,就可能设置pic的tpr寄存器,前提是当前正在处理更高优先级中断,用此相对低优先级中断的优先级设置完tpr后然后再通过这个优先级变形(位运算)后或上当前屏蔽字后设置pic的屏蔽寄存器,最后就把这个相对低优先级的中断pending, 以备将来分发。将来分发时就是用tpr寄存器取反与上pending寄存器,然后取左边第一个不是0的,就是要处理的第一个未决中断,看来windows 的实现是很复杂的,这样的好处就是,系统设计复杂了,但是驱动开发简单了,开发人员不用触及真实中断了。
solaris是个中断线程化的巨猛系统,每个cpu都有个lpi,这个概念等同于windows的irql,但是不同的是,它将中断线程化了,中断有了自己的上下文,每个cpu有15个lpi级别,10以上的保留,10以下的每个ipl对应一个线程池(也就是一个链表),中断来临时,由cpu当前的 ipl和它需要的ipl决定它将分配到那个队列的线程执行,当前cpu的ipl将需要之下ipl的中断屏蔽(是否这样要看在哪个硬件平台,简单说,不管 ipl还是irql都是虚拟出来的概念,理解时可以考虑虚拟内存原理),但是也就一瞬间,因为它唤醒相应线程就把ipl恢复了,ipl是和中断线程一一对 应的。solaris上有中断优先级的概念,这其实也是它原来只在sparc运行,而sparc拥有硬件中断优先级控制机制的缘故吧。linux却不能保 证硬件有这样的机制。怎么样,它的实现更纯粹些。