共计 6126 个字符,预计需要花费 16 分钟才能阅读完成。
搞内核钻研的常常对中断这个概念必定不生疏,常常咱们会接触很多与中断相干的术语,依照软件和硬件进行分类:
硬件 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、指定了主片的工作模式为 80×86 模式,中断完结形式为一般完结模式
- 5、指定了从片的工作形式为级联、中断形式为电信号边际触发
- 6、指定了从片 IRQ 的中断向量映射基址:0x38
- 7、指定了从片的工作形式级联形式为主片的 IRQ2 这个管脚
- 8、指定了从片的工作模式为 80×86 模式,中断完结形式为一般完结模式
至此咱们能够晓得,在应用 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 实现此次中断解决。上面咱们重点来看看这两个函数。
BOOLEAN
HalBeginSystemInterrupt(
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 有没有记录没被解决的中断以便在这个时候进行解决。