2. 异常处理基础:异常的分类、处理流程与处理程序

异常(Exception)是操作系统响应处理器内部或外部事件的一种机制,它迫使控制流从当前指令转移到预先定义的异常处理程序。理解异常的分类、处理流程以及处理程序的设计,是掌握 Trap 机制的核心基础。

2.1 异常的分类:故障、陷阱与终止

根据异常发生的原因、触发时机以及是否可恢复,现代处理器通常将异常分为三类:故障(Fault)、陷阱(Trap) 和 终止(Abort)。这三类异常在恢复点、返回地址和处理方式上存在本质区别。

类别 触发原因 返回地址 是否可恢复 典型示例
故障(Fault) 指令执行过程中检测到的错误条件 指向引发异常的指令 通常可恢复(修复后重试) 缺页异常、除零错误、段错误
陷阱(Trap) 程序员主动触发的同步事件(系统调用) 指向下一条指令 总是可恢复 系统调用(int 0x80 / syscall)、断点指令
终止(Abort) 严重的硬件或系统错误 无定义(不返回) 不可恢复,通常终止进程或系统 机器检查异常、双故障、硬件校验错误
关键区分:故障的返回地址指向“肇事指令”,处理程序修复条件后重新执行该指令;陷阱的返回地址指向“下一条指令”,用于系统调用或调试;终止则没有有效返回地址,系统必须采取紧急措施。

2.2 异常处理流程

异常处理流程是硬件与软件协同工作的结果。当异常发生时,处理器硬件自动完成现场保存和跳转,随后操作系统内核接管进行具体处理。完整的异常处理流程包含以下五个阶段:

  1. 异常检测与触发:处理器在执行指令时检测到异常条件(如缺页、除零、系统调用指令)。
  2. 硬件现场保存:处理器自动将关键寄存器(如程序计数器 PC、状态寄存器 SR、栈指针 SP)压入内核栈,并切换至内核态。
  3. 异常向量查找:处理器根据异常号(Exception Number)从中断描述符表(IDT)或异常向量表中获取对应的处理程序入口地址。
  4. 控制权转移:处理器跳转到异常处理程序入口,开始执行内核代码。
  5. 异常处理与返回:内核处理程序执行具体逻辑(如分配物理页、执行系统调用服务),完成后执行 iret 或 eret 指令恢复现场,返回用户态。
故障排查要点:在分析异常处理流程时,重点关注“硬件现场保存”是否完整(尤其是错误码和异常号),以及“异常向量表”的配置是否正确。向量表地址错误或处理程序入口无效,会导致系统直接崩溃(双故障或三故障)。
异常处理流程示意图 异常处理流程(硬件 + 软件协同) ① 异常发生 ② 硬件现场保存 ③ 异常向量查找 ④ 控制权转移 ⑤ 异常处理与返回 各阶段关键动作 ① 指令执行检测到异常条件 ② PC、SR、SP 压入内核栈 切换至内核态 ③ 从 IDT 获取处理程序入口 ④ 跳转到处理程序地址 ⑤ 执行具体处理逻辑 执行 iret/eret 恢复现场 返回用户态继续执行

2.3 异常处理程序

异常处理程序是内核中负责响应特定异常事件的代码模块。每个异常号对应一个独立的处理程序入口,这些入口被组织在中断描述符表(IDT)或异常向量表中。处理程序的设计遵循“快速判断、分层处理”的原则。

2.3.1 处理程序的结构

一个典型的异常处理程序包含以下三个逻辑部分:

  • 前置处理(Prolog):保存通用寄存器、加载内核数据结构指针、获取异常号与错误码。
  • 核心处理(Handler):根据异常类型执行具体逻辑。例如,缺页异常调用 do_page_fault(),系统调用陷阱调用 sys_call_table[] 中的服务函数。
  • 后置处理(Epilog):恢复寄存器、检查是否需要调度、执行 iret 返回。
常见故障模式:处理程序未正确保存/恢复寄存器会导致内核栈破坏,引发“内核栈溢出”或“返回地址错误”。排查时,检查 SAVE_ALL 和 RESTORE_ALL 宏是否成对出现,以及栈指针是否在异常入口处正确切换。

2.3.2 处理程序的分发机制

为了简化代码结构,现代内核通常采用统一入口 + 分发表的模式。所有异常共享一个入口函数(如 entry_64.S 中的 common_interrupt),该函数保存现场后,根据异常号跳转到对应的 C 语言处理函数。

// 伪代码:异常分发逻辑
void exception_handler(struct pt_regs *regs, unsigned long error_code) {
    unsigned int vector = regs->vector;  // 获取异常号

    switch (vector) {
        case 0x0E:  // Page Fault
            do_page_fault(regs, error_code);
            break;
        case 0x80:  // System Call (x86)
            do_syscall_64(regs);
            break;
        case 0x06:  // Invalid Opcode
            do_invalid_op(regs, error_code);
            break;
        default:
            do_unexpected_exception(regs, error_code);
            break;
    }
}

2.3.3 处理程序与进程状态

异常处理程序运行在进程上下文(系统调用、缺页)或中断上下文(硬件中断)中。故障类异常(如缺页)通常可以睡眠等待 I/O,而终止类异常必须立即处理,不能阻塞。

异常类型 运行上下文 是否可睡眠 典型处理策略
故障(缺页) 进程上下文 是(可等待磁盘 I/O) 分配物理页、更新页表、重试指令
陷阱(系统调用) 进程上下文 是(可等待锁或 I/O) 执行系统调用服务、返回用户态
终止(机器检查) 中断上下文 否(不可阻塞) 记录错误日志、终止进程或触发 panic
故障排查技巧:当系统出现“unexpected kernel trap”或“Oops”信息时,重点关注异常号(vector)和错误码(error code)。例如,x86 架构下异常号 0x0E 表示缺页,错误码的 bit 0 指示是否由“页不存在”引起,bit 1 指示是读还是写操作。这些信息直接指向根因。
异常处理程序架构图 异常处理程序架构:统一入口 + 分发表 异常发生 统一入口(SAVE_ALL) 异常号分发(switch) 缺页异常处理 系统调用处理 非法指令处理 处理完成后执行 RESTORE_ALL + iret,返回异常发生前的执行流

2.4 异常处理中的关键数据结构

异常处理机制依赖以下核心数据结构,这些结构在故障排查中经常需要检查:

  • 中断描述符表(IDT):x86 架构下,IDT 包含 256 个门描述符,每个描述符指向一个处理程序入口。IDT 的基地址由 lidt 指令加载到 IDTR 寄存器。
  • 异常向量表:ARM/RISC-V 架构下,异常向量表是一个连续的跳转指令序列,每个异常号对应一个跳转目标。
  • 内核栈:每个进程拥有独立的内核栈,异常发生时硬件自动将寄存器压入当前进程的内核栈。内核栈溢出是常见的故障原因。
  • 进程控制块(PCB):异常处理程序可能需要修改进程状态(如发送信号、设置错误码),这些信息存储在 PCB 中。
排查建议:当系统频繁出现“double fault”或“triple fault”时,优先检查 IDT 是否被正确初始化,以及异常处理程序入口地址是否有效。使用调试器查看 idtr 寄存器的值和 IDT 表项的内容,是定位问题的第一步。