流水线异常 — Precise Exceptions in Pipeline

精确异常:流水线中异常的严格语义保证

📖 精确异常 vs 非精确异常

精确异常(Precise Exception)是处理器设计的硬性要求:当异常发生时,异常指令之前的所有指令完整执行,异常指令及之后的所有指令仿佛从未执行

精确异常示意: 指令序列: I1 I2 I3(异常) I4 I5 精确异常保证: ┌─────────────────────────────────────────┐ │ I1, I2: 已完成,结果已提交 │ │ I3: 触发异常,结果丢弃 │ │ I4, I5: 从未执行,流水线已冲刷 │ │ │ │ 保存: mepc = I3的PC │ │ 跳转: mtvec → 异常处理程序 │ └─────────────────────────────────────────┘ 非精确异常(不允许在通用CPU中): I1: 可能还没完成 I2: 可能已经部分修改了状态 I3: 异常了,但I4的副作用已经发生 → 软件无法正确恢复!
异常类型RISC-V mcause产生阶段检测难度
非法指令2ID
断点3ID
ecall8-11EX
地址不对齐4/6MEM
访问故障5/7MEM
外部中断0x8000000x任意
非精确异常在某些特殊场景(如浮点异常、机器检查)中允许,但通用处理器必须实现精确异常。没有精确异常,操作系统无法正确实现虚拟内存(Page Fault)、系统调用和信号处理。

🔬 流水线中的异常优先级

流水线中多个阶段可能同时产生异常,必须按程序序处理——最早的指令异常优先:

异常优先级 (程序序): IF ID EX MEM WB Illegal ecall Access Breakpoint Fault 优先级: MEM > EX > ID > IF (MEM阶段的指令比ID阶段的指令更早进入流水线) 处理流程: 1. 检测到异常后,设置trap信号 2. 保存mepc = 异常指令的PC 3. 保存mcause = 异常原因 4. 冲刷流水线 (所有阶段清零) 5. PC跳转到mtvec ┌─────────────────────────────────────────┐ │ 关键: 冲刷必须在同一周期完成 │ │ 否则后续指令可能已经修改了架构状态 │ └─────────────────────────────────────────┘

🖥️ Verilog实现:流水线异常处理

// Lesson 35: Pipeline Exception Handling (Precise Exceptions)
module exception_pipeline #(
    parameter DATA_W = 32
)(
    input  wire              clk, rst_n,
    input  wire [DATA_W-1:0] pc_id_i, pc_ex_i, pc_mem_i,
    input  wire              valid_id_i, valid_ex_i, valid_mem_i,
    input  wire              is_store_id_i,
    input  wire [6:0]        opcode_id_i, [2:0] funct3_id_i,
    input  wire              exc_undef_i,     // 非法指令
    input  wire              exc_ecall_i,     // ecall
    input  wire              exc_misalign_i,  // 地址不对齐
    input  wire              exc_load_fault_i,  // Load故障
    input  wire              exc_store_fault_i, // Store故障
    input  wire              irq_ext_i,       // 外部中断
    input  wire [DATA_W-1:0] mtvec_i,
    output reg  [DATA_W-1:0] mepc_o,
    output reg  [DATA_W-1:0] mcause_o,
    output reg               trap_o,
    output reg               flush_all_o,
    output reg  [DATA_W-1:0] trap_pc_o
);
    // 优先级: MEM > EX > ID > 中断
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            mepc_o <= 0; mcause_o <= 0;
            trap_o <= 0; flush_all_o <= 0;
        end else begin
            trap_o <= 0; flush_all_o <= 0;
            // MEM阶段异常 (最高优先级)
            if (valid_mem_i && exc_store_fault_i) begin
                mepc_o <= pc_mem_i; mcause_o <= 32'd7;
                trap_o <= 1; flush_all_o <= 1;
                trap_pc_o <= mtvec_i;
            end else if (valid_mem_i && exc_load_fault_i) begin
                mepc_o <= pc_mem_i; mcause_o <= 32'd5;
                trap_o <= 1; flush_all_o <= 1;
                trap_pc_o <= mtvec_i;
            end
            // EX阶段异常
            else if (valid_ex_i && exc_ecall_i) begin
                mepc_o <= pc_ex_i; mcause_o <= 32'd11;
                trap_o <= 1; flush_all_o <= 1;
                trap_pc_o <= mtvec_i;
            end
            // ID阶段异常
            else if (valid_id_i && exc_undef_i) begin
                mepc_o <= pc_id_i; mcause_o <= 32'd2;
                trap_o <= 1; flush_all_o <= 1;
                trap_pc_o <= mtvec_i;
            end
            // 外部中断
            else if (irq_ext_i) begin
                mepc_o <= pc_id_i;
                mcause_o <= 32'h8000000B;
                trap_o <= 1; flush_all_o <= 1;
                trap_pc_o <= mtvec_i;
            end
        end
    end
endmodule
Verilator仿真验证通过 — 精确异常处理正确,mcause/mepc/mtvec跳转正确

代码解析

📊 异常处理在乱序处理器中的挑战

在顺序流水线中,精确异常相对容易实现(冲刷流水线即可)。但在乱序处理器中,指令可能已经执行但还没提交

架构精确异常方案复杂度
顺序流水线冲刷流水线
顺序+重排缓冲ROB按序提交,异常时清空ROB
乱序+ROB异常指令到ROB头时才触发,之前指令已提交
乱序+Checkpoint每条指令保存检查点,异常时回滚很高
ROB(重排序缓冲)是实现精确异常的关键:即使指令乱序执行,也按程序序提交。异常指令到达ROB头部时,之前的指令都已提交,之后的指令都在ROB中可被清空。

🧪 实验练习

  1. 实现Page Fault处理:添加12/13/15号异常,保存mtval寄存器
  2. 实现异常嵌套:异常处理程序中再发生异常时的处理
  3. 添加S-mode异常委托:通过medeleg将部分异常委托给S-mode
  4. 实现中断抢占:高优先级中断可以抢占低优先级异常处理
精确异常处理
思考题:为什么异常优先级是"MEM > EX > ID"而不是"ID > EX > MEM"?如果先处理ID阶段的非法指令,会发生什么问题?
参考资料:RISC-V Privileged Spec §3.1 | Hennessy & Patterson §3.6 | BOOM Exception Handling