Article
Interrupts
什么是中断
中断是一种异步事件通知机制。当特定事件发生时(硬件信号、CPU 异常、软件指令),CPU 会:
- 保存当前状态 - 保存程序计数器、寄存器等上下文
- 查找处理函数 - 根据中断类型,在中断描述符表(IDT)中找到对应的处理函数
- 执行处理逻辑 - 跳转到中断处理函数,处理该事件
- 恢复执行 - 恢复之前保存的状态,继续执行被中断的程序
中断的关键特性是异步性:被中断的程序不知道何时会被打断,也不需要主动配合。这让操作系统能够在不修改应用程序的情况下,响应外部事件
中断的类型
x86 架构将中断分为三类:异常(Exception)、硬件中断(Hardware Interrupt) 和 软件中断(Software Interrupt)
1. 异常(Exception)
异常由 CPU 内部产生,通常表示程序执行过程中遇到了错误或特殊情况:
- 除零错误(Divide by Zero) - 除法指令的除数为 0
- 页错误(Page Fault) - 访问未映射或无权限的内存地址
- 无效操作码(Invalid Opcode) - CPU 遇到无法识别的指令
- 断点(Breakpoint) - 调试器设置的断点指令
- 双重错误(Double Fault) - 处理异常时又发生了异常
异常是同步的:它们在特定指令执行时确定性地发生。例如,执行 mov [0], eax 访问空指针时,必然触发页错误
2. 硬件中断(Hardware Interrupt)
硬件中断由外部设备通过中断控制器发送给 CPU:
- 定时器中断(Timer Interrupt) - 周期性触发,用于任务调度
例如:Linux 默认每 10ms 触发一次,操作系统借此实现进程切换
- 键盘中断(Keyboard Interrupt) - 按键按下或释放时触发
例如:用户按下 Ctrl+C 时,键盘中断让操作系统能立即响应并终止程序
- 网卡中断(Network Interrupt) - 收到网络数据包时触发
例如:收到 TCP 数据包时,网卡通过中断通知操作系统处理
- 硬盘中断(Disk Interrupt) - 磁盘 I/O 完成时触发
例如:读取文件完成后,硬盘控制器发送中断,操作系统可以继续处理数据
硬件中断是异步的:它们可以在任意时刻发生,与当前执行的指令无关
3. 软件中断(Software Interrupt)
软件中断由程序通过特殊指令主动触发,常用于系统调用:
- int 0x80 - Linux 传统系统调用接口
- syscall - 现代 x86-64 系统调用指令
软件中断是同步的,但它是程序主动请求的,而不是错误
中断控制器
理解了中断的三种类型后,我们来看硬件中断如何到达 CPU。CPU 异常可以直接触发,但外部设备的中断需要通过中断控制器来管理
中断控制器负责:
- 接收外部设备的中断信号 - 多个设备可能同时发送中断
- 优先级仲裁 - 决定哪个中断应该先被处理
- 向 CPU 发送中断向量号 - 告诉 CPU 应该执行哪个中断处理函数
PIC(可编程中断控制器)

早期 x86 系统使用 8259 PIC(Programmable Interrupt Controller)。一个 PIC 芯片支持 8 个中断源,通过级联两个 PIC 可以支持 15 个中断(主 PIC 的一个引脚连接从 PIC)
PIC 的局限性:
- 只支持单核 CPU
- 中断向量号固定,不够灵活
- 性能较低,不适合现代高速设备
APIC(高级可编程中断控制器)

现代 x86 系统使用 APIC(Advanced Programmable Interrupt Controller),分为两部分:
- Local APIC - 每个 CPU 核心内置一个,接收来自 I/O APIC 的中断和核间中断(IPI)
- I/O APIC - 连接外部设备,将中断路由到指定的 Local APIC
APIC 的优势:
- 支持多核 CPU,可以将中断分发到不同核心
- 支持中断优先级和中断屏蔽
- 支持核间中断(IPI),用于多核同步
操作系统启动时需要初始化 APIC,配置中断路由表,将每个硬件中断映射到合适的中断向量号
中断处理流程

当中断发生时,CPU 会执行以下步骤:
1. 中断触发
- 硬件中断:外部设备通过中断控制器(APIC/PIC)向 CPU 发送中断信号
- 异常:CPU 执行指令时检测到错误条件
- 软件中断:程序执行
int或syscall指令
2. 保存上下文
CPU 自动执行一系列复杂的状态保存操作,确保中断处理函数能在正确的环境中运行:
2.1 对齐栈指针
任何指令都可能触发中断,所以栈指针可能是任何值。部分 CPU 指令(如 SSE 指令)需要栈指针 16 字节边界对齐,因此 CPU 会在中断触发后立刻进行对齐
2.2 切换栈(特权级改变时)
当 CPU 特权等级改变时(如用户态程序触发异常),会发生栈切换:
- 从任务状态段(TSS)或中断栈表(IST)中获取新的栈指针
- 切换到内核栈,避免使用可能不可信的用户栈
2.3 压入旧栈指针
在栈指针对齐之前,CPU 将栈指针寄存器(RSP)和栈段寄存器(SS)压入新栈,以便中断返回后恢复
2.4 压入并更新 RFLAGS 寄存器
RFLAGS 寄存器包含各种控制位和状态位。CPU 会:
- 将当前 RFLAGS 值压入栈
- 清除某些标志位(如中断使能标志 IF),防止中断嵌套
2.5 压入指令指针
CPU 将指令指针寄存器(RIP)和代码段寄存器(CS)压入栈,记录被中断的位置
2.6 压入错误码(部分异常)
某些异常(如页错误、通用保护错误)会额外压入一个错误码,用于标记错误的具体原因:
- 页错误:错误码指示是读/写错误、用户/内核模式、页是否存在
- 通用保护错误:错误码指示违规的段选择子
栈布局示例(特权级切换时):
高地址
+------------------+
| SS | ← 旧栈段
+------------------+
| RSP | ← 旧栈指针
+------------------+
| RFLAGS | ← 旧标志寄存器
+------------------+
| CS | ← 旧代码段
+------------------+
| RIP | ← 返回地址
+------------------+
| 错误码(可选) | ← 仅部分异常
+------------------+ ← 当前 RSP
低地址3. 查找处理函数
CPU 根据中断向量号(0-255)在中断描述符表(IDT)中查找对应的处理函数入口地址
4. 执行中断处理函数
跳转到中断处理函数,执行具体的处理逻辑:
- 读取设备数据(硬件中断)
- 终止进程或发送信号(异常)
- 执行系统调用(软件中断)
5. 中断返回
执行 iret 指令,从栈中恢复之前保存的状态,继续执行被中断的程序
这个流程保证了中断处理的透明性:被中断的程序感觉不到自己被暂停过,就像中断从未发生
中断描述符表(IDT)
中断描述符表(Interrupt Descriptor Table)是一个包含 256 个条目的数组,每个条目对应一个中断向量号(0-255)。CPU 通过 IDT 找到每个中断的处理函数
x86 架构预定义了前 32 个中断向量用于 CPU 异常,剩余的 32-255 可用于硬件中断和软件中断
IDT 结构示例
以下是一个典型的 IDT 结构:
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry<HandlerFunc>,
pub debug: Entry<HandlerFunc>,
pub non_maskable_interrupt: Entry<HandlerFunc>,
pub breakpoint: Entry<HandlerFunc>,
pub overflow: Entry<HandlerFunc>,
pub bound_range_exceeded: Entry<HandlerFunc>,
pub invalid_opcode: Entry<HandlerFunc>,
pub device_not_available: Entry<HandlerFunc>,
pub double_fault: Entry<HandlerFuncWithErrCode>,
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
pub page_fault: Entry<PageFaultHandlerFunc>,
pub x87_floating_point: Entry<HandlerFunc>,
pub alignment_check: Entry<HandlerFuncWithErrCode>,
pub machine_check: Entry<HandlerFunc>,
pub simd_floating_point: Entry<HandlerFunc>,
pub virtualization: Entry<HandlerFunc>,
pub security_exception: Entry<HandlerFuncWithErrCode>,
// some fields omitted
}关键字段说明:
- divide_by_zero (0) - 除零异常,除法指令除数为 0 时触发
- debug (1) - 调试异常,单步执行或断点触发
- breakpoint (3) - 断点异常,执行
int 3指令时触发 - page_fault (14) - 页错误,访问未映射或无权限的内存时触发
- double_fault (8) - 双重错误,处理异常时又发生异常
- general_protection_fault (13) - 通用保护错误,违反段保护或特权级检查
每个条目的类型取决于是否需要错误码:
HandlerFunc- 不带错误码的处理函数HandlerFuncWithErrCode- 带错误码的处理函数(如页错误、通用保护错误)PageFaultHandlerFunc- 页错误专用处理函数,包含错误码和触发地址
装载 IDT
操作系统启动时需要初始化 IDT 并通过 lidt 指令告诉 CPU:
// 初始化 IDT
let mut idt = InterruptDescriptorTable::new();
idt.divide_by_zero.set_handler_fn(divide_by_zero_handler);
idt.page_fault.set_handler_fn(page_fault_handler);
// ... 设置其他处理函数
// 装载 IDT
idt.load();一旦 IDT 装载完成,CPU 就能正确响应各种中断和异常
总结
中断是操作系统响应异步事件的核心机制:
- 异步通知 - CPU 不再轮询设备,而是在事件发生时被主动通知
- 透明处理 - 被中断的程序无感知,操作系统自动保存和恢复状态
- 高效利用 - 释放 CPU 时间用于真正的计算任务,而不是空转等待
中断机制涉及三个关键组件:
- 中断描述符表(IDT) - 存储每个中断的处理函数地址
- 中断控制器(APIC/PIC) - 管理硬件中断的优先级和路由
- 中断处理函数 - 执行具体的事件处理逻辑
没有中断,现代操作系统将无法高效地处理键盘输入、网络数据包、定时器事件等异步事件。中断让 CPU 从”主动询问”变为”被动响应”,是操作系统设计中的关键创新