4. KVM架构与源码结构
KVM 并不是一个独立运行的虚拟机监控器,而是 Linux 内核中的一个虚拟化子系统。它把 Linux 内核本身变成 Hypervisor,向上通过字符设备 /dev/kvm 暴露统一的控制接口,向下依赖 CPU 提供的硬件虚拟化扩展。在 ARM64 平台上,这套扩展就是 EL2 异常级别、HCR_EL2 寄存器、VGIC 中断虚拟化以及 Stage-2 地址翻译。理解 KVM 的架构,核心是理解它如何用“一个内核模块 + 两类抽象对象 + 一组 ioctl 命令”把复杂的硬件虚拟化能力封装成用户态可编程的接口。
一、KVM 内核模块:从加载到 /dev/kvm
KVM 在源码中以模块形式组织。x86 上分为 kvm.ko 与 kvm-intel.ko,而 ARM64 上通常编译为 kvm.ko,其架构相关代码位于 arch/arm64/kvm/。模块初始化时会调用 kvm_init(),注册字符设备、初始化全局的 VM 与 vCPU 链表,并设置架构相关的回调函数表 kvm_arch_ops。加载成功后,用户态即可看到 /dev/kvm 设备节点。
# 查看 KVM 模块与设备节点
lsmod | grep kvm
ls -l /dev/kvm
dmesg | grep -i kvm
# 典型输出
kvm [ 1234.567] kvm: initialized
kvm [ 1234.568] kvm: KVM_CAP_ARM_VMID_BITS = 8
这个模块承担三类职责:第一,管理硬件虚拟化能力的探测与使能,例如检查 CPU 是否支持 EL2、是否支持 52 位物理地址;第二,提供 VM 与 vCPU 的生命周期管理;第三,处理用户态通过 ioctl 下发的所有控制命令。可以说,/dev/kvm 是用户态与 KVM 内核模块之间的唯一入口。
二、VM 抽象与 vCPU 抽象
KVM 用两个核心结构体描述虚拟机:struct kvm 表示一个 VM,struct kvm_vcpu 表示一个虚拟 CPU。一个 VM 可以包含多个 vCPU,每个 vCPU 在内核中对应一个线程上下文,并拥有独立的寄存器组、异常级别状态和 Stage-2 页表视图。
- VM 抽象:持有内存槽(memory slot)、Stage-2 页表、虚拟中断控制器、VMID 等全局资源。用户态通过
KVM_CREATE_VM创建,得到 VM 的文件描述符。 - vCPU 抽象:持有通用寄存器、系统寄存器、PC、PSTATE、vGIC 的 CPU 接口状态。用户态通过
KVM_CREATE_VCPU创建,得到 vCPU 的文件描述符。 - 运行关系:vCPU 必须绑定到某个 VM,多个 vCPU 共享同一份 Stage-2 页表和虚拟中断控制器。
在 ARM64 上,vCPU 的运行入口是 kvm_arch_vcpu_ioctl_run(),它最终会执行 kvm_arm_vcpu_enter_exit(),通过 eret 指令切换到 EL2 或 EL1 执行 Guest 代码。当 Guest 触发敏感指令或外部中断时,CPU 会陷入 EL2,KVM 根据退出原因决定是继续运行还是返回用户态。
三、ioctl 接口:用户态控制 KVM 的通道
KVM 的所有控制都通过 ioctl 完成。用户态程序(如 QEMU、Firecracker、crosvm)打开 /dev/kvm 后,先创建 VM,再创建 vCPU,然后反复调用 KVM_RUN 驱动 vCPU 执行。下表列出 ARM64 上最常用的 ioctl 命令及其作用。
| ioctl 命令 | 作用对象 | 说明 |
|---|---|---|
KVM_GET_API_VERSION | /dev/kvm | 获取 KVM API 版本,通常为 12 |
KVM_CREATE_VM | /dev/kvm | 创建一个 VM,返回 VM fd |
KVM_CREATE_VCPU | VM fd | 创建 vCPU,返回 vCPU fd |
KVM_ARM_VCPU_INIT | vCPU fd | 初始化 vCPU 的复位状态与特性 |
KVM_SET_USER_MEMORY_REGION | VM fd | 注册用户态内存作为 Guest 物理内存 |
KVM_RUN | vCPU fd | 运行 vCPU,直到发生 VM Exit |
KVM_GET_REGS / KVM_SET_REGS | vCPU fd | 读写通用寄存器 |
KVM_GET_ONE_REG / KVM_SET_ONE_REG | vCPU fd | 读写单个系统寄存器 |
其中 KVM_RUN 是最关键的命令。它并不返回 Guest 的执行结果,而是返回一个退出原因 struct kvm_run,其中 exit_reason 字段告诉用户态“为什么退出”。常见的退出原因包括 KVM_EXIT_MMIO、KVM_EXIT_IO、KVM_EXIT_ARM_NISV 以及 KVM_EXIT_SYSTEM_EVENT。用户态必须根据退出原因进行模拟,然后再次调用 KVM_RUN。
四、ARM64 KVM 目录组织
ARM64 的 KVM 源码分布在多个目录中,理解目录结构有助于快速定位问题。通用 KVM 代码位于 virt/kvm/,ARM64 架构相关代码位于 arch/arm64/kvm/,而 ARM 共用的虚拟化代码位于 arch/arm64/kvm/ 与 virt/kvm/arm/ 之间。下表给出主要目录与职责。
| 路径 | 职责 |
|---|---|
virt/kvm/kvm_main.c | 通用 VM/vCPU 管理、ioctl 分发、内存槽管理 |
arch/arm64/kvm/ | ARM64 架构初始化、vCPU 进入退出、系统寄存器处理 |
arch/arm64/kvm/vgic/ | 虚拟 GIC 中断控制器实现 |
arch/arm64/kvm/sys_regs.c | 系统寄存器访问陷入与模拟 |
arch/arm64/kvm/mmu.c | Stage-2 页表管理与内存映射 |
arch/arm64/kvm/hyp/ | EL2 异常级别下的底层运行代码 |
include/uapi/linux/kvm.h | 用户态可见的 ioctl 命令与结构体定义 |
其中 arch/arm64/kvm/hyp/ 是 ARM64 特有的关键目录,它包含在 EL2 下运行的代码,例如 vhe/ 与 nvhe/ 两种模式。VHE(Virtualization Host Extensions)允许 KVM 直接复用 EL1 的代码路径,减少上下文切换开销;而 nVHE 则把 Host 与 Hypervisor 严格分离,适合安全隔离场景。
五、案例:一次 KVM_RUN 的完整路径
假设用户态 QEMU 已经创建好 VM 与 vCPU,并注册了内存。当 QEMU 调用 KVM_RUN 时,内核路径大致如下:
- 用户态执行
ioctl(vcpu_fd, KVM_RUN, 0)。 - 内核进入
kvm_vcpu_ioctl(),分发到kvm_arch_vcpu_ioctl_run()。 - ARM64 代码保存 Host 上下文,配置
HCR_EL2、VTTBR_EL2等寄存器。 - 执行
eret进入 Guest,Guest 在 EL1/EL0 运行。 - Guest 访问 MMIO 或执行 WFI,触发异常陷入 EL2。
- KVM 填充
struct kvm_run的exit_reason,返回用户态。 - QEMU 读取退出原因,模拟设备或处理中断,再次调用
KVM_RUN。
这个循环是 KVM 虚拟化的基本节拍。每一次 KVM_RUN 都可能对应多次 VM Exit,而用户态模拟的效率直接决定了虚拟机的整体性能。
/sys/module/kvm/parameters/ 下的参数,或使用 trace-cmd 跟踪 kvm_exit 事件,快速定位退出原因分布。
六、常见误区与复盘
- 误区一:把 KVM 当成完整 Hypervisor。 KVM 只提供内核态虚拟化能力,设备模拟、固件加载、虚拟机生命周期管理都由用户态完成。
- 误区二:认为 vCPU 就是物理 CPU。 vCPU 是内核线程,多个 vCPU 可以调度到同一个物理 CPU 上,也可以迁移。
- 误区三:忽略 ioctl 的返回值。
KVM_RUN返回 0 并不代表 Guest 执行成功,必须检查kvm_run.exit_reason。 - 误区四:混淆 Stage-2 与 Stage-1 页表。 Stage-1 由 Guest 内核管理,Stage-2 由 KVM 管理,两者共同完成 Guest 虚拟地址到物理地址的翻译。
复盘时建议从三个维度检查:第一,/dev/kvm 是否可访问、KVM 模块是否加载;第二,VM 与 vCPU 的创建顺序是否正确,内存区域是否覆盖 Guest 的物理地址空间;第三,KVM_RUN 的退出原因是否被正确处理。掌握这三层,就掌握了 KVM 架构的主干。