摘要:本文从源码层面剖析 QEMU 的 CPU 执行流程,围绕主循环调度、基本块缓存、TCG 动态翻译与主机代码执行四条主线展开。文章先介绍 TCG、CPUState、CPUClass 等核心概念,再依次分析 vCPU 线程入口、cpu_exec 主循环、基本块查找与翻译、TCG 翻译流程、主机代码执行以及异常与中断处理,帮助读者建立从顶层循环到底层翻译执行的完整认知。
1. 引言
QEMU 是一款功能强大的开源模拟器,支持多种 CPU 架构的模拟与虚拟化。理解 QEMU 的 CPU 执行流程,是深入掌握其整体架构的关键。本文将从源码层面剖析 QEMU 如何加载、翻译并执行客户机 CPU 指令,帮助读者建立从顶层循环到底层翻译执行的完整认知。
2. 核心概念与架构概览
在深入源码之前,先厘清几个核心概念:
- TCG(Tiny Code Generator):QEMU 的动态二进制翻译后端,将客户机指令翻译为主机指令。
- CPUState:描述单个 CPU 核心状态的统一结构体,包含寄存器、异常状态等。
- CPUClass:定义 CPU 的操作回调,如复位、执行、同步状态等。
- AccelState:加速器抽象层,TCG 与 KVM 均通过该接口接入。
QEMU 的 CPU 执行整体流程可概括为:主循环获取 CPU 状态,调用 CPUClass 的执行回调,进入 TCG 翻译与执行循环,最终完成指令的翻译、缓存与执行。
3. 主循环与 CPU 执行入口
QEMU 的主循环位于softmmu/cpus.c与cpus-common.c中。以 TCG 模式为例,每个 vCPU 线程运行qemu_tcg_cpu_thread_fn,其核心逻辑如下:
static void *qemu_tcg_cpu_thread_fn(void *arg) { CPUState *cpu = arg; int r; rcu_register_thread(); qemu_mutex_lock_iothread(); qemu_thread_get_self(cpu->thread); cpu->thread_id = qemu_get_thread_id(); cpu->can_do_io = 1; current_cpu = cpu; r = qemu_init_cpu_loop(cpu); if (r) { error_report("qemu_init_cpu_loop failed: %d", r); exit(1); } qemu_mutex_unlock_iothread(); /* wait for initial kick-off */ qemu_mutex_lock_iothread(); while (!cpu->stop) { qemu_cond_wait(cpu->halt_cond, &&iothread_mutex); } qemu_mutex_unlock_iothread(); /* start the cpu */ cpu_exec(cpu); /* terminate the cpu */ qemu_mutex_lock_iothread(); qemu_cpu_is_self(cpu); current_cpu = NULL; qemu_mutex_unlock_iothread(); rcu_unregister_thread(); return NULL; }线程启动后,主循环调用cpu_exec进入真正的执行阶段。该函数是 CPU 执行的核心入口,位于accel/tcg/cpu-exec.c。
4. cpu_exec 主循环分析
cpu_exec是 TCG 模式下 CPU 执行的主循环,其简化流程如下:
int cpu_exec(CPUState *cpu) { int ret; SyncClocks sc = { 0 }; /* 进入执行前同步时钟 */ sync_clocks(&sc); /* 初始化执行上下文 */ cpu_exec_enter(cpu); /* 主执行循环 */ while (!cpu->exit_request) { TranslationBlock *tb; uintptr_t last_tb = 0; int tb_exit = 0; /* 处理中断与异常 */ if (cpu->exception_index >= 0) { /* 处理异常,可能返回用户态或重新调度 */ ret = cpu_handle_exception(cpu, &ret); if (ret != 0) { break; } } /* 处理中断 */ if (cpu->interrupt_request & CPU_INTERRUPT_HARD) { cpu_handle_interrupt(cpu, &last_tb); } /* 查找或翻译基本块 */ tb = tb_find(cpu, last_tb, tb_exit); if (!tb) { /* 翻译失败或需要重新翻译 */ break; } /* 执行翻译后的代码 */ cpu_tb_exec(cpu, tb); /* 记录执行状态 */ last_tb = (uintptr_t)tb; tb_exit = cpu->tb_exit; } cpu_exec_exit(cpu); return ret; }主循环的核心步骤包括:异常处理、中断处理、基本块查找与执行。每次循环都会检查退出请求,确保外部事件(如调试器、停止请求)能及时响应。
5. 基本块查找与翻译
QEMU 以基本块(TranslationBlock,TB)为单位进行翻译与缓存。基本块通常以跳转指令或分支指令结尾。查找流程如下:
static TranslationBlock *tb_find(CPUState *cpu, uintptr_t last_tb, int tb_exit) { TranslationBlock *tb; CPUClass *cc = CPU_GET_CLASS(cpu); target_ulong cs_base, pc; uint32_t flags; /* 获取当前 PC 与状态标志 */ cc->get_pc_and_flags(cpu, &pc, &cs_base, &flags); /* 在哈希表中查找已翻译的基本块 */ tb = tb_lookup(cpu, pc, cs_base, flags); if (tb == NULL) { /* 未命中,进行翻译 */ tb = tb_gen_code(cpu, pc, cs_base, flags); } return tb; }翻译过程由tb_gen_code完成,其内部调用目标架构的翻译函数,将客户机指令逐条转换为 TCG 中间表示(IR),再由 TCG 后端优化并生成主机指令。
6. TCG 翻译流程
TCG 翻译的核心流程如下:
TranslationBlock *tb_gen_code(CPUState *cpu, target_ulong pc, target_ulong cs_base, uint32_t flags) { TranslationBlock *tb; TCGContext *tcg_ctx = &tcg_ctx; target_ulong next_tb; /* 分配新的 TB 并初始化 */ tb = tb_alloc(pc); if (unlikely(!tb)) { /* 缓存已满,刷新后重试 */ tb_flush(cpu); tb = tb_alloc(pc); } /* 初始化翻译上下文 */ tcg_ctx->tb = tb; tcg_ctx->cpu = cpu; tcg_ctx->gen_opc_ptr = tcg_ctx->gen_opc_buf; tcg_ctx->gen_opparam_ptr = tcg_ctx->gen_opparam_buf; /* 调用目标架构的翻译入口 */ cpu->cc->tcg_initialize(); next_tb = cpu->cc->tb_start(cpu, tb, cs_base, pc, flags); /* 生成 TCG IR */ cpu->cc->translate(cpu, tb, pc, cs_base, flags); /* 结束翻译,生成主机代码 */ tcg_gen_code(tcg_ctx, tb); /* 将 TB 插入哈希表 */ tb_add(tb); return tb; }其中translate回调由各架构实现,例如 x86 的translate.c、ARM 的translate.c。翻译器逐条解码客户机指令,生成对应的 TCG 操作。
7. 主机代码执行
翻译完成后,QEMU 通过cpu_tb_exec执行生成的主机代码:
static inline void cpu_tb_exec(CPUState *cpu, TranslationBlock *tb) { uintptr_t ret; /* 记录当前执行的 TB,用于调试与异常回溯 */ cpu->current_tb = tb; /* 执行翻译后的代码 */ ret = tcg_qemu_tb_exec(cpu, tb->tc.ptr); /* 根据返回值处理退出原因 */ switch (ret) { case TB_EXIT_REQUESTED: /* 外部请求退出 */ cpu->exit_request = 1; break; case TB_EXIT_ICOUNT_EXPIRED: /* 指令计数到期 */ cpu->icount_decr.u16.low = 0; break; default: /* 正常跳转,更新 PC */ break; } cpu->current_tb = NULL; }tcg_qemu_tb_exec是 TCG 生成的代码入口,执行完毕后返回退出原因,主循环据此决定继续执行还是处理事件。
8. 异常与中断处理
异常与中断是 CPU 执行的重要组成部分。QEMU 在每次循环中检查异常与中断请求:
static int cpu_handle_exception(CPUState *cpu, int *ret) { CPUClass *cc = CPU_GET_CLASS(cpu); int exception_index = cpu->exception_index; if (exception_index >= 0) { /* 调用架构相关的异常处理 */ if (cc->handle_cpu_exception) { *ret = cc->handle_cpu_exception(cpu, exception_index); } else { *ret = -1; } /* 异常已处理,清除标志 */ cpu->exception_index = -1; return 1; } return 0; }中断处理类似,通过检查cpu->interrupt_request标志位,调用架构相关的cpu_handle_interrupt回调。
9. 总结
QEMU 的 CPU 执行流程可以概括为以下几个关键环节:
- 主循环调度:每个 vCPU 线程运行
cpu_exec主循环,持续处理异常、中断与基本块执行。 - 基本块缓存:以 TB 为单位缓存翻译结果,通过哈希表加速查找。
- 动态翻译:TCG 将客户机指令翻译为 TCG IR,再优化生成主机指令。
- 执行与退出:执行主机代码后根据退出原因决定继续执行或处理外部事件。
理解这条主线后,再深入各架构的翻译实现、TCG 优化器以及 KVM 加速路径,就能逐步构建起对 QEMU 整体执行引擎的完整认识。