qemu-system 与 qemu-user 的区别:整机模拟与单进程转译怎么选
装好 QEMU 后,/usr/bin/ 下会躺着两类名字相近却分工完全不同的程序:一类是 qemu-system-riscv64、qemu-system-x86_64 这样的 qemu-system-*,另一类是 qemu-riscv64、qemu-aarch64 这样的 qemu-<架构>。很多人把它们统称为「QEMU 模拟器」,但二者模拟的对象根本不是一个层面的东西:system 态模拟的是一整台机器,user 态转译的是一个进程。理解这条分界线,是决定后续实验该写哪条命令、该准备内核镜像还是只丢一个 ELF 的前提。如果你对虚拟机、客户机这些词还没有画面,先读 认识 QEMU:从零看懂虚拟机、镜像与快照 建立基本图景。
本篇用同一份 RISC-V 程序做对照实验:user 态下 qemu-riscv64 直接把它当作主机上的普通进程跑完,system 态下 qemu-system-riscv64 启动一台 virt 虚拟整机,再通过 QMP 协议枚举到两个 RISC-V HART(硬件线程,Hart 是 RISC-V 对 CPU 线程的称呼),观察整机从暂停到关机的完整生命周期。user 态搭配 binfmt_misc 实现透明执行的深入机制,姊妹篇 QEMU user 态跨架构运行:x86 主机直接执行 RISC-V 程序 会单独展开。
本文要回答的问题
qemu-riscv64和qemu-system-riscv64名字只差一段,运行起来的进程模型有什么不同?- user 态没有内核、没有虚拟机,RISC-V 程序发起的
open、write这些系统调用由谁响应? - system 态启动后主机上为什么会多一个常驻 QEMU 进程,QMP 枚举到的 HART 是什么?
- 同样一份交叉编译产物,什么场景用 user 态一条命令搞定,什么场景必须老老实实装系统?
- 两种模式各自的能力边界在哪里,常见的误用有哪些?
一、一句话分界:转译一个进程,还是模拟一台机器
先把结论摆在前面。user 态(qemu-<架构>)是一个用户态指令转译器:它本身就是主机上的一个普通进程,加载一份外来架构的 ELF 文件,把外来指令块翻译成本机指令块执行,程序看到的内存、文件、网络全部是主机内核直接提供的,中间没有第二颗「CPU」、没有客户机内核、也没有设备模型。system 态(qemu-system-<架构>)则是一台完整机器的软件实现:它在一个常驻进程里造出 CPU(含若干 HART 或 vCPU)、物理内存、中断控制器、时钟、磁盘与网卡等一整套硬件,客户机操作系统内核跑在这套假硬件之上,自己管理进程调度、内存分页和驱动。
这张对比卡是全篇的骨架,读的时候抓住一个判断标准:被运行的程序是否需要一个操作系统来托底。只算个函数、打几行日志的单程序,user 态足够;要验证内核启动、驱动加载、多进程调度或整套发行版行为,system 态才是对应的工具。下面两节用同一份 RISC-V 程序的两次实测,把卡里每一行落到真实输出上。
二、user 态实测:没有整机,只有一个转译进程
实验程序是一个静态链接的 RISC-V 64 位 ELF,行为很简单:打印一行问候语,报告自己看到的架构,然后以退出码 0 结束。在 x86_64 主机上直接执行这份 ELF,主机会拒绝,因为指令集对不上;交给 qemu-riscv64 之后,它成为主机上一个普通进程,指令被逐块翻译执行。
实录第二行是程序自己的输出:hello from pid=14753 on arch=riscv64。这里有两个值得停下来看的细节。第一,进程号 14753 是主机内核分配的 PID——在主机上另开终端用 ps 就能看到这个进程,它的可执行文件其实是 qemu-riscv64,RISC-V 程序作为参数被加载进来。第二,程序自报架构 riscv64,说明转译器连 uname 这类带架构信息的查询都替客户程序伪造了返回值,让它相信自己真的跑在 RISC-V 机器上;而第三行的 0 是进程退出码,RISC-V 程序 main 返回 0,这个码原样透传给主机 shell。
程序结束的瞬间,转译进程也随之消失,系统里不留任何常驻 QEMU。这正是 user 态「命令即进程」模型的直接体现:没有虚拟机需要启动,也没有虚拟机需要关闭,它的生命周期与被转译的那个程序完全等长。交叉工具链编译完一个库、想立刻在 x86 构建机上跑它的测试集,靠的就是这种零启动成本的能力;binfmt_misc 还能把 qemu-riscv64 注册为外来 ELF 的解释器,此后连 qemu-riscv64 前缀都不用敲,机制细节见 QEMU user 态跨架构运行:x86 主机直接执行 RISC-V 程序。
三、system 态实测:常驻进程、两个 HART 与关机事件
换成 system 态,画面完全不同。这次启动的是 qemu-system-riscv64,参数指定 -machine virt 虚拟平台并用 -S 让整机启动后立刻暂停,控制通道交给 QMP(QEMU Machine Protocol,QEMU 的 JSON 机器管理协议)。于是主机上先出现一个常驻的 QEMU 进程,哪怕客户机一条指令都还没执行,它也已经占着 PID 活着。
这八行输出值得逐行读。第 6 行的 qemu 进程: 14757 ... qemu-system-ris 证明主机上多了一个 PID 14757 的常驻进程,与上一节「程序退出即消失」形成第一个对照。第 7 行是 QMP 握手返回的版本信息,QEMU 6.2.0;第 9 行查询运行状态,返回 "status": "prelaunch" 且 running: false——整机已经造好,但被 -S 冻结在执行第一条客户机指令之前,这是 system 态独有的「机器存在但未运行」状态,user 态没有任何对应物。
第 10 行是全篇最硬的证据。query-cpus-fast 枚举回两个 CPU 线程:thread-id 14761 与 14762,core-id 分别是 0 和 1,QOM 路径 /machine/soc0/harts[0] 与 /machine/soc0/harts[1] 明确标出这是片上系统里的两个 RISC-V HART,目标架构 riscv64。也就是说,QEMU 进程内部造出了两颗可供客户机内核调度的硬件线程,它们在主机上表现为 QEMU 进程的两个线程,但对客户机而言就是两块真实的 CPU 资源。最后通过 QMP 发 quit,第 11 行收到 SHUTDOWN 事件,原因 host-qmp-quit,第 12 行进程退出码为 0——整机走完了「创建、暂停、关机、回收」的完整生命周期,而这一切都发生在一个主机进程内部。
上面的终端实录上下两半分别对应两种模式。上半段里 RISC-V 程序报出主机分配的 PID 与 riscv64 架构后立刻退出,退出码 0,主机上没有残留进程;下半段 QMP 先回报 prelaunch 暂停态,随后 query-cpus-fast 列出 harts[0]、harts[1] 两个硬件线程及其线程号,quit 触发的 SHUTDOWN 事件与退出码 0 收尾。同一份 RISC-V 血统的负载,一边是进程级转译,一边是整机级模拟,边界一目了然。
QMP 的握手方式与 query-* 命令族在 QEMU 监视器入门:QMP/HMP 与 info 命令族 里有完整讲解,这里先记住一个用法差异:user 态没有监视器可连,因为它身后没有机器可查;能连上 QMP、能枚举 CPU,本身就是 system 态的身份标志。
四、运行边界对比:系统调用由谁响应
两种模式最容易被忽略、却最决定能力边界的差异,是系统调用的处理路径。任何程序除了执行指令,还要通过系统调用向操作系统要服务:读写文件、申请内存、建立网络连接。user 态里根本没有客户机内核,qemu-riscv64 不得不扮演「系统调用翻译官」:它拦截 RISC-V 程序发出的系统调用,把参数按 RISC-V 的调用约定取出,转换成主机 Linux 的系统调用号与参数布局,再请主机内核代劳,最后把结果按 RISC-V 约定塞回去。程序以为自己在和 RISC-V 内核对话,实际应答的是 x86 主机内核。
图中蓝色是 user 态路径,橙色与紫色是 system 态路径,灰色是两种模式最终都要落到的主机内核。读图时对比两条系统调用链的长度:user 态的程序经转换层直接找主机内核,中间没有第二个操作系统;system 态的程序先陷入客户机内核,由客户机内核的驱动去操作 QEMU 模拟的设备,QEMU 再以主机进程身份向主机内核申请真实资源,链路上有两个内核协同。
这条差异解释了三个实测现象。其一,user 态程序看到的文件系统就是主机的文件系统,路径、权限、环境变量全部沿用主机命名空间,所以它能直接读写主机当前目录;system 态客户机有独立的磁盘镜像与文件系统,与主机互相隔离。其二,user 态无法运行内核模块或驱动——主机不可能让一个普通进程去加载内核代码,这类负载只能进 system 态。其三,凡涉及客户机内核独有行为的实验,比如页表映射、中断处理、调度时延,user 态无从表达,因为那条系统调用被翻译后执行的是主机内核的策略,不是 RISC-V 内核的策略。
五、选型决策与常见误区
把前面的机理压成一张决策表,拿到任务时按「负载需要什么」对号入座即可。
| 判断维度 | 选 qemu-user | 选 qemu-system |
|---|---|---|
| 负载形态 | 单个用户态 ELF、测试集、构建产物 | 内核、固件、完整发行版、多进程系统 |
| 是否需要客户机内核 | 不需要,主机内核代答系统调用 | 需要,客户机内核管理整机 |
| 启动成本 | 毫秒级,命令即进程 | 需引导固件与内核,常驻进程 |
| 文件与网络视图 | 共享主机命名空间 | 独立磁盘与网络栈,需另行配置 |
| 调试对象 | 应用逻辑、跨架构兼容性 | 启动流程、驱动、中断、系统软件 |
| 典型命令 | qemu-riscv64 ./app | qemu-system-riscv64 -machine virt ... |
表里最实用的是第一行与第四行。持续集成里跑交叉编译后的单元测试,追求的是快和简单,user 态共享主机文件系统,测试二进制和输入数据都不用搬运;而验证一份补丁能否让 RISC-V 内核正常启动、驱动能否认出虚拟网卡,就必须接受 system 态的引导成本,因为这些代码只存在于整机环境里。
两个高频误区值得专门点破。第一个是「user 态能跑 RISC-V 程序,所以也能跑 RISC-V 发行版」。发行版的核心是内核与系统服务,user 态既没有内核也没有 PID 1 进程的概念,systemd、init 这类程序根本无处安放;跑发行版是 system 态的专职。第二个是反向误区:「既然 system 态什么都能模拟,那就一律用 system 态」。对单程序验证来说,整机引导要准备固件、内核、磁盘镜像,启动以秒计,还要处理登录与文件传输,而 user 态一条命令、零镜像、退出即净,杀鸡用牛刀的代价会直接拖慢构建流水线。此外还要注意,两个模式来自不同的软件包,主机上 qemu-riscv64 与 qemu-system-riscv64 是 /usr/bin/ 下两个相互独立的二进制,装了套件的一半并不等于另一半也可用。
实验环境
- 机器/系统:云主机 2C8G / Ubuntu 22.04.5
- 工具版本:QEMU 6.2.0(
qemu-riscv64与qemu-system-riscv64,QMP 握手回报 major 6 / minor 2 / micro 0) - 日期:2026-11-08
- 完整命令序列:先以
qemu-riscv64执行静态链接的 RISC-V 64 位 ELF 并记录退出码;再以-machine virt -smp 2 -S启动qemu-system-riscv64,经 Unix 套接字连接 QMP,依次执行query-status、query-cpus-fast,最后发quit并记录 SHUTDOWN 事件与进程退出码;末尾自省两个模式二进制的安装路径。输出即本篇引用的完整实录。
代码获取:本篇实验的可复现脚本见公开仓库 derekzhuo/geek-doc.cn 的
experiments/qemu-system-vs-user-mode目录。RISC-V 静态 ELF 的编译方式见同仓experiments/qemu-user-cross-arch-binary/,克隆后进入目录执行./run.sh,终端会依次打印 user 态转译结果与 system 态 QMP 枚举、关机全过程,与本篇实录逐条对应。
这一篇在知识体系里的位置
本篇是 SoC 与 QEMU 入门路线的模式分界篇,回答「该用哪类 QEMU 命令」这个一切实操之前的问题。确立 user 态方向后,单进程跨架构执行与 binfmt_misc 透明注册的完整做法在 QEMU user 态跨架构运行:x86 主机直接执行 RISC-V 程序;确立 system 态方向后,虚拟机、镜像与快照的基本概念在 认识 QEMU:从零看懂虚拟机、镜像与快照,运行时管控整机的 QMP/HMP 命令体系在 QEMU 监视器入门:QMP/HMP 与 info 命令族。再往后,整机的网络接入、串口控制台接线与 GDB 调试,都是 system 态这条支线上的专题。
总结
qemu-system 与 qemu-user 的区别可以浓缩成一句话:前者在一个常驻进程里模拟带 HART、内存与设备的完整机器,客户机内核跑在假硬件上,适合系统软件与完整发行版;后者只是主机上的普通转译进程,把单个外来架构 ELF 的指令翻译执行、系统调用转换后转交主机内核,程序退出它就消失,适合跨架构构建与单程序验证。实测中 user 态的 RISC-V 程序拿到主机 PID、报出 riscv64 后以退出码 0 干净结束,system 态则经 QMP 枚举到 harts[0]、harts[1] 两个硬件线程,走完 prelaunch 到 SHUTDOWN 的整机生命周期。选型时先问负载是否需要客户机内核:需要就上 system 态,不需要就用 user 态换零镜像、毫秒级启动的轻便。