CPU 虚拟化——从 trap-and-emulate 到 VT-x

本文目标:把 CPU 虚拟化的完整脉络讲透——从 Popek-Goldberg 理论、软件 trap-and-emulate、到硬件辅助 VT-x,再到 KVM 的工程实现。读完你能回答"为什么 x86 早期不能做高效虚拟化"“VT-x 的 root/non-root 是什么”“VMCS 存什么”“一次 VM-Exit 的开销和优化”“嵌套虚拟化怎么实现”。


一、要解决的问题:一个物理 CPU 怎么变成多个虚拟 CPU

CPU 虚拟化的本质是:让多个 Guest OS 以为自己在独占一个物理 CPU,实际由 hypervisor(VMM)时分复用一个或多个物理核。

核心难点在于"特权指令"。Guest OS 内核要执行 hlt(停 CPU)、cli(关中断)、改 CR3(切页表)、lgdt(加载 GDT)这些指令——这些指令一旦真在物理 CPU 上执行,会真的关掉中断、真的换掉页表、真的停下 CPU,整机就乱了。所以必须让 Guest 不能直接执行这些指令,或执行后能被截获交给 VMM 处理

这就是 1974 年 Popek-Goldberg 虚拟化定理的核心:

一个体系结构要支持虚拟化,所有敏感指令必须是特权指令(即能被 trap 陷入 VMM 模拟)。

满足这条的架构叫"可虚拟化架构"(如 IBM PowerPC、MIPS)。x86 早期不满足,下面讲它怎么"先天不足"以及硬件怎么补回来。


二、软件方案:trap-and-emulate(陷入再模拟)

2.1 基本思想

让 Guest OS 运行在非 ring0(如 ring1 或 ring3),特权指令一执行就自动 trap(陷入)到 ring0 的 VMM,VMM 模拟这条指令的语义后返回 Guest。

这套"运行在非最高特权级、敏感指令触发陷入、VMM 模拟"的模型叫 trap-and-emulate,也叫软件虚拟化

2.2 x86 的先天缺陷:敏感非特权指令

Popek-Goldberg 要求"敏感指令都是特权指令"。但 x86 有约 17 条敏感指令不是特权指令——它们在 ring1/ring3 执行也不会触发 trap,而是默默执行或返回错误结果。典型代表:

指令 问题
POPF/POPFD(弹标志) 在非 ring0 执行时不恢复 IF(中断标志),Guest 关中断的语义丢失
PUSHF(压标志) 读不到真实 IF
SGDT/SIDT/SLDT/SMSW/STR 在非 ring0 也能读 GDT/IDT/LDT/状态字地址,泄露 host 信息
CPUID 直接返回真实 CPU 信息
CLTS(清 TS 位) 部分场景不 trap

这些指令"静默执行"破坏了 trap-and-emulate 的前提——Guest 跑了敏感操作 VMM 却截获不到,要么信息泄露、要么语义错误、要么直接崩溃。所以早期 x86 在理论上是"不可虚拟化"的。

2.3 三种软件绕过方案(历史)

为绕过这个缺陷,早期 VMM 用三类技术:

  1. 二进制翻译(Binary Translation, BT)——VMware 的看家本领。在 Guest 代码执行前,扫描并把敏感指令翻译成等价的、能 trap 或安全执行的代码块。开销大、实现复杂,但能做到"全虚拟化"(Guest 不用改)。
  2. 半虚拟化(Para-Virtualization, PV)——Xen 的路线。直接改 Guest 内核,让它知道自己活在虚拟机里,主动用 hypercall(类似系统调用,但陷 VMM)替代敏感指令。性能好但要改内核,Linux 早期有 Xen 的 pvops 分支。
  3. 限制 Guest 特权级 + 模拟:妥协方案,性能/兼容性折中。

面试金句:“软件虚拟化有 BT(全虚拟化、Guest 不改但 VMM 复杂)和 PV(半虚拟化、改 Guest 用 hypercall、性能好但侵入)两条路;它们的存在就是因为 x86 没满足 Popek-Goldberg。直到 VT-x 出现,硬件把缺口补上,才有了今天的硬件辅助虚拟化。”


三、硬件辅助:Intel VT-x / AMD-V

2005 年起 Intel 和 AMD 分别加入硬件虚拟化扩展,直接在 CPU 层面补上 Popek-Goldberg 的缺口,从此 x86 可原生高效虚拟化,软件 trap-and-emulate 和 BT 退出主流。

3.1 核心思想:两套特权级——root 与 non-root

VT-x 在原 ring0~3 之上再加一层维度,引入两种操作模式

  • VMX Root Mode(root):hypervisor/VMM 跑在这里,拥有对所有资源的完全控制。
  • VMX Non-Root Mode(non-root):Guest 跑在这里,即使 Guest 在 ring0,它的特权也是"被限制的 ring0"——能管 Guest 自己,但敏感操作会触发 VM-Exit 回 root。

关键点:Guest 的 ring0 不等于物理的 ring0。Guest 内核以为自己握有最高特权,实际被困在 non-root 里,任何敏感指令/事件都会"破壳"回到 root 的 VMM。这就解决了"敏感非特权指令不 trap"的老问题——现在不管什么敏感指令,都在 non-root 下被强制陷入。

3.2 VM-Entry 与 VM-Exit

  • VM-Entry:VMM 执行 VMLAUNCH/VMRESUME,从 root 切到 non-root,开始跑 Guest。CPU 状态从 VMCS 的 Guest-state 区加载。
  • VM-Exit:Guest 执行敏感指令、发生异常、外部中断、IO 等"需要 VMM 介入"的事件时,CPU 自动切回 root,把控制权交还 VMM。CPU 把 Guest 现场存进 VMCS 的 Guest-state,并加载 Host-state。

这套 VMLAUNCH/VMRESUME → 跑 Guest → VM-Exit → VMM 处理 → VMRESUME 的循环就是 KVM 跑一个 vCPU 的本质。

3.3 触发 VM-Exit 的常见原因(面试常问)

类别 举例
敏感指令 hltclilgdtmov CR3wrmsr
异常 Guest 内的 #PF、#GP 等(可设是否注入回 Guest)
外部中断 物理设备中断到达,需 VMM 分发
IO 访问 Guest 访问 IO 端口/MMIO(默认 trap)
主动 hypercall Guest 主动 VMCALL 请求 VMM 服务
EPT 相关 EPT Violation(GPA 未映射)
控制位触发 预设的"无条件 exit"控制位

VMM 可通过 VM-Exit Control 字段精细配置哪些事件触发 exit,做性能权衡——exit 越少越快,但要让 Guest 更多地"自跑"。


四、VMCS:虚拟机的"控制台"

VMCS(Virtual Machine Control Structure) 是 VT-x 的核心数据结构——每个 vCPU 一个 VMCS,是一块 4KB 对齐的内存区域,由硬件识别,存三类信息:

  1. Guest-state area:Guest 的寄存器现场(通用寄存器、段寄存器、CR/DR、RFLAGS、RIP 等)。VM-Exit 时 CPU 把 Guest 现场存进来,VM-Entry 时从这里恢复。
  2. Host-state area:VMM 的寄存器现场。VM-Exit 时 CPU 从这里恢复 Host 状态(RIP、CR3、段等),决定"exit 后跳到 VMM 哪里执行"。
  3. Control fields:控制 VM-Entry/VM-Exit 行为的位域:
    • VM-Execution Controls:决定 non-root 下哪些指令/事件触发 exit(如是否拦截 hlt、是否用 EPT)。
    • VM-Entry Controls:进入时是否加载特定 MSR、注入事件等。
    • VM-Exit Controls:退出时存哪些、是否 ack 中断等。
    • 还有 VM-Exit Information:本次 exit 的原因、限定信息(如哪个 IO 端口、什么异常向量),VMM 据此分派。

VMCS 字段的读写只能用专用指令 VMREAD/VMWRITE不能直接当内存访问。这也是 VMCS 操作开销的来源之一。

面试金句:“VMCS 是硬件为每个 vCPU 维护的一块结构,存 Guest 现场、Host 现场、和控制位;VM-Exit 把现场存进 Guest 区、从 Host 区恢复,VM-Entry 反过来。VMM 靠 VMREAD/VMWRITE 改控制位和取 exit 原因。”


五、一次 VM-Exit 的开销与优化(性能核心)

VM-Exit 是 CPU 虚拟化的性能税。每次 exit/entry 都要:

  1. CPU 保存 Guest 现场到 VMCS(写一堆寄存器)。
  2. 加载 Host 现场从 VMCS(RIP、CR3、段、栈)。
  3. 切换到 VMM 代码,VMM 读 exit reason(VMREAD),分派处理。
  4. 处理完(可能要改 VMCS 控制位),再 VMRESUME 回去。

单次开销约 1000~2000 个周期(几百纳秒)。对 IO 密集或频繁陷入的负载,exit 频率高时 CPU 大量时间花在 root/ non-root 切换上,性能塌方。

优化手段(工程重点)

  • 减少 exit 次数:用控制位放掉 Guest 能自处理的(如 hlt 配合 VMX preemption timer、cli/sti 用 interrupt-window exiting 替代每次拦截)。
  • EPT 免除内存虚拟化陷入:Guest 访问内存走 EPT 硬件翻译,不必每次访存都 exit(详见内存虚拟化篇)。
  • APIC-v / virtual interrupt:硬件虚拟化本地 APIC,收发中断不 exit。
  • posted interrupt:VMM 把中断"投递"给 Guest,硬件直接在 non-root 注入,免 exit。
  • virtio + ioeventfd:Guest 写 virtio queue 的 kick 寄存器被 ioeventfd 捕获,KVM 不必走完整 VMM 路径,IO 性能大增。
  • vCPU 绑核 + 大页:减少 TLB/调度抖动。
  • VMFUNC / #VE:更细粒度的功能切换,进一步少 exit。

面试区分点:“调优 KVM 的本质就是降 exit 频率、降单次 exit 代价。”perf kvm stat/kvm_stat 看 exit 原因分布,热点通常是 hltPIOMMIOMSR read/write,对症优化。


六、KVM:把 VT-x 用起来的工程实现

KVM 是 Linux 内核里的虚拟化框架,它和 QEMU 的分工是云虚拟化的标准答案:

  • KVM(内核模块 /dev/kvm:薄薄一层,负责CPU 与内存虚拟化——用 VT-x 管 vCPU(VMCS、VM-Entry/Exit、调度 vCPU 线程)、用 EPT 管内存翻译。它把"创建 VM/跑 vCPU/注册内存区"作为 ioctl 暴露给用户态。
  • QEMU(用户态进程):负责设备模拟(磁盘、网卡、显卡、串口)、IO 路径(virtio/tap)、vCPU 线程的封装与调度。一个 VM = 一个 QEMU 进程,每个 vCPU = 该进程的一个线程。

KVM 核心 ioctl(呼应操作系统接口篇)

1
2
3
4
5
6
7
fd = open("/dev/kvm");            // 拿到 KVM fd
vmfd = ioctl(fd, KVM_CREATE_VM); // 建一个 VM(对应一个地址空间)
ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ...); // 给 Guest 注册一段 host 内存
vcpufd = ioctl(vmfd, KVM_CREATE_VCPU); // 建 vCPU
mmap(small run area per vcpu);
ioctl(vcpufd, KVM_RUN); // 跑 Guest: 内部 VMLAUNCH/VMRESUME
// VM-Exit 后返回, QEMU 按 exit reason 处理

KVM_RUN 内部就是那个 VMLAUNCH/VMRESUME → 跑 Guest → VM-Exit 的循环——exit 后控制权回到 QEMU 线程,它按 exit reason(IO、MMIO、MMIO virtio kick、异常)分派,处理完再 KVM_RUN

面试金句:“KVM 管芯,QEMU 管壳。KVM 用 VT-x 做 CPU/内存虚拟化并把能力以 /dev/kvm 的 ioctl 暴露,QEMU 作为用户态进程通过这些 ioctl 驱动 KVM、并负责设备模拟和 IO 路径。” 这就是为什么"一个虚拟机是宿主机上的一个进程、一个 vCPU 是一个线程"。


七、嵌套虚拟化:在 VM 里再开 VM

要在 Guest 里跑 hypervisor(如 KVM-in-KVM、容器里跑 VM),需要L0(真硬件)支持 L1(Guest hypervisor)也跑 VMX 指令。难点:L1 执行 VMLAUNCH 时它处于 non-root,不能直接真触发硬件 VM-Entry。

思路:指令 trap + 模拟

  • L0 把 L1 对 VMX 指令(VMLAUNCH/VMREAD/VMWRITE 等)的执行拦截(trap),在 L0 里软件模拟这些指令对 L2 的效果——即 L0 帮 L1 维护 L2 的"虚拟 VMCS",把 L1 想对 L2 做的 entry/exit 翻译成 L0 对 L2 的真实操作。
  • 现代 Intel 有 VMCS ShadowingVMREAD/VMWRITE 在 shadow VMCS 上直接生效少陷入)和 VMCS Shadow + VMFUNC 等硬件加速,降低嵌套开销。
  • AMD 有 AVIC/nVMCB 路线,性能更好。

嵌套虚拟化性能损失通常明显(exit 翻译放大),生产一般不嵌套,但开发/测试/CI、在云 VM 里跑 Kata 容器+VM 等场景会用到。


八、AMD-V 与 ARM 的对照(横向认知)

  • AMD-V(AMD-V/SVM):AMD 的等价物,用 VMRUN/VMLOAD/VMSAVEVMCB(类似 VMCS)做 root/non-root 切换。概念对偶 VT-x。
  • ARMv8 VHE / ARMv9 RME:ARM 用 EL0~EL3 异常等级,hypervisor 跑 EL2,Guest 跑 EL0/1;VHE(Virtualization Host Extension)让 host 内核直接在 EL2 跑减少切换。Stage-2 页表等价 EPT。

跨平台共识:两套特权级 + 现场容器(VMCS/VMCB)+ 陷入再恢复 是 CPU 虚拟化的通用范式,x86/AMD/ARM 都是这个骨架,只是命名和细节不同。


九、面试速答清单

Q1:为什么 x86 早期不能高效虚拟化?

Popek-Goldberg 要求"敏感指令都是特权指令",但 x86 有约 17 条"敏感非特权指令"(如 POPFSGDTCPUID),在非 ring0 执行也不 trap,会静默执行或泄露 host 信息,破坏 trap-and-emulate 前提。只能靠软件 BT(全虚拟化)或改 Guest 的 PV(半虚拟化)绕过,代价大。

Q2:VT-x 的 root/non-root 是什么?解决了什么?

VT-x 在 ring 之上加一层模式维度:root 跑 VMM、non-root 跑 Guest。Guest 的 ring0 不等于物理 ring0,任何敏感指令/事件在 non-root 下都会强制触发 VM-Exit 回 root。这就把"敏感非特权指令不 trap"的缺口从硬件补上,从此 x86 可原生高效虚拟化,BT/PV 退出主流。

Q3:VMCS 存什么?VMREAD/VMWRITE 为什么慢?

每个 vCPU 一个 VMCS(4KB 结构),存三类:Guest 现场、Host 现场、控制位 + exit 信息。VM-Exit 时存 Guest 现场、加载 Host 现场,VM-Entry 反之。VMCS 字段不能当内存直接访问,只能用 VMREAD/VMWRITE 专用指令逐个读写,这是 VMCS 操作开销的来源。

Q4:一次 VM-Exit 多贵?怎么优化?

约 1000~2000 周期(几百 ns)。优化围绕"降 exit 频率 + 降单次代价":用 EPT 免除访存陷入、APIC-v/虚拟中断免中断 exit、posted interrupt 硬件注入中断、ioeventfd 加速 virtio、放掉能自处理的控制位、绑核大页。用 perf kvm stat/kvm_stat 看 exit 原因热点对症调。

Q5:KVM 和 QEMU 各自负责什么?

KVM 是内核模块(/dev/kvm),用 VT-x 做 CPU 和内存虚拟化,把创建 VM/跑 vCPU/注册内存以 ioctl 暴露;QEMU 是用户态进程,通过这些 ioctl 驱动 KVM,并负责设备模拟、IO 路径、vCPU 线程封装。一个 VM = 一个 QEMU 进程,一个 vCPU = 一个线程。

Q6:触发 VM-Exit 的常见原因有哪些?

敏感指令(hlt/cli/lgdt/mov CR3)、Guest 异常(#PF/#GP)、外部中断、IO/MMIO 访问、Guest 主动 VMCALL、EPT Violation、以及控制位预设的无条件 exit。VMM 通过 VM-Exit Control 精细配置哪些触发,做性能权衡。

Q7:嵌套虚拟化怎么实现?

L0 把 L1(Guest hypervisor)对 VMX 指令的执行拦截,在 L0 软件模拟——帮 L1 维护 L2 的虚拟 VMCS,把 L1 想 entry/exit L2 翻译成 L0 对 L2 的真实操作。VMCS Shadowing 等硬件特性降低陷入开销。性能损失明显,一般只用于开发/测试/CI。


十、一张图收口

1
2
3
4
5
6
7
8
9
10
11
软件时代:  Guest(ring1/3) ─敏感指令─► trap ─► VMM(ring0)模拟 ─► 返回
(x86 缺口: 敏感非特权指令不 trap → 只能 BT/PV 绕过)

硬件时代: VMM 在 VMX root, Guest 在 VMX non-root
Guest ring0 ≠ 物理 ring0, 任何敏感事件强制 VM-Exit
VMCS(每vCPU一个): Guest现场 | Host现场 | 控制位+exit信息
循环: VMLAUNCH/VMRESUME → 跑Guest → VM-Exit → VMM处理 → 再回去

工程落地: KVM(/dev/kvm, 内核) 用VT-x, ioctl暴露能力
QEMU(用户态进程) 驱动KVM + 模拟设备 + 封装vCPU线程
一个VM = 一个进程, 一个vCPU = 一个线程

主线一句话:CPU 虚拟化 = 让 Guest 的 ring0 不等于物理 ring0,靠"两套特权级(root/non-root) + 现场容器(VMCS) + 陷入再恢复(VM-Entry/Exit)"把 Guest 关在笼子里自由跑、敏感事件随时能被 VMM 接管。 KVM 把这套硬件能力包成 /dev/kvm 的 ioctl,QEMU 在用户态驱动它并模拟设备——这就是当代云虚拟化的全部地基。


参考资料

  • Intel ® 64 and IA-32 Architectures SDM, Vol.3C, Ch.23~34(VMX 架构权威)
  • AMD64 Architecture Programmer’s Manual, Vol.2(SVM/AMD-V)
  • KVM 源码与文档:Documentation/virt/kvm/, arch/x86/kvm/
  • KVM Forum 历年 slides(嵌套虚拟化、性能优化)
  • 《系统虚拟化:原理与实现》
  • Popek & Goldberg, Formal Requirements for Virtualizable Third Generation Architectures, 1974