9.5 中断体系与异常处理
在操作系统内核中,中断与异常机制是整个系统感知外部硬件事件、维系多任务并发调度以及构筑特权级安全边界的枢纽。处理器的正常执行流往往是确定且线性的,而外部物理设备的数据输入、时钟节拍的周期性到达以及算术除零或缺页等突发状态,都要求处理器能够在纳秒级的时间内暂停当前任务,平滑转入特权上下文予以响应,随后再无缝恢复先前的计算。
可以说,正是中断子系统的运转,才驱动了外设 I/O 响应、内存按需加载以及抢占式进程调度等核心行为。
1. 中断架构核心机制
x86 架构将中断与异常统一抽象为一组编号在 0~255 之间的中断向量。在不同的 CPU 执行模式下,寻址这组向量的具体硬件结构存在显著差异。
1.1 中断描述符表(IDT)
在 32 位保护模式与 64 位长模式下,CPU 借由中断描述符表(Interrupt Descriptor Table,简称 IDT)来管理所有中断与异常的处理入口。
IDT 表具有如下核心特征:
- 线性基址任意重定位:IDT 可以驻留在物理内存或分页后的线性地址空间的任意位置,处理器通过加载到
IDTR寄存器中的信息进行寻址。IDTR包含了 IDT 的基地址以及 16 位的界限长度(Limit,表示 IDT 字节长度减 1)。为了获得最佳的总线访问效率,IDT 基地址通常按 8 字节边界对齐。 - 描述符并非指针,而是门结构:IDT 中的表项被称为“门描述符”(Gate Descriptor)。在保护模式下,门描述符占 8 字节,包含目标代码段选择子、处理函数的段内偏移地址、特权级属性以及存在位(Present)。
- 门描述符的三种主要类型:
- 中断门(Interrupt Gate):进入中断例程时,硬件会自动清零
EFLAGS寄存器中的IF标志位以禁用可屏蔽中断,防止同级或低级中断重入破坏未保存的上下文,待执行iret指令恢复现场时再自动复原IF位; - 陷阱门(Trap Gate):进入例程时保留
IF标志位的当前状态,允许后续可屏蔽中断继续打断,常用于断点调试或系统调用; - 任务门(Task Gate):利用硬件任务状态段(TSS)实现完整的硬件级任务切换,现代系统由于软件上下文切换效率更高,已较少使用任务门。
- 中断门(Interrupt Gate):进入中断例程时,硬件会自动清零
- 特权级防护规则:门描述符中定义了描述符特权级(DPL)。当由用户空间通过
int <vector>软件指令尝试触发中断时,CPU 会严密校验当前特权级(CPL)是否不低于门描述符的 DPL,从而防止非特权代码越权直接调用敏感的底层例程。
1.1.1 历史渊源:实模式中断向量表(IVT)
在 16 位实模式下,承担中断索引任务的结构被称为中断向量表(Interrupt Vector Table,简称 IVT)。与 IDT 不同,IVT 的物理基地址由硬件固化在内存最底部的 0x00000000 至 0x000003FF 区间,恰好占 1 KiB 空间。256 个向量每项占用 4 字节,由 2 字节的段基地址和 2 字节的段内偏移构成一个远指针。由于实模式缺乏特权级隔离机制,IVT 表项不包含任何访问控制与存在位检查。
1.2 处理器内部异常(Exceptions)
异常属于 CPU 内部检测到的非可屏蔽事件,通常由正在执行的指令直接触发。依据架构规范,Intel 将前 32 个中断向量(向量号 0~31)严格保留为系统级异常:
- 错误(Fault):发生在引起异常的指令执行之前,通常可以被修复并重新执行该指令(如缺页异常 Page Fault,向量 14);
- 陷阱(Trap):发生在引起异常的指令执行之后,返回地址指向下一条指令(如单步调试陷阱或溢出检查);
- 中止(Abort):发生致命硬件故障或不可恢复的逻辑错误,处理程序通常无法恢复原任务的继续执行。
在异常嵌套处理的时序中,存在两类特殊的升级状态:
- 双重错误(Double Fault,向量 8):当处理器在尝试触发某个既有异常处理例程期间,再度引发了另一个无法协同的异常,CPU 会转向执行双重错误处理函数;
- 三重错误(Triple Fault):若在双重错误处理函数的加载或执行过程中又引发了新的致命错误,CPU 将判定系统已陷入不可逆的崩溃状态,随即拉低处理器的关机引脚信号,导致物理主板直接硬重启。
下图列出了 x86 架构常见异常向量及其属性:

关于各异常的详细触发条件与错误码压栈规则,亦可参考 OSDEV Exceptions。
1.3 外部硬件中断(IRQ)与控制器演进
外部硬件中断(Interrupt Request,简称 IRQ)由处理器外部的外设控制器产生,用于通知内核设备数据已就绪或传输已完成。硬件中断属于可屏蔽中断,其投递受 EFLAGS.IF 控制位支配。
在 x86 系统发展史中,中断路由架构经历了关键迭代:
1.3.1 8259A 可编程中断控制器(PIC)
在经典 PC/AT 架构中,系统通过两片 8259A 芯片级联来管理 15 条硬件中断线。其配置逻辑简单,通过少数几组 I/O 端口即可完成向量重映射与掩码读写。然而,8259A 仅支持单处理器环境,硬件中断只能以串行方式送往单个 CPU 核心,无法适应现代多核处理器的并发需求。
1.3.2 高级可编程中断控制器(APIC)
为了支持对称多处理(SMP),现代 PC 引入了 APIC 架构,由集成在各 CPU 核心内部的 Local APIC 以及主板芯片组上的 I/O APIC 协同构成。I/O APIC 负责将外设的物理引脚中断重定向并动态分发给特定的 CPU 核心,Local APIC 则负责处理核间中断(IPI)以及高精度本地时钟中断。有关 APIC 的深入细节可参见 ACPI 与高级中断控制器。
PCI 消息传递中断(MSI / MSI-X)
在现代 PCI/PCIe 总线体系中,外设逐渐淘汰了物理中断引脚,转而通过向指定的主机内存物理地址执行写入操作(Message Signaled Interrupts)来直接触发中断。这种机制免除了总线仲裁与中断线共享的寻址开销,极大提升了多设备并发时的响应吞吐。
1.4 软件中断与系统调用接口
软件中断是由内核或应用程序主动执行诸如 int <vector> 等机器指令而显式触发的执行流转移:
int <vector> ; x86 触发软中断的指令,vector 代表预设的中断向量号软件中断在指令层面由软件自发引发,不受 EFLAGS.IF 屏蔽位的限制,即便在 cli 关闭中断的状态下执行 int 指令,CPU 依然会强制执行门描述符寻址。经典 Unix 系统正是借由特定软中断向量(如 0x80)实现由用户态特权级向内核态的主动陷入。
2. 习题
IDTR 里的限长是「以字节为单位」的 IDT 长度。
(1) 建满 256 个门描述符时,正确的限长是多少(十六进制)?如果误把限长写成 0x00FF,向量 31 还能用吗?向量 32 呢?
(2) 门描述符占 8 字节,中断门和陷阱门的偏移在描述符里被拆成三段存放。给定处理函数地址 0x00102345、内核代码段选择子 0x0008、类型与属性字节 0x8E,写出这个门的 8 个字节(按内存地址从低到高),并说明 0x8E 里每一位的含义。
(3) 对比遍历 IDT 的两种写法:按限长算 (limit + 1) / 8 个表项,以及固定写 256。哪一种在这块 IDT 上是对的?在哪些情况下另一种才对?写一个 int idt_entries(uint16_t limit) 返回表项数,对 0x07FF、0x00FF、0x0017 分别输出什么。
(4) 实模式下的 IVT 固定从 0x00000000 开始,每项 4 字节,共 256 项。写出向量 0x08、0x10、0x21 对应的表项地址;给出一行 C 代码,从一个 uint16_t 指针出发同时取出 0x21 号向量的偏移与段基址,并说明偏移与段基址在表项里的先后顺序。
从 code/项目/mdrOS/interrupt.asm 里摘出的两段存根如下(宏定义与 isr_common_stub 的完整版本见该文件):
%macro ISR_NOERRCODE 1
[global isr%1]
isr%1:
cli
push byte 0
push %1
jmp isr_common_stub
%endmacro
%macro ISR_ERRCODE 1
[global isr%1]
isr%1:
cli
push %1
jmp isr_common_stub
%endmacro2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(1) x86 在进入异常时,哪些异常由 CPU 自动压入错误码?对照本页 1.2 的异常表,数出 ISR_ERRCODE 应该用在哪些向量上,与文件里实际的宏调用逐条核对。
(2) 两个宏里 push 的顺序不同,isr_common_stub 最后统一执行 add esp, 8。说明这样统一处理的前提,以及为什么无错误码的宏要先压一个 0。
(3) 存根开头有 cli,而中断门本身会清 IF,陷阱门不会。分别说明用中断门和陷阱门描述同一个存根时,这个 cli 各起什么作用。int 3 通常配哪种门,为什么?
(4) 写出与 isr_common_stub 传参相符的 registers_t 结构体,用 offsetof 打印各字段偏移,并加 _Static_assert 断言 add esp, 8 弹掉的「中断号、错误码」两个字段正好在结构体末尾。验收:ds 在偏移 0,各字段都是 4 字节,结构体总长为 44 字节。
PIC 的 16 个 IRQ 默认映射到向量 0x08~0x0F 与 0x70~0x77,这会盖住异常向量。正文把 IRQ 重映射到 32 起。
(1) 按主片基址 0x20、从片基址 0x28,写出从片 8 个 IRQ 的向量号。IRQ 15 的向量号是多少(十进制)?
(2) 重映射要依次写 0x20、0x21、0xA0、0xA1 四个端口,顺序为 ICW1→ICW2→ICW3→ICW4。ICW 各位的含义与端口对应关系查 OSDev 的 8259A 页面或芯片手册。写出每一步往哪个端口写什么值,并说明 ICW3 主片与从片的值为什么不同。
(3) 一个 IRQ 处理完后要发 EOI。如果从片上的设备触发中断,只往主片 0x20 写 0x20 会发生什么?正确顺序是什么?
(4) 写一个主机程序模拟 PIC 的请求寄存器与在服务寄存器(IRR、ISR、IMR 各 8 位,用两个 uint8_t 对象表示主片和从片):实现 pic_raise(irq)、pic_mask(irq, int masked)、pic_eoi(irq)、pic_next(void)。pic_next() 返回当前可服务的最高优先级 IRQ 号,没有则返回 -1;从片的中断要经过主片的 IRQ 2 才能送到 CPU,模拟时要体现这一点。
写测试覆盖:只开 IRQ 3 与 IRQ 11 时各自触发一次、IRQ 3 在服务时 IRQ 11 到达、IRQ 11 在服务时 IRQ 3 到达。打印每一步的 IRR、ISR。验收:前两种情况分别返回 3 和 11(开启 IRQ 11 时别忘了级联用的 IRQ 2);IRQ 3 在服务时再次触发 IRQ 3 应排队等待 EOI,而不是重入。
本页 1.1 列出了 IDT 里的三种门描述符。这一题把门描述符与保护检查连起来,做一个能跑在主机上的检查器。
给定一组门(向量号、类型、DPL、选择子、偏移、P 位)与一次 int n / int3 / 外部 IRQ 的触发上下文(CPL),判定这次触发是否被允许、进入处理函数前栈上会多出哪些内容:
(1) 用 C 写 int gate_check(const struct gate *g, unsigned vector, int cpl, int source),source 取 SOFT_INT、SOFT_INT3、SOFT_INTO、HARDWARE。按 Intel 手册的规则判定 DPL 检查:软件中断要求 CPL <= gate.DPL;int3 与 into 不检查 DPL;硬件中断不检查 DPL。P 位为 0 时一律触发 #NP。
(2) 判定允许后,输出这次进入会压栈的先后内容(x86-32,同特权级与跨特权级两种情形),以及是否切换栈、从 TSS 的哪个字段取新 esp。
(3) 造 12 组用例覆盖:三种门各一组、DPL 0/3 与 CPL 0/3 的四种组合、P=0、int3 与 int 3 的不同结果、跨特权级、任务门。每组打印判定结果与压栈列表。验收:int3(DPL 0 的门)在 CPL 3 下允许,而 int 3 在同样条件下被拒。
(4) 说明为什么 IDT 的限长检查与 DPL 检查是两回事,各在什么阶段发生;写一个 5 行的表格概括。