QEMU 中断与异常:TCG 执行循环里的处理流程¶
主要贡献者
- 作者:@zevorn
本文以 QEMU system mode + TCG 为背景,梳理“模拟 CPU 的中断/异常”处理链路。我们关注两个问题:中断/异常如何进入 CPU 执行循环?架构相关的处理逻辑在哪里落地?
一句话总结:QEMU 会在 TB 边界检查中断和异常,并把真正的处理交给目标架构的回调。
概览
- 中断/异常/退出请求的来源与状态表示
- TB 边界的执行检查点与主循环路径
- setjmp/longjmp 的异常返回机制
- 设备到 vCPU 的中断注入与延迟处理
- 架构相关处理钩子与 RISC-V 例子
中断、异常、退出请求¶
在 QEMU 里,vCPU 会因为这三类事件而回到 host 世界:
- 异常(exception):同步事件,通常由当前指令触发(非法指令、TLB miss、系统调用等)。以
CPUState::exception_index表示。 - 中断(interrupt):异步事件,通常来自设备、定时器或外部控制。以
CPUState::interrupt_request的比特位表示。 - 退出请求(exit):不是传统意义的中断/异常,更多用于让执行循环“停下来重新评估状态”。例如
CPU_INTERRUPT_EXITTB。
CPU_INTERRUPT_* 的定义集中在 include/exec/cpu-interrupt.h,其中硬件中断与“退出 TB”请求最常见:
/* include/exec/cpu-interrupt.h */
#define CPU_INTERRUPT_HARD 0x0002
#define CPU_INTERRUPT_EXITTB 0x0004
#define CPU_INTERRUPT_HALT 0x0020
#define CPU_INTERRUPT_RESET 0x0400
理解这两类状态很重要:异常用 exception_index,中断用 interrupt_request,而它们最终都会在执行循环的固定检查点被处理。
主循环:双层 while 的“检查点”¶
TCG 执行循环在 accel/tcg/cpu-exec.c,核心是双层 while:外层处理异常,内层处理中断并执行 TB。
/* accel/tcg/cpu-exec.c */
while (!cpu_handle_exception(cpu, &ret)) {
TranslationBlock *last_tb = NULL;
while (!cpu_handle_interrupt(cpu, &last_tb)) {
TranslationBlock *tb;
TCGTBCPUState s = cpu->cc->tcg_ops->get_tb_cpu_state(cpu);
tb = tb_lookup(cpu, s);
if (tb == NULL) {
tb = tb_gen_code(cpu, s);
}
cpu_loop_exec_tb(cpu, tb, s.pc, &last_tb, &tb_exit);
}
}
可以把它理解成:
- 外层 while:如果
exception_index已经被设置(同步异常),先处理异常再继续。 - 内层 while:在执行 TB 之前/之后检查
interrupt_request,必要时把控制权交给中断处理逻辑。
这也解释了“中断通常在 TB 边界被处理”的现象:处于性能考虑,QEMU 会在循环检查点处理它,而不是在 TB 内部任意位置打断。
异常返回点:setjmp/longjmp¶
异常通常由 vCPU 自身在执行指令时触发(例如非法指令、访存异常或调试事件)。为了让 vCPU 及时回到“可控的主循环”,QEMU 在进入 cpu_exec_loop() 之前设置异常返回点,并在需要退出 TB 时用 siglongjmp() 直接跳回该位置。
设置返回点的逻辑在 cpu_exec_setjmp() 中:
/* accel/tcg/cpu-exec.c */
static int cpu_exec_setjmp(CPUState *cpu, SyncClocks *sc)
{
/* Prepare setjmp context for exception handling. */
if (unlikely(sigsetjmp(cpu->jmp_env, 0) != 0)) {
cpu_exec_longjmp_cleanup(cpu);
}
return cpu_exec_loop(cpu, sc);
}
而“跳回返回点”的入口是 cpu_loop_exit(),它会清理状态并直接 siglongjmp():
/* accel/tcg/cpu-exec-common.c */
void cpu_loop_exit(CPUState *cpu)
{
/* Undo the setting in cpu_tb_exec. */
cpu->neg.can_do_io = true;
/* Undo any setting in generated code. */
qemu_plugin_disable_mem_helpers(cpu);
siglongjmp(cpu->jmp_env, 1);
}
因此,当 vCPU 触发异常或需要“强制退出 TB”时,相关路径会调用 cpu_loop_exit(),把控制权带回 cpu_exec_loop() 的安全检查点,再由 exception_index 和中断标志进行后续处理。
中断注入路径:从设备到 vCPU¶
中断进入 vCPU 的入口之一是 tcg_handle_interrupt。它会设置 interrupt_request 的位,并唤醒 vCPU:
/* accel/tcg/tcg-accel-ops.c */
void tcg_handle_interrupt(CPUState *cpu, int mask)
{
cpu_set_interrupt(cpu, mask);
if (!qemu_cpu_is_self(cpu)) {
qemu_cpu_kick(cpu);
} else {
qatomic_set(&cpu->neg.icount_decr.u16.high, -1);
}
}
流程上可以理解为:
- 设备或定时器触发中断,请求设置
CPU_INTERRUPT_*位。 - 若 vCPU 在别的线程,
qemu_cpu_kick()促使其从 TB 中退出。 - vCPU 回到
cpu_exec_loop,在内层cpu_handle_interrupt()中消费中断。
中断处理路径:可延迟处理¶
当 interrupt_request 非空时,cpu_handle_interrupt() 会转给架构回调 cpu_exec_interrupt():
/* accel/tcg/cpu-exec.c */
if (tcg_ops->cpu_exec_interrupt(cpu, interrupt_request)) {
if (unlikely(cpu->singlestep_enabled)) {
cpu->exception_index = EXCP_DEBUG;
return true;
}
cpu->exception_index = -1;
*last_tb = NULL;
}
关键点:
- 是否真正触发中断由目标架构决定(
cpu_exec_interrupt()返回值)。 - 处理完后通常需要清理
exception_index,并让 TB 链接失效(last_tb = NULL)。 CPU_INTERRUPT_EXITTB会强制退出 TB,但不一定代表“真正的外部中断”。
异常处理路径:需要立刻处理¶
异常通常需要立刻处理,由 exception_index 驱动,核心处理逻辑在 cpu_handle_exception():
/* accel/tcg/cpu-exec.c */
if (replay_exception()) {
const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops;
bql_lock();
tcg_ops->do_interrupt(cpu);
bql_unlock();
cpu->exception_index = -1;
}
这里的 do_interrupt() 才是真正的异常处理入口,由目标架构实现。例如非法指令、系统调用、访存异常都会进入这里。
异常/中断处理的钩子函数¶
TCG 将“通用循环”和“架构细节”分开,关键钩子定义在 include/accel/tcg/cpu-ops.h:
/* include/accel/tcg/cpu-ops.h */
void (*do_interrupt)(CPUState *cpu);
bool (*cpu_exec_interrupt)(CPUState *cpu, int interrupt_request);
do_interrupt():处理同步异常(由exception_index指示)。cpu_exec_interrupt():处理异步中断(由interrupt_request指示)。
这也是为什么不同架构的中断/异常行为差异很大:真正的语义由 target 目录下的实现决定。
RISC-V 的异常与中断处理¶
RISC-V 的实现位于 target/riscv/cpu_helper.c。中断是否被接受由 riscv_cpu_exec_interrupt() 决定,它会将中断号编码进 exception_index 并直接调用 riscv_cpu_do_interrupt():
/* target/riscv/cpu_helper.c */
if (interrupt_request & mask) {
int interruptno = riscv_cpu_local_irq_pending(env);
if (interruptno >= 0) {
cs->exception_index = RISCV_EXCP_INT_FLAG | interruptno;
riscv_cpu_do_interrupt(cs);
return true;
}
}
RISC-V 用 RISCV_EXCP_INT_FLAG 表示“这是异步中断”,exception_index 的低位则是具体 cause。真正的陷入处理在 riscv_cpu_do_interrupt() 中完成,下面这段展示了陷入 S-mode 时的寄存器保存与向量跳转:
/* target/riscv/cpu_helper.c */
env->scause = cause | ((target_ulong)async << (sxlen - 1));
env->sepc = env->pc;
env->stval = tval;
env->htval = htval;
env->htinst = tinst;
env->pc = (env->stvec >> 2 << 2) +
((async && (env->stvec & 3) == 1) ? cause * 4 : 0);
riscv_cpu_set_mode(env, PRV_S, virt);
它的行为与规范一致:保存 EPC/CAUSE/TVAl,按 stvec 的 direct/vectored 模式计算新 PC。若陷入未委派或在 M-mode 处理,则逻辑会切换为 mcause/mepc/mtvec 路径,整体结构保持相同。
章节小结¶
- 异常是同步的,靠
exception_index驱动,进入do_interrupt()。 - 中断是异步的,靠
interrupt_request驱动,进入cpu_exec_interrupt()。 - 处理时机多在 TB 边界:中断/异常先让 TB 退出,再由主循环处理。
- 架构差异决定最终行为,QEMU 只提供统一框架与调用时机。