virt-report虚拟化社区动态

QEMU 虚拟机启动与 vCPU 初始化

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、从创建进程到执行第一条指令

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 等机器属性,所以它必须在这些属性确定之后运行。

从后端创建到系统盘引导的初始化顺序
块后端在早期后端阶段创建,KVM 随后初始化,网络后端在晚期后端阶段创建。板级初始化建立 CPU、内存和总线,命令行设备再连接磁盘与网络后端;复位和 cont 后客户机开始固件与系统引导。
块后端在早期后端阶段创建,KVM 随后初始化,网络后端在晚期后端阶段创建。板级初始化建立 CPU、内存和总线,命令行设备再连接磁盘与网络后端;复位和 cont 后客户机开始固件与系统引导。 打开原图
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 回调创建线程。

vCPU 创建握手与首次执行
KVM vCPU 和共享区由新线程建立;主线程等待 created 通知,再继续机器初始化。cont 之后,BSP 从复位向量开始,其他处理器等待启动事件。
KVM vCPU 和共享区由新线程建立;主线程等待 created 通知,再继续机器初始化。cont 之后,BSP 从复位向量开始,其他处理器等待启动事件。 打开原图
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:BSPCPU 1:AP
机器创建完成,-S 生效线程与 KVM vCPU 已存在,stopped = true线程与 KVM vCPU 已存在,stopped = true
复位后的多处理器状态KVM_MP_STATE_RUNNABLE内核 irqchip 路径下为 KVM_MP_STATE_UNINITIALIZED
执行 contQEMU 解除 stopped,允许进入 KVM_RUNQEMU 同样解除 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 与操作系统运行。

六、机器、执行后端与处理器对象

对象关键字段职责
MachineStatesmp / ram_size / ram / cpu_type / accelerator保存整机配置,关联 RAM 后端、CPU 型号和执行后端;MachineClass 的 init 创建具体板级布局。
KVMStatefd / vmfd / memory_listenerfd 指向 /dev/kvm,vmfd 指向本虚拟机;监听地址空间变更并维护 KVM 内存槽。
CPUStatecpu_index / thread / created / stopped / kvm_fd / kvm_run管理一个 vCPU 的宿主线程、控制状态及 KVM 接口。
X86CPU / CPUX86Stateapic_id / env / env.eip / env.segs / env.mp_state保存 x86 型号、体系结构寄存器和处理器启动状态,与通用 CPUState 一起组成 CPU 实例。
MemoryRegion / AddressSpaceRAM 区域、别名、子区域与解析后的映射表达客户机可见地址布局,供 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_initmachine、早期块后端、accelerator、晚期网络后端、CPU 类型与板级初始化的顺序。
后端初始化、qemu_create_cli_devices块图和网络后端先建立,磁盘与网卡前端随后通过 -device 连接。
machine_run_board_init默认内存后端、CPU 类型校验和 MachineClass.init。
pc_q35_initCPU 创建早于 Q35 内存布局与芯片组设备完成。
kvm_init系统 fd、VM fd、能力查询、中断控制器和内存监听器。
qemu_init_vcpu、kvm_vcpu_thread_fnstopped 初始化、线程握手与 cpu_can_run。
kvm_init_vcpuKVM_CREATE_VCPU、共享区 mmap 与架构初始化。
qdev_machine_creation_done、x86 复位寄存器初始化收尾、ROM、整机复位与 BSP 取指地址。
qmp_cont、vm_start继续执行前的状态检查、通知与 vCPU 恢复。

九、参考资料