Interrupts

什么是中断

中断是一种异步事件通知机制。当特定事件发生时(硬件信号、CPU 异常、软件指令),CPU 会:

  1. 保存当前状态 - 保存程序计数器、寄存器等上下文
  2. 查找处理函数 - 根据中断类型,在中断描述符表(IDT)中找到对应的处理函数
  3. 执行处理逻辑 - 跳转到中断处理函数,处理该事件
  4. 恢复执行 - 恢复之前保存的状态,继续执行被中断的程序

中断的关键特性是异步性:被中断的程序不知道何时会被打断,也不需要主动配合。这让操作系统能够在不修改应用程序的情况下,响应外部事件

中断的类型

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 异常可以直接触发,但外部设备的中断需要通过中断控制器来管理

中断控制器负责:

  1. 接收外部设备的中断信号 - 多个设备可能同时发送中断
  2. 优先级仲裁 - 决定哪个中断应该先被处理
  3. 向 CPU 发送中断向量号 - 告诉 CPU 应该执行哪个中断处理函数

PIC(可编程中断控制器)

PIC

早期 x86 系统使用 8259 PIC(Programmable Interrupt Controller)。一个 PIC 芯片支持 8 个中断源,通过级联两个 PIC 可以支持 15 个中断(主 PIC 的一个引脚连接从 PIC)

PIC 的局限性:

  • 只支持单核 CPU
  • 中断向量号固定,不够灵活
  • 性能较低,不适合现代高速设备

APIC(高级可编程中断控制器)

APIC

现代 x86 系统使用 APIC(Advanced Programmable Interrupt Controller),分为两部分:

  • Local APIC - 每个 CPU 核心内置一个,接收来自 I/O APIC 的中断和核间中断(IPI)
  • I/O APIC - 连接外部设备,将中断路由到指定的 Local APIC

APIC 的优势:

  • 支持多核 CPU,可以将中断分发到不同核心
  • 支持中断优先级和中断屏蔽
  • 支持核间中断(IPI),用于多核同步

操作系统启动时需要初始化 APIC,配置中断路由表,将每个硬件中断映射到合适的中断向量号

中断处理流程

interrupt_flow

当中断发生时,CPU 会执行以下步骤:

1. 中断触发

  • 硬件中断:外部设备通过中断控制器(APIC/PIC)向 CPU 发送中断信号
  • 异常:CPU 执行指令时检测到错误条件
  • 软件中断:程序执行 intsyscall 指令

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 时间用于真正的计算任务,而不是空转等待

中断机制涉及三个关键组件:

  1. 中断描述符表(IDT) - 存储每个中断的处理函数地址
  2. 中断控制器(APIC/PIC) - 管理硬件中断的优先级和路由
  3. 中断处理函数 - 执行具体的事件处理逻辑

没有中断,现代操作系统将无法高效地处理键盘输入、网络数据包、定时器事件等异步事件。中断让 CPU 从”主动询问”变为”被动响应”,是操作系统设计中的关键创新