操作系统关于中断的实现

/*Interrupt, trap and exception handling in Windows nt 把硬件中断映射到software interrupt request level 上了,实现了很好的隔离和跨平台特性。对驱动程序员来说,除了在获取资源列表和调用IoConnectInterrupt 之外,几乎接触不到硬 件中断。所有和中断硬件(主要是pic )打交道的代码都集中在hal 的一个角落里,hal 的大部分以及全部的kernel 只和一个虚拟的中断控制器打交 道。在这个虚拟的中断控制器里,nt 定义了32 个软件中断级别,当硬件中断发生的时候,hal 将硬件中断映射成这32 个软中断之一,并更新虚拟中断控制器 的内部状态保持和硬件中断控制器同步。从kernel 往上到执行体以及驱动程序,所有的代码也都是和这个虚拟出来的中断控制器打交道,最大量使用的操作 是,keex 或者driver 通过操纵irql 来控制当前活动的优先级,使得低优先级的活动不至于干扰当前计算任务,同时保持对高优先级计算任务的及时 响应。于是,nt 可以将不同的计算任务分配到不同的优先级上,从而为高效使用cpu 资源提供了相当的灵活性。
  nt
做了大量的工作,保证这个虚拟中断控制器(pic) 像硬件pic 一样精确而可靠的工作,这个虚拟pic 的支持数据结构都位于PCR ,也就是 processor control region 中,PCRhalkernel 共享的一个非常重要的数据结构。(pcr 这个结构很好,实际上它就是硬件抽象层抽象出来的真实cpu)
 
现在流行可编程设备,像8259 之类的简单器件都可以从网上下载到freeip 了,要了解它的工作原理或者把它变成软件比从前容易多了。在当年,不要说实现一个虚拟的pic ,就是能有这个想法的,大概也得算是软硬通吃的牛人了。
 
作为一个比较粗糙的总结,我认为,理解了下面的功能,基本上就能掌握nt 的虚拟中断控制器的实现原理了。这个总结不是很好,在那些熟悉硬件的看来,只能说是贻笑大方了。
*) interrupt request register
:记录尚未被cpu 处理的中断,以便cpu 在适当的时候查询。
*) interrupt mask
:屏蔽不重要的中断,在nt 中通过irql 实现
*) interrupt request recognizing
nt 在适当的时候会检查IRR ,如果发现有比当前irql 高的未处理中断,则会立刻启动该中断的处理程序,这个检查的过程我称为 recognizingrecognizing 发生在很多场合,KeLowerIrql 是最经常的一个,HalRequestSoftwareInterrupt 也是一个。
*) interrupt routing /& dispatching
:相当于硬件的中断向量表,nt 也有个软件中断派发表,SWInterruptTableHandlerTable ,后面将要看 到,在向x86 上,这个派发表和idt 协同工作实现中断的派发。
*) interrupt acknowledgement
:虚拟pic 的状态也可以看成是硬件pic 的状态的cache 。如同memory cache 一样,虚拟pic 的状态和硬件pic 也不总是一致。事实上,在提升irql 的时候,nt 采用lazy irql 技术将对硬件imr 的编程推迟到当被屏蔽的中断到来的时候,这个策略和memory cachelazy
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
:也是打扫战场的。
 
除了clockprofilent 内置的中断处理程序,硬件中断的处理函数都是动态生成的,所以用!idt 调试命令看不到相关符号信息。这种动态生成 的代码实际上是一个thunk ,可以参见前面关于thunk 的那个文章,源代码:KeInitializeInterruptx86hal 中以及 kernelx86 部分,很多代码是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 响应该中断期间,如果设备撤销了该中断请求,picinterrupt pending registerinterrupt status register 会发生不一致,中断响应代码检查interrupt status 的时候会发现没有设备中断,这个就称为spurious interrupt 。有点像顽皮的小孩乱按门铃,等你出去的时候却看不到敲门的人。
Q
:什么是delayed interrupt
A
: 在lazy irql 期间,如果没有中断到达,则nt 就会摆谱儿不去更新硬件pic 的状态。直到低优先级的中断溜进来,nt 才忙不迭的堵后门去设置picimr ,但 这个已经来了的中断怎么办呢?不能就这么丢了啊,nt 于是把这个中断记录在pcrirr 里,这样就形成一个所谓delayed interrupt ,也有称postponed interruptnt 将在随后适当的时候用软件模拟的方式重新产生这个中断事件。需要注意的事,apcdpc 也可能产生delayed interrupt ,这个事实应该比硬件中断产生的delayed interrupt 更好理解,在中断处理函数里调用KeInsertQueueDpc ,显然这个dpc 不能立刻开始执行,因此只能作为delayed interrupt 。由于delayed interrupt 是通过软件方式模拟的,所以硬件中断就这样摇身一变成了软件中断,并且将和apcdpc 一样通过软件中断派发表派发(而不是 idt )。
Q
nt 在什么时候执行delayed interrupt
A
:显然在当降低irql 的时候检查是比较合适的,nt 降低irql 的途径主要是通过三个函数:
HalpEndSystemInterrupt
HalpEndSoftwareInterrupt
KfLowerIrql
在 其中会针对每个比当前irql 高的delayed interrupt 事件,通过SWInterruptHandlerTable 派发,对apcdpc 两个软件中断来说,会直接派发到 HalpApcInterrupt HalpDispatchInterrupt ;而对于其它也就是硬件中断产生的delayed interrupt ,实际上会被派发到一个stub 函数,这个函数里只包含一个int xx 指令,其中xx=interrupt vector + PRIMARY_VECTOR_BASE(0x30) 。这样我们顺便知道idt 中从0x300x3f 的作用了---- 用来仿真一个delayed hardware interrupt 。这个话题延伸开去,就是idt 的布局问题了,也是一个比较有意思的话题,不过牵涉到的硬件部分暂时不是很熟悉,暂且打住。(按: 惊!)
Q
KfLowerIrqlHalEnd{System|Software}Interrupt 有何区别?
A
:从语义上看,这两 个函数完全是一回事,看它们的注释就知道,三者都是降低irql ,同时检查是否有需要派发的软件中断(包括delayed interrupt )。但读代码的时候会注意到,KfLowerIrql 只处理一个pending software interrupt ,而后两者则会循环处理每个pending software interrupt 。这个原因似乎是因为只要KfLowerIrql 模拟产生一个软件中断事件,那么那个事件的中断处理函数最终肯定会调用 HalpEndxxxInterrupt ,因此会在那里处理所有的中断,这样在KfLowerIrql 里就没有必要循环了。但这个分析我自己很不满意,仔 细查看HalpEndSystemInterruptHalpEndSoftwareInterrupt 发现有些细小的区别,不目前不是很清楚是什么原 因。
Q
:为什么需要在HalpEndxxxInterrupt 检测delayed interrupt 的重入?
A
:硬件中断的堆栈帧太大 了(查看ENTER_INTERRUPT 可知,有将近100 个字节),所以不得不采取一定的措施,不然kmode 12k 堆栈很快就报销了。至于apcdpc 中断,则可以复用当前中断的堆栈帧,也是一种优化。注:为什么硬件中断和软件中断有这个区别,目前还没有想明 白。
Q
:为什么delayed hardware interrupt 必须派发到用int 指令来仿真的stub 上,而apcdpc 中断则可以直接派发到函数地址上?
A
: 稍微动动脑筋就可以想到,如果不通过int 指令(进而通过idt 派发),那么派发到哪儿呢?也就是 SWInterruptHandlerTable 表里填什么呢?因为硬件中断处理函数的地址随系统运行可能会变化(尤其在PnP 环境下),所以,干脆用 int 指令仿真得了。反正在IoConnectInterrupt 的时候会将正确的中断处理函数地址填到idt 里头去。这就是最开始我们说 SWInterruptHandlerTableidt 一起完成中断派发的意思了。
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 的代码,它并没有使用tprtpr 到底干什么用?简单的说tpr 就是将低于当前tpr 寄存器值得中断全部屏蔽掉),这就是说,linux 并不屏蔽任何优先级的中断,即使有一个比它正在处理的中断优先级低的中断到来,它还是会去处理的,从这个意义上说,linux 根本没有中断优先级的概念, 它的原则就是:该来的就让它来,一切随缘,绝对不刻意阻止。看看代码:(来自2.6.27 内核)

static void mask_and_ack_8259A(unsigned int irq)

{

         unsigned int irqmask = 1 << irq;// 要将正在被处理的中断屏蔽掉。

         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 对于虚拟中断的实现里面每当一个中断来临时,就可能设置pictpr 寄存器,前提是当前正在处理更高优先级中断,用此相对低优先级中断 的优先级设置完tpr 后然后再通过这个优先级变形(位运算)后或上当前屏蔽字后设置pic 的屏蔽寄存器,最后就把这个相对低优先级的中断pending , 以备将来分发。将来分发时就是用tpr 寄存器取反与上pending 寄存器,然后取左边第一个不是0 的,就是要处理的第一个未决中断,看来windows 的实现是很复杂的,这样的好处就是,系统设计复杂了,但是驱动开发简单了,开发人员不用触及真实中断了。
  solaris
是个中断线程化的巨猛系统,每个cpu 都有个lpi ,这个概念等同于windowsirql ,但是不同的是,它将中断线程化了,中断有了 自己的上下文,每个cpu15lpi 级别,10 以上的保留,10 以下的每个ipl 对应一个线程池(也就是一个链表),中断来临时,由cpu 当前的 ipl 和它需要的ipl 决定它将分配到那个队列的线程执行,当前cpuipl 将需要之下ipl 的中断屏蔽(是否这样要看在哪个硬件平台,简单说,不管 ipl 还是irql 都是虚拟出来的概念,理解时可以考虑虚拟内存原理),但是也就一瞬间,因为它唤醒相应线程就把ipl 恢复了,ipl 是和中断线程一一对 应的。solaris 上有中断优先级的概念,这其实也是它原来只在sparc 运行,而sparc 拥有硬件中断优先级控制机制的缘故吧。linux 却不能保 证硬件有这样的机制。怎么样,它的实现更纯粹些。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值