中断处理 - 理论讲解

学习目标

本章你将学会:

  • 理解中断的概念和工作原理
  • 配置中断描述符表(IDT)
  • 编写中断服务程序(ISR)
  • 重映射可编程中断控制器(PIC)
  • 处理CPU异常和硬件中断

1. 什么是中断?

中断是CPU暂停当前执行流程、转去处理某个事件的机制。中断是操作系统最核心的机制之一。

中断的来源

┌───────────────────────────────────────────────┐
│                 中断来源                        │
├─────────────┬──────────────┬──────────────────┤
│  CPU异常     │  硬件中断    │  软件中断         │
│ (向量 0-31)  │ (向量 32-47) │ (INT指令)        │
├─────────────┼──────────────┼──────────────────┤
│ 除零错误     │ 时钟(IRQ0)   │ 系统调用         │
│ 页错误       │ 键盘(IRQ1)   │ 调试断点         │
│ 通用保护错误 │ 硬盘(IRQ14)  │ ...              │
└─────────────┴──────────────┴──────────────────┘

中断处理流程

正常执行 → 中断发生 → 保存现场 → 查IDT → 执行ISR → 恢复现场 → 继续执行

详细步骤:

  1. 保存现场: CPU自动压入 EFLAGS、CS、EIP(有时还有错误码)
  2. 查找处理程序: 根据中断向量号查IDT
  3. 执行ISR: 跳转到中断服务程序
  4. 发送EOI: 对于硬件中断,告知PIC处理完毕
  5. 恢复现场: IRET指令恢复之前的状态

2. 中断描述符表(IDT)

IDT是保护模式下的中断向量表,每个条目描述一个中断处理程序。

IDT条目结构(8字节)

位:    0               15 16             31
      ┌─────────────────┬────────────────┐
      │   基址低16位     │  段选择子       │  字节 0-3
      ├─────────────────┼────────────────┤
      │   保留(全0)      │  标志位         │  字节 4-5
      ├─────────────────┼────────────────┤
      │   基址高16位     │                │  字节 6-7
      └─────────────────┴────────────────┘

标志位详解

7                           0
┌───┬──┬──┬─────────────────┐
│ P │DPL│ 0│  Gate Type     │
└───┴──┴──┴─────────────────┘
P = 1       (Present, 条目有效)
DPL = 00    (特权级0)
Gate = 1110 (32位中断门)

组合: 1 00 0 1110 = 0x8E

C语言表示

struct idt_entry {
    uint16_t base_low;   // 处理程序地址低16位
    uint16_t selector;   // 代码段选择子
    uint8_t  always0;    // 保留
    uint8_t  flags;      // 类型和属性
    uint16_t base_high;  // 处理程序地址高16位
} __attribute__((packed));

3. CPU异常(向量 0-31)

Intel预定义了32个CPU异常:

向量名称类型有错误码?
0除零错误Fault无
1调试异常Fault/Trap无
6无效操作码Fault无
8双重错误Abort有(总是0)
10无效TSSFault有
11段不存在Fault有
12栈段错误Fault有
13通用保护错误Fault有
14页错误Fault有

异常类型

  • Fault: 可以修复,修复后重新执行造成错误的指令
  • Trap: 报告完毕后执行下一条指令(如断点)
  • Abort: 严重错误,通常无法恢复

4. 可编程中断控制器(PIC)

8259A PIC

PC系统中有两个PIC芯片级联使用:

         主PIC (Master)              从PIC (Slave)
         端口 0x20/0x21             端口 0xA0/0xA1
         ┌──────────┐              ┌──────────┐
IRQ0 ────│          │              │          │──── IRQ8
IRQ1 ────│          │              │          │──── IRQ9
IRQ2 ────│    ◄─────┼──────────────┤          │──── IRQ10
IRQ3 ────│          │              │          │──── IRQ11
IRQ4 ────│          │              │          │──── IRQ12
IRQ5 ────│          │              │          │──── IRQ13
IRQ6 ────│          │              │          │──── IRQ14
IRQ7 ────│          │              │          │──── IRQ15
         └──────────┘              └──────────┘

为什么要重映射PIC?

BIOS默认将 IRQ 0-7 映射到向量 8-15,与CPU异常冲突!

重映射方案:

  • IRQ 0-7 → 向量 32-39(主PIC)
  • IRQ 8-15 → 向量 40-47(从PIC)

重映射代码解析

void pic_remap(void) {
    // ICW1: 开始初始化(级联模式 + 需要ICW4)
    outb(0x20, 0x11);  // 主PIC
    outb(0xA0, 0x11);  // 从PIC
 
    // ICW2: 设置基础向量号
    outb(0x21, 0x20);  // 主PIC: 向量32开始
    outb(0xA1, 0x28);  // 从PIC: 向量40开始
 
    // ICW3: 主从连接配置
    outb(0x21, 0x04);  // 主PIC: 第2号引脚连从PIC
    outb(0xA1, 0x02);  // 从PIC: 连到主PIC的IRQ2
 
    // ICW4: 8086模式
    outb(0x21, 0x01);
    outb(0xA1, 0x01);
 
    // 清除中断掩码(启用所有IRQ)
    outb(0x21, 0x00);
    outb(0xA1, 0x00);
}

5. 中断服务程序的编写

汇编入口(interrupt.asm)

为什么需要汇编?因为中断处理程序需要:

  1. 统一栈布局(为有/无错误码的中断创建一致的栈帧)
  2. 保存所有寄存器(pusha)
  3. 切换到内核数据段
  4. 调用C函数
  5. 恢复现场并 iret
; 无错误码的ISR宏
%macro ISR_NOERRCODE 1
global isr%1
isr%1:
    push dword 0      ; 伪错误码(统一栈布局)
    push dword %1     ; 中断号
    jmp isr_common_stub
%endmacro
 
; 有错误码的ISR宏(CPU已压入错误码)
%macro ISR_ERRCODE 1
global isr%1
isr%1:
    push dword %1     ; 中断号
    jmp isr_common_stub
%endmacro

栈帧布局

中断发生后的完整栈布局:

高地址
┌──────────────┐
│   EFLAGS     │  ← CPU自动压入
├──────────────┤
│   CS         │  ← CPU自动压入
├──────────────┤
│   EIP        │  ← CPU自动压入
├──────────────┤
│  错误码/0    │  ← CPU或我们压入
├──────────────┤
│   中断号     │  ← 我们压入
├──────────────┤
│  EAX~EDI     │  ← pusha
├──────────────┤
│   DS         │  ← 我们保存
├──────────────┤  ← ESP → 作为参数传给C函数
低地址

这个栈帧对应C语言中的 registers_t 结构体!

6. EOI(中断结束信号)

处理硬件中断后,必须发送EOI告知PIC:

void irq_handler(registers_t *regs) {
    // 如果来自从PIC(IRQ8-15),先给从PIC发EOI
    if (regs->int_no >= 40) {
        outb(0xA0, 0x20);
    }
    // 总是给主PIC发EOI
    outb(0x20, 0x20);
 
    // 调用注册的处理程序
    if (handlers[regs->int_no]) {
        handlers[regs->int_no](regs);
    }
}

忘记发EOI会导致该IRQ线路永远被屏蔽,不再收到中断!

7. 关键概念总结

概念说明
IDT中断描述符表,256个条目,每个8字节
ISR中断服务程序,处理中断的代码
PIC可编程中断控制器,管理硬件中断
EOI中断结束信号,告知PIC处理完毕
IRET中断返回指令,恢复EFLAGS/CS/EIP
Fault可恢复异常,重新执行出错指令
Trap执行下一条指令(如断点)

下一步

中断系统建立后,我们就能处理硬件事件了。下一章将实现键盘驱动——第一个真正的设备驱动程序!