搞内核钻研的常常对中断这个概念必定不生疏,常常咱们会接触很多与中断相干的术语,依照软件和硬件进行分类:
硬件CPU相干:
- IRQ
- IDT
- cli&sti
软件操作系统相干:
- APC
- DPC
- IRQL
始终以来对中断这一部分内容弄的只知其一;不知其二,操作系统和CPU之间如何协同工作也是很含糊。最近花了点工夫认真把这块常识进行了梳理,不当之处,还请高手指出,后行谢过了!
本文旨在解答上面这些问题:
- IRQ和IRQL之间是什么关系?
- Windows是如何在软件层面上虚构出IRQL这套中断机制的
- APC和DPC都是软件中断,既然是中断那么对应的IDT表项中的解决例程在哪里呢?
0x00 Intel 80386处理器的中断
首先,让咱们遗记Windows,从最开始的80386处理器开始,看看Intel设计它的时候是如何解决中断这个货色的。
先来看看这个诞生于1985年的CPU长什么样子:
看看那些伸出来的引脚,上面是它的引脚标注图:
留神用红圈标注的两个引脚,这两个就是80386处理器为中断留出的两个引脚。其中INTR是可屏蔽中断输出口,NMI是不可屏蔽中断输出口。
那么中断是如何输出给处理器的呢?那么多外部设备,而这只有一个引脚(临时只思考可屏蔽中断),这里就须要为CPU装备一个治理中断的秘书——可编程中断控制器PIC。这个秘书须要干哪些活呢?外部设备的中断都从它来进入中央处理器,所以它负责从外设接管中断信号,并依据优先级向CPU发动中断请求。最开始的这个PIC角色是一个代号为8259A的芯片在进行表演,这货长这样:
上面是它的引脚图:
其中IR0-IR7共8个引脚负责连贯外部设备, 8259A PIC的每个IR口都连贯着一条IRQ线,用于接管外设的中断信号。INT负责连贯CPU的INTR引脚,用于向CPU发动中断请求。通常状况下,应用两片8259A芯片进行级联,一片连贯CPU,称为主片,另一片连贯到主PIC的IR2引脚,称为从片,这样总共就能够连贯8+7=15个外设了。如下图所示:
在8259A中,默认状况下的优先级是主片IR0的中断请求优先级最高,主片IR7最低,从片IR0-7所有中断请求优先级都相当于IR2。所以IRQ线的优先级由高到低秩序为IRQ0,IRQ1,IRQ8-15,IRQ3-7。这是默认状况,能够通过编程扭转。
在8259a芯片外部有几个重要的寄存器:
中断请求寄存器: IRR,8bit,对应IR0-IR7,当对应引脚产生中断信号时,该bit地位1。
中断服务寄存器: ISR,8bit,对应IR0-IR7,当对应引脚的中断正在被CPU解决时,该bit地位1。
中断屏蔽寄存器: IMR,8bit,对应IR0-IR7,当对应位为1时,示意屏蔽该引脚产生的中断信号。
还有一个中断优先级裁决器: PR,当中断引脚有信号时,联合这次产生中断的IRQ号和ISR中记录的以后正在解决的中断信息,依据优先级来决定是否把这个新的中断信号报告给CPU,以此来产生中断嵌套。
上面是这15条IRQ线别离连贯的外设:
当初咱们来看看这个秘书是如何和CPU之间进行协调工作的。
当初假如咱们敲击了一个键盘按键,键盘有中断事件产生,这一事件通过IRQ1这根线告知了主PIC,主PIC通过外部一些判断解决后通过INT发送电信号到CPU侧的INTR。CPU在执行完以后的指令后,查看到INTR有信号,阐明有中断请求来了,再查看eflags中的IF不为零,示意以后容许中断,则发送信号给PIC的-INTA,通知它把本次中断的向量号发送过去。主PIC收到-INTA管脚上的信号后,通过D0-D7引脚,输入此次中断的中断向量号到数据总线(这里简化了交互过程,实际上有两次INTA信号的发送)。CPU拿到这个号后,就能够从IDT中寻找中断服务例程(ISR)进行解决了,前面的事大家都晓得了。
那PIC中的中断向量号是怎么来的呢?各个IRQ是如何对应到IDT中的各个项呢?这里就利用了中断控制器的可编程性来决定的了。
PIC全称为可编程中断控制器,那么它的可编程体现在哪些方面呢?参考资料2《i8259A中断控制器剖析一》一文有比拟具体的形容,大体包含编程指定主从片的IRQ线对应的中断在IDT表中的中断向量号、8259a中断控制器的中断形式、优先级形式、中断嵌套形式,中断屏蔽形式、中断完结形式等等,这些都能够由操作系统编程指定。具体的编程格局在参考资料3《i8259A中断控制器剖析二》一文中有图文介绍。
回到上一个问题,IRQ线上的中断如何和IDT中的条目对应起来,操作系统在初始化的时候,会通过对8259a芯片编程(读写I/O端口),将指定PIC芯片的起始向量号,并要求低三位为0,起始向量号依照8对齐,这样规定的起因是,当中断产生时,低三位将主动填充对应的IRQ号,这样就能够和起始向量号相加间接送给数据总线从而被CPU拿到。具体到Windows中,零碎初始化的时候对PIC的编程为:指定主片的起始中断向量号为0x30,指定从片的起始中断向量号为0x38。这样,通过中断控制器连贯的15个外设将被平坦的映射到IDT中0x30-0x40这一范畴中。Windows内核启动初始化过程中应用了hal!HalpInitializePICs对8259a芯片进行编程,ReactOS中代码如下:
其中0x20,0x21是主片的IO端口,0xa0,0xa1是从片的IO端口:
PRIMARY_VECTOR_BASE定义为:
具体8259a的编程办法就是读写IO端口,设置对应的管制命令,不必深入研究。咱们来看Windows编程8259a的时候指定了哪些货色。
- 1、指定了主片的工作形式为级联、中断形式为电信号边际触发
- 2、指定了主片IRQ的中断向量映射基址:0x30
- 3、指定了主片的级联形式为应用了本人的IRQ2这个管脚
- 4、指定了主片的工作模式为80x86模式,中断完结形式为一般完结模式
- 5、指定了从片的工作形式为级联、中断形式为电信号边际触发
- 6、指定了从片IRQ的中断向量映射基址:0x38
- 7、指定了从片的工作形式级联形式为主片的IRQ2这个管脚
- 8、指定了从片的工作模式为80x86模式,中断完结形式为一般完结模式
至此咱们能够晓得,在应用8259A中断控制器的计算机上,通过IRQ线连贯的那15个外设可屏蔽中断是被操作系统线性的映射到了IDT中的一个范畴段。在Windows中是0x30-0x40(PS:在Linux中是0x20-0x2F),同时指定了中断控制器的中断形式为边际触发,完结模式为一般完结模式(也就是须要CPU侧告知中断解决有没有完结并设置对应bit位,不能主动设置)。
0x02 8259a上的Windows IRQL
上面来看看IRQL。
从后面咱们看到,硬件层面曾经对中断的解决提供了很好的反对,须要操作系统做的也就两点:首先,初始化的时候对PIC进行编程设置其工作形式并对IRQ进行映射,让这些中断对应到IDT中的各个项,其次,实现这些IDT中的中断服务例程。仿佛这样就够了,那Windows弄出来的一套IRQL又是什么货色呢?
看看《Windows Internals》一书对IRQL的定义:
写驱动的时候常常会接触到IRQL这个概念,它实现了Windows里的中断优先级制度,高优先级的中断总是能够优先被解决,而低优先级的中断则不得不期待高优先级中断被解决完后才失去解决。软件虚构进去的这一套机制怎么能管到硬件的优先级呢?这是如何实现的呢?
先来解决两个问题:
1、IRQ和IRQL的关系是什么?、应用KeRaiseIrql晋升以后IRQL后,为什么就能保障不被低优先级的中断打搅?
对于第一个问题,在应用8259a中断控制器的计算机中,IRQL=27-IRQ,其就是一个线性关系。
对于第二个问题,《Windows Internals》一书是这样解答的:
上面咱们具体来看Windows的实现:
IRQL是一个齐全虚构进去的概念,Windows为了实现这一个虚构的机制,齐全虚构了一个中断控制器,它在KPCR中:
+0x024 Irql : UChar //IRQL+0x028 IRR : Uint4B //虚构中断请求寄存器+0x02c IrrActive : Uint4B //虚构中断在服务寄存器+0x030 IDR : Uint4B //虚构中断屏蔽寄存器
在后面第一局部提到过,通过两片8259a芯片连贯的15个中断源被映射到处理器IDT中的一段范畴,具体Windows而言,是在0x30-0x40这个范畴。这15个IDT中的中断描述符所形容的中断解决例程(ISR)不同于int 3所对应的KiTrap03和int 0e所对应的KiTrap0E,他们的ISR指向的代码位于各自的中断对象KINTERRUPT的DispatchCode。上面是这个构造的定义:
typedef struct _KINTERRUPT { CSHORT Type; CSHORT Size; LIST_ENTRY InterruptListEntry; PKSERVICE_ROUTINE ServiceRoutine; PVOID ServiceContext; KSPIN_LOCK SpinLock; ULONG TickCount; PKSPIN_LOCK ActualLock; PVOID DispatchAddress; ULONG Vector; KIRQL Irql; KIRQL SynchronizeIrql; BOOLEAN FloatingSave; BOOLEAN Connected; CHAR Number; UCHAR ShareVector; KINTERRUPT_MODE Mode; ULONG ServiceCount; ULONG DispatchCount; ULONG DispatchCode[106];} KINTERRUPT, *PKINTERRUPT;
复制代码 DispatchCode外面的代码是依据一个模板来的,这些ISR解决开始和KiTrap03这些一样,首先会建设陷阱帧,而后会获取本人所在KINTERRUPT对象地址,失去这两个参数之后,便开始应用KiInterruptDispatch或KiChainedDispatch(如果对该中断注册了多个KINTERRUPT构造形成了链表应用此函数)进行中断差遣。而在这两个具体的差遣中都会先调用HalBeginSystemInterrupt,而后才会执行对应中断的理论解决工作,最初会执行HalEndSystemInterrupt实现此次中断解决。上面咱们重点来看看这两个函数。
BOOLEANHalBeginSystemInterrupt( IN KIRQL Irql IN CCHAR Vector, OUT PKIRQL OldIrql);
输出参数Irql示意本次产生的中断对应的的IRQL,Vector示意中断向量号,如前所述,这两个参数都是DispatchCode从本人所在KINTERRUPT对象中取出来的。
HalBeginSystemInterrupt外部应用IRQL参数在一个表格中进行了散发,这个表中除了个别函数不同外(其实也只是多了一层判断),其余表项都是统一的,在ReactOS中名为HalpDismissIrqGeneric,该函数间接转而调用其下划线版本_HalpDismissIrqGeneric。这里就是IRQL优先级实现的外围所在了。该函数不长,上面是ReactOS中的代码(在Windows2000代码中是汇编模式不如ReactOS应用的C语言模式直观,所以采纳了ReactOS的代码进行阐明):
首先,判断本次产生的中断对应的IRQL与以后处理器(KPCR)中的IRQL进行比拟,如果大于了以后处理器的IRQL,则示意来了一个优先级更高的中断,这时设置KPCR中的IRQL为这个新的更高的数值,前面返回了TRUE,示意须要解决这次中断请求。如果不大于以后处理器的IRQL的话,首先把本次中断记录记录到KPCR中的虚构中断控制器的IRR值,而后就间接通过KiI8259MaskTable表中选取以后处理器IRQL对应的屏蔽码写入PIC,用以屏蔽那些IRQL比本人低的中断源,前面返回FALSE,示意不解决这次中断请求。为什么不在设置处理器新IRQL的时候就进行设置屏蔽码呢?《Windows Internals》是这样解释的:
HalpDismissIrqGeneric的返回值将间接作为HalBeginSystemInterrupt的返回值。以中断差遣函数KiInterruptDispatch为例看看它是如何应用这个返回值的:
能够看出,如果HalBeginSystemInterrupt返回了FALSE,则间接导致本次中断解决提前结束。只有当HalBeginSystemInterrupt返回了TRUE时,才继续执行真正的中断解决例程。最初, 状况下都会调用KiExitInterrupt完结中断处理过程,看一下这个函数。联合KiInterruptDispatch的代码,能够看出,只有当HalBeginSystemInterrupt返回的是TRUE时,上面的if条件才会成立,从而进入HalEndSystemInterrupt。
最初看一下HalEndSystemInterrupt,后面提到如果产生的中断对应的IRQL低于处理器的IRQL,则不会执行其ISR,但会在KPCR中的虚构中断控制器的IRR中记录起来,等到处理器执行完了高IRQL的工作时,到了HalEndSystemInterrupt的时候,就会升高处理器的IRQL并从新设置PIC的中断屏蔽码,另外很重要的就是去查看IRR中的记录,如果记录中有比升高后的IRQL高的记录,则差遣该中断。
→【技术文档】←
总结
最初总结一下应用8259a中断控制器的计算机中Windows的IRQL。
首先,系统启动时对8259a芯片编程,设置其工作形式,并将15个中断源(IRQ)映射到IDT中的0x30-0x40这一段。
第二,Windows本人定义了一个称为中断请求级的IRQL概念用来形容中断的优先级别,IRQL是一个DWORD,共计32个级别,Windows应用一个简略的线性关系来映射IRQ和IRQL:IRQL=27-IRQ。
第三,被映射中断请求的0x30-0x40这一段的中断描述符的每个ISR都指向了一个KINTERRUPT构造中的DispatchCode,这段DispatchCode应用中断差遣函数KiInterruptDispatch或KiChainedDispatch进行中断差遣。
第四,差遣过程为:先应用HalBeginSystemInterrupt对本次中断的IRQL进行判断来决定是否须要解决本次中断,若不须要,则设置中断控制器的屏蔽码,避免再被打搅,同时将本次中断注销在KPCR中的虚构中断控制器IRR中。若须要则晋升IRQL,进而执行该中断的理论解决例程,执行结束后应用HalEndSystemInterrupt升高IRQL,而后查看IRR有没有记录没被解决的中断以便在这个时候进行解决。