QEMU 虚拟机启动与 vCPU 初始化
一、从创建进程到执行第一条指令
QEMU 根据启动参数创建一台完整的虚拟计算机:CPU 与内存提供执行环境,磁盘提供系统镜像,网卡连接外部网络,芯片组、总线、中断控制器和固件把这些部件组织起来。硬件模型完成创建与复位后,客户机 CPU 开始执行固件,再由固件装载操作系统。
本文以 x86 的 pc-q35-10.2、KVM 加速、普通 RAM、virtio 磁盘与网卡为例,沿“参数与后端 → 机器与设备 → 复位 → 固件与系统引导”说明 BIOS 冷启动。重点是各层的依赖和初始化顺序,后续执行循环见KVM vCPU 执行与 I/O 退出。
源码版本:QEMU 10.2.0。
二、包含系统盘与网卡的启动示例
下面的命令创建 2 个 vCPU、4 GiB 内存、一块 virtio 系统盘和一张 virtio 网卡,通过 -S 暂停在客户机执行之前。/path/demo.qcow2 需替换为已有的、支持 BIOS 引导且安装了 virtio 驱动的系统镜像;创建一个空镜像不会自动安装操作系统。
qemu-system-x86_64 \
-machine pc-q35-10.2,accel=kvm \
-cpu host -smp 2 -m 4G \
-nodefaults -display none -S \
-blockdev driver=file,node-name=os-file,filename=/path/demo.qcow2 \
-blockdev driver=qcow2,node-name=os-disk,file=os-file \
-device virtio-blk-pci,id=disk0,drive=os-disk,bootindex=1 \
-netdev user,id=net0 \
-device virtio-net-pci,id=nic0,netdev=net0 \
-serial stdio \
-qmp unix:/tmp/virt-startup-qmp.sock,server=on,wait=off
-machine 选择芯片组、总线和兼容属性;-cpu 决定客户机可见的处理器能力;-smp 描述启动 CPU 数量与拓扑。-m 配置的是客户机 RAM 容量,既不表示所有宿主物理页已分配,也不表示 EPT 已全部建好。-nodefaults 省去默认外设,不会取消 Q35 的核心芯片组和 CPU。
磁盘参数把文件后端 os-file、qcow2 格式节点 os-disk 和客户机可见的 virtio-blk-pci 分开声明。网卡参数把用户态网络后端 net0 与 virtio-net-pci 相连;该后端要求 QEMU 构建包含相应支持,无需手动建立 TAP 或宿主桥。Libvirt 的桥接网络通常使用 TAP 后端,客户机端仍可使用相同的 virtio 网卡。
-display none 关闭图形显示,-serial stdio 将串口连接到当前终端;要看到客户机日志,还需客户机配置串口控制台。显示输出与启动状态彼此独立,终端没有内容不等于客户机没有运行。
连接 QMP 后,先完成能力协商,再查询或继续执行:
{"execute":"qmp_capabilities"}
{"execute":"query-status"}
{"execute":"query-cpus-fast"}
{"execute":"query-block"}
{"execute":"cont"}
正常冷启动的 -S 配置在 cont 前处于 prelaunch,running 为 false。此时 query-cpus-fast 已能返回 vCPU 的宿主线程信息。QMP 的 RESUME 事件表示开始恢复执行,不代表固件初始化、磁盘引导或客户机服务已经完成。
这个例子把后面的调用链分为两段:qemu_init → machine init → CPU / RAM / 设备就绪 → 复位 建立可运行的机器;cont → vm_start → KVM_RUN 才允许客户机执行。下面先按这个顺序跟踪启动,再用对象表对照源码。
三、创建机器、KVM 虚拟机与板级设备
main() 进入 qemu_init(),解析参数并创建 MachineState。随后早期后端初始化打开块图和字符设备;机器属性设置完成后才配置 accelerator。KVM 初始化需要读取 irqchip 等机器属性,所以它必须在这些属性确定之后运行。
Mermaid 源码
flowchart LR
accTitle: 从后端创建到系统盘引导的初始化顺序
accDescr: QEMU 先创建 MachineState,在早期后端阶段打开文件与 qcow2 块图,再配置 KVM,晚期后端阶段建立 user 或 TAP 网络后端。Q35 板级初始化创建暂停的 CPU 线程、RAM、固件与总线;命令行 virtio-blk 和 virtio-net 设备连接已有后端。整机复位后由 cont 启动客户机,固件从系统盘引导操作系统,再初始化磁盘和网络驱动。
subgraph PREP["一、配置、后端与 KVM"]
direction TB
A["qemu_init:解析参数<br/>初始化主循环基础设施"] --> B["创建 MachineState<br/>选定机器类型与默认设备配置"]
B --> EB["早期后端<br/>configure_blockdev<br/>文件 os-file → qcow2 os-disk"]
EB --> MP["应用机器属性<br/>随后配置 accelerator"]
MP --> C["初始化 KVM<br/>VM、中断控制器与内存监听器"]
C --> D["晚期后端:net_init_clients<br/>net0、CPU 型号与 NUMA"]
end
subgraph BOARD["二、板级布局与设备前端"]
direction TB
E["machine_run_board_init<br/>准备 RAM 后端"]
F["计算 RAM 布局<br/>x86_cpus_init:创建暂停的 vCPU 线程"]
G["pc_memory_init:映射 RAM 与固件<br/>监听器登记 KVM 内存槽"]
H["创建 PCIe 根总线、ICH9 等板级设备"]
I["qemu_create_cli_devices<br/>virtio-blk → os-disk<br/>virtio-net → net0"]
E --> F --> G --> H --> I
end
subgraph RUN["三、复位、执行与客户机引导"]
direction TB
J["同步 CPU 状态、初始化完成通知<br/>登记 ROM、整机复位"]
J --> L["本例 -S:prelaunch<br/>QMP 查询 CPU / 磁盘,随后 cont"]
L --> N["vm_start / resume_all_vcpus<br/>BSP 执行 BIOS;AP 等待启动事件"]
N --> FW["固件枚举 PCI 设备<br/>从系统盘装载引导程序与内核"]
FW --> OS["客户机初始化磁盘与网络驱动<br/>挂载文件系统、配置网络、启动服务"]
end
PREP --> BOARD --> RUN
classDef setup fill:#e4f1ed,stroke:#78a399,color:#163c38;
classDef board fill:#edf3fa,stroke:#7897b3,color:#243d53;
classDef run fill:#fff4db,stroke:#b89a55,color:#59491e;
class A,B,EB,MP,C,D,E setup;
class F,G,H,I,J board;
class L,N,FW,OS run;
kvm_init() 打开 /dev/kvm,检查 API 版本与能力,调用 KVM_CREATE_VM 获取 vmfd,初始化架构相关功能与中断控制器,并注册内存监听器。此时已经有 KVM 虚拟机容器,客户机 RAM 和板级设备尚未全部挂入地址空间。
后端初始化分成早、晚两组。qemu_create_early_backends() 中的 configure_blockdev() 建立本例的文件与 qcow2 节点;KVM 配置完成后,qemu_create_late_backends() 中的 net_init_clients() 创建 net0。块后端需要尽早可用,供机器属性引用;网络后端则在后续阶段建立。两者都先于依赖它们的 virtio 设备前端。早期与晚期后端
随后解析 CPU 型号与 NUMA 配置,machine_run_board_init() 准备 RAM 后端并调用机器类的 init。对于本例,pc_q35_init() 先计算低端和高端 RAM 容量,再调用 x86_cpus_init() 创建 CPU,然后通过 pc_memory_init() 布置内存、加载固件,继续创建 PCIe 根总线、ICH9、中断控制器等板级设备。
在示例的默认 Q35 布局中,4 GiB RAM 的后端被切成两个别名:前 2 GiB 映射在 4 GiB 以下,后 2 GiB 从 4 GiB 以上开始映射,为 PCI 等区域留出地址空间。低端地址中还会叠加固件和传统设备窗口,所以客户机物理地址空间不能简单理解为从 0 连续铺满 4 GiB。后备创建、RAMBlock、别名挂接与 KVM 注册的完整过程见QEMU 内存初始化与地址空间管理,其中使用 8 GiB RAM 展开地址换算。
内存区域加入 AddressSpace 后,KVM 的监听回调把合适的 RAM 区域转成内存槽,登记 GPA 范围与 QEMU 用户态地址。这个步骤提供地址转换的基础,EPT 的实际叶子映射通常在客户机访问时逐步建立;详见KVM 内存映射与 EPT 缺页处理。
板级初始化返回后,qemu_create_cli_devices() 才处理命令行中的 -device。virtio-blk 连接 os-disk,virtio-net 连接 net0,它们作为 PCI 设备接入已存在的总线;realize 过程建立设备状态、配置空间和中断资源。客户机后续通过 PCI 枚举发现它们,再由驱动协商 virtio 特性、建立 virtqueue。QEMU 创建完成与客户机驱动初始化属于两个阶段。板级与命令行设备创建
按上述顺序,vCPU 线程在板级初始化中较早建立,内存布局与其他设备随后才完成。线程保持 stopped,仍可响应寄存器同步等内部工作。QEMU 用运行状态、线程条件变量和 BQL 协调初始化,使客户机在整机准备完成后才开始执行。下面展开 x86_cpus_init() 中的线程创建握手,再回到整机复位与放行执行的节点。
四、vCPU 线程与内核接口的建立
x86_cpus_init() 按拓扑计算 APIC ID,为每个启动 CPU 创建 X86CPU 并 realize。x86_cpu_realizefn() 验证 CPU 特性,准备 APIC,进入 qemu_init_vcpu()。该函数先设 cpu->stopped = true,再通过 accelerator 的 create_vcpu_thread 回调创建线程。
Mermaid 源码
sequenceDiagram
accTitle: vCPU 创建握手与首次执行
accDescr: 主线程创建 CPU 对象并等待 vCPU 线程建立内核接口。线程完成 KVM_CREATE_VCPU 与 kvm_run 映射后报告 created,保持 stopped。机器初始化、复位和寄存器同步完成,cont 才允许执行;BSP 执行固件,AP 等待 INIT 和 SIPI。
participant Q as QEMU 主线程
participant V as vCPU 线程
participant K as KVM 内核接口
participant G as 客户机
Q->>Q: x86_cpu_new / qdev_realize
Q->>Q: qemu_init_vcpu:stopped = true
Q->>V: 创建线程
Note over Q,V: 主线程等待 created 时释放 BQL
V->>K: KVM_CREATE_VCPU(APIC ID)
K-->>V: vCPU fd
V->>K: 查询共享区大小,mmap(vCPU fd)
K-->>V: kvm_run 共享区
V->>V: kvm_arch_init_vcpu
V-->>Q: created = true,唤醒主线程
Note over V,G: 线程已建立,客户机尚未执行
Q->>Q: 完成 RAM 布局、板级与附加设备
Q->>V: 初始化同步与整机复位后的同步
V->>K: 写入寄存器与多处理器状态
Note over Q,V: -S:保持 prelaunch,等待管理侧 cont
Q->>Q: qmp_cont → vm_start
Q->>V: 运行状态更新,stopped = false,kick
V->>K: cpu_can_run → kvm_cpu_exec → KVM_RUN
alt BSP:RUNNABLE
K->>G: 从复位取指地址 0xfffffff0 执行
else AP:UNINITIALIZED
Note over K,G: 等待客户机 INIT / SIPI 启动序列
K->>G: 收到启动事件后从指定入口执行
end
KVM 线程进入 kvm_vcpu_thread_fn(),获取 BQL,在自己的线程上下文中调用 kvm_init_vcpu():创建内核 vCPU,查询共享区长度,mmap 得到 kvm_run,再完成架构相关初始化。之后 cpu_thread_signal_created() 设置 created 并唤醒等待中的主线程。
/dev/kvm 的 fd
├─ KVM_CREATE_VM → KVMState.vmfd
└─ KVM_GET_VCPU_MMAP_SIZE → 共享区长度
vmfd
├─ KVM_CREATE_VCPU(APIC ID 0) → CPU 0 的 kvm_fd
└─ KVM_CREATE_VCPU(APIC ID 1) → CPU 1 的 kvm_fd
每个 kvm_fd
├─ mmap → 各自的 kvm_run
└─ KVM_RUN → 运行或处理该 vCPU 的执行请求
默认的两核拓扑下,APIC ID 为 0 和 1;其他拓扑可以产生不连续的 APIC ID,不能总把它当成 cpu_index。两个 vCPU 共享一个 KVM 虚拟机的内存槽与机器资源,各自拥有独立 kvm_run 和寄存器状态。
线程的创建握手不包含客户机第一条指令。主线程等待 created 时会释放 BQL,使新线程可以完成初始化;它收到通知后继续设备创建。vCPU 线程通过 cpu_can_run() 检查 stopped 和整机运行状态,只在允许执行时进入 kvm_cpu_exec()。
五、复位、继续执行与固件入口
设备创建完成后,qdev_machine_creation_done() 同步 CPU 初始化状态,推进到 MACHINE_READY,发送机器初始化完成通知,登记 ROM 复位处理,再执行整机复位。x86 CPU 的复位回调建立起始寄存器和启动状态,后续通过 KVM 的寄存器同步接口写入内核 vCPU。
普通 BIOS 冷启动时,启动处理器 BSP 的 CS selector 为 0xf000,CS 隐藏基址为 0xffff0000,EIP 为 0xfff0。复位取指使用隐藏基址加偏移:0xffff0000 + 0xfff0 = 0xfffffff0。这是复位时的特定段状态;固件从这里跳转并完成平台初始化。
| 时刻 | CPU 0:BSP | CPU 1:AP |
|---|---|---|
| 机器创建完成,-S 生效 | 线程与 KVM vCPU 已存在,stopped = true | 线程与 KVM vCPU 已存在,stopped = true |
| 复位后的多处理器状态 | KVM_MP_STATE_RUNNABLE | 内核 irqchip 路径下为 KVM_MP_STATE_UNINITIALIZED |
| 执行 cont | QEMU 解除 stopped,允许进入 KVM_RUN | QEMU 同样解除 stopped,但 AP 尚不能直接执行 BSP 的固件路径 |
| 客户机开始执行 | 从复位向量取指 | 等待客户机的 INIT / SIPI 启动序列,随后从指定入口执行 |
qmp_cont() 完成状态检查后调用 vm_start,后者更新运行状态、通知后端、恢复虚拟时钟并唤醒 vCPU。KVM_RUN 最终让 BSP 进入客户机。AP 的宿主线程也可以进入内核等待启动事件,因此看到两个线程或两次 ioctl 入口,不等于两个客户机 CPU 都在执行固件指令。
不指定 -S 时,普通冷启动会在初始化结束路径中自动调用 cont。-incoming 则先恢复迁移状态,-loadvm 先恢复快照;这些路径的第一条客户机指令可能是恢复点,而非复位向量。直接加载内核也会改变引导准备过程,需要另外跟踪所选机器的内核装载代码。
从固件到磁盘与网络驱动
本例中,BIOS 固件从复位向量开始执行,初始化平台并枚举 PCI 设备。bootindex=1 将 virtio 磁盘列入启动顺序,固件通过自己的 virtio-blk 驱动读取引导数据,再将控制权交给磁盘上的引导程序与操作系统内核。此时请求已经通过 virtio-blk、块后端和 qcow2/file 节点访问系统镜像。
操作系统接管后,加载自己的 virtio-blk 与 virtio-net 驱动,协商设备特性并配置队列。系统盘用于读取根文件系统;网卡完成接口与地址配置后才开始提供网络通信。QEMU 已创建网卡、客户机已识别网卡、客户机已取得 IP 和业务已可访问,是四个不同的检查点。常见启动故障也据此分层:镜像打不开会阻止 QEMU 初始化,固件找不到引导程序会停在引导阶段,缺少磁盘驱动可能使内核无法挂载根文件系统,网络配置失败则通常不会阻止 CPU 与操作系统运行。
六、机器、执行后端与处理器对象
| 对象 | 关键字段 | 职责 |
|---|---|---|
MachineState | smp / ram_size / ram / cpu_type / accelerator | 保存整机配置,关联 RAM 后端、CPU 型号和执行后端;MachineClass 的 init 创建具体板级布局。 |
KVMState | fd / vmfd / memory_listener | fd 指向 /dev/kvm,vmfd 指向本虚拟机;监听地址空间变更并维护 KVM 内存槽。 |
CPUState | cpu_index / thread / created / stopped / kvm_fd / kvm_run | 管理一个 vCPU 的宿主线程、控制状态及 KVM 接口。 |
X86CPU / CPUX86State | apic_id / env / env.eip / env.segs / env.mp_state | 保存 x86 型号、体系结构寄存器和处理器启动状态,与通用 CPUState 一起组成 CPU 实例。 |
MemoryRegion / AddressSpace | RAM 区域、别名、子区域与解析后的映射 | 表达客户机可见地址布局,供 CPU、设备和 KVM 监听器使用。 |
BlockDriverState / BlockBackend | 块图节点、格式与协议驱动、前端 I/O 接口 | 将客户机磁盘请求连接到 qcow2 格式节点和宿主文件;virtio-blk 是客户机可见的设备前端。 |
NetClientState / NICState | 网络客户端、peer、队列与设备状态 | 连接 virtio-net 前端和 user、TAP 等网络后端;队列与驱动在客户机启动后进一步初始化。 |
表中的结构保存在 QEMU 用户态;KVMState 和 CPUState 通过 fd 关联宿主内核的 VM 与 vCPU 对象。object_new() 分配并初始化 QOM 对象,qdev_realize() 检查属性、连接总线并完成设备可用化;KVM_CREATE_VM 和 KVM_CREATE_VCPU 则建立内核对象。一个 CPU 对象经过 realize 才会创建执行线程。对象与设备的关系见QEMU 对象模型与设备初始化。
源码中的 created 表示 vCPU 线程初始化握手完成,stopped 表示 QEMU 主动停止它执行客户机。两者可以同时为 true。客户机执行 HLT 后的 halted 又是另一种处理器状态,不能拿来代替管理侧暂停。
七、受控启动的演进与耗时分析
自动启动适合直接运行虚拟机,管理程序则需要在客户机运行前检查拓扑、设置资源策略或等待迁移。-S 提供了“机器已经完成初始化、客户机尚未执行”的控制点。此时内存和 CPU 拓扑已经落地,不能再把启动参数当成任意可改的配置表。
2018 年引入的 preconfig 模式 把控制点前移到板级初始化之前,让管理程序可以先查询允许的配置,再通过 QMP 设置 NUMA 等信息。在本文版本中,它仍是实验接口,只有标记允许的命令可用;x-exit-preconfig 才会继续创建板级设备。它也已经完成一部分前置初始化,不能理解为 QEMU 尚未创建任何对象。
| 控制点 | 解决的问题 | 约束 |
|---|---|---|
| preconfig | 在板级创建前补充允许的机器配置 | QMP 命令集合受限,退出后不能回到此前的配置阶段 |
| -S / prelaunch | 机器创建完成后检查状态,再决定何时执行 | 已确定的启动拓扑不再通过普通配置命令重建 |
| 运行时暂停 | 检查或保存已有执行状态 | 设备异步工作、I/O 收尾和线程同步仍需协调 |
另一条演进线是机器类型版本化。q35 别名指向当前默认版本;pc-q35-10.2 固定相应机器兼容设置,使软件升级时能够继续选择既有虚拟硬件接口。CPU 能力、设备属性和固件也会影响客户机可见行为,所以单独固定机器名不能保证任意两台宿主都能迁移。-cpu host 暴露宿主能力,跨宿主兼容需求通常还需要单独约束 CPU 模型。
分析启动耗时时,也应沿这些边界分别计量:QEMU 参数与对象初始化、内存后端准备、设备 realize、固件执行、内核引导和服务就绪。预分配内存可能把首次访问成本移到启动阶段;减少外设能缩短设备枚举,但不直接消除客户机服务的启动时间。仅比较进程创建到 QMP 可连接的间隔,会漏掉后面的主要工作。
八、源码:初始化与首次执行入口
| 位置 | 阅读重点 |
|---|---|
| qemu_init | machine、早期块后端、accelerator、晚期网络后端、CPU 类型与板级初始化的顺序。 |
| 后端初始化、qemu_create_cli_devices | 块图和网络后端先建立,磁盘与网卡前端随后通过 -device 连接。 |
| machine_run_board_init | 默认内存后端、CPU 类型校验和 MachineClass.init。 |
| pc_q35_init | CPU 创建早于 Q35 内存布局与芯片组设备完成。 |
| kvm_init | 系统 fd、VM fd、能力查询、中断控制器和内存监听器。 |
| qemu_init_vcpu、kvm_vcpu_thread_fn | stopped 初始化、线程握手与 cpu_can_run。 |
| kvm_init_vcpu | KVM_CREATE_VCPU、共享区 mmap 与架构初始化。 |
| qdev_machine_creation_done、x86 复位寄存器 | 初始化收尾、ROM、整机复位与 BSP 取指地址。 |
| qmp_cont、vm_start | 继续执行前的状态检查、通知与 vCPU 恢复。 |