Linux内核分析之虚拟化-00
49.1 KVM 模块加载与 /dev/kvm
KVM 对用户态的全部接口浓缩为一个字符设备 /dev/kvm 与它派生出的两类 fd(VM fd、vCPU fd)。本章解剖这个"三层 fd"的创建链:/dev/kvm 的 misc 注册(40.3 节)、KVM_CREATE_VM 建立的 struct kvm、KVM_CREATE_VCPU 建立的 struct kvm_vcpu,以及 KVM_SET_USER_MEMORY_REGION 如何把 QEMU 的 malloc 内存"划拨"给客户机。
49.1.1 三层 fd 的对象模型
KVM 的对象层级与 fd 映射:
/dev/kvm (misc 设备, 40.3 节)
│ ioctl(KVM_CREATE_VM, type)
▼
struct kvm (一台虚拟机) ←── vm fd
├── struct kvm_memslots[] ← ioctl(KVM_SET_USER_MEMORY_REGION)
├── struct kvm_io_bus ← 设备 MMIO/PIO 总线 (QEMU 注册)
├── irqchip 模型 (pic/ioapic 的内核态模拟)
└── vcpus[] ← ioctl(KVM_CREATE_VCPU, vcpu_id)
└── struct kvm_vcpu ←── vcpu fd
├── kvm_run 共享页 (mmap(vcpu_fd, 0) 得到!)
├── vcpu 状态 (寄存器/MSR/MMU)
└── 归属一个宿主线程 (QEMU 的 vCPU 线程)
// include/linux/kvm_host.h:770(节选要点)
struct kvm {
struct mm_struct *mm; /* 用户态进程的 mm (QEMU 的!) */
...
struct kvm_memslots __rcu *memslots[KVM_ADDRESS_SPACE_NUM];
struct srcu_struct srcu;
...
struct kvm_vcpu *vcpus[KVM_MAX_VCPUS];
...
struct kvm_io_bus __rcu *buses[KVM_NR_BUSES];
...
};
// :325
struct kvm_vcpu {
struct kvm *kvm;
...
int cpu; /* 当前跑在哪个物理 CPU */
struct kvm_run *run; /* 共享页 (UAPI, kvm.h:224) */
...
int vcpu_id;
...
};
两个关键设计:kvm->mm 是 QEMU 进程的 mm——客户机物理内存就是 QEMU 用户态的普通 malloc/mmap 内存(匿名页/文件页,22 章),KVM 内核侧经 gfn_to_pfn 把"GPA→宿主虚拟地址→物理页"串起来;kvm_run 共享页是 UAPI 契约(kvm.h:223 注释 "for KVM_RUN, returned by mmap(vcpu_fd, offset=0)")——QEMU mmap vcpu_fd 得到这页,KVM_RUN 返回后从这里读 exit_reason 与退出详情,免 ioctl 拷贝。
49.1.2 创建链:KVM_CREATE_VM → KVM_CREATE_VCPU
// virt/kvm/kvm_main.c:5486
static int kvm_dev_ioctl_create_vm(unsigned long type)
创建链的函数级走读:
ioctl(/dev/kvm, KVM_CREATE_VM)
└─ kvm_dev_ioctl_create_vm (:5486)
├─ kvm_dev_ioctl_check_extension (架构能力协商)
├─ kvm_create_vm(type)
│ ├─ 分配 struct kvm, kvm->mm = current->mm
│ ├─ memslots/io_bus/irqchip 初始化
│ ├─ 架构钩子 kvm_arch_init_vm (VMX 的 VMCS 区域池等)
│ └─ 挂入 kvm VM 链表, 返回 fd (anon inode)
▼
ioctl(vm_fd, KVM_CREATE_VCPU, id)
└─ kvm_vm_ioctl_create_vcpu
├─ kvm_arch_vcpu_create → vmx_create_vcpu
│ (分配 VMCS 页 + 装载, 49.2 节)
├─ create_vcpu_fd(vcpu) ← vcpu fd
└─ kvm_vcpu mmap 实现指向 kvm_run 共享页
ioctl(vcpu_fd, KVM_SET_USER_MEMORY_REGION, ®ion)
└─ kvm_vm_ioctl_set_memory_region
└─ __kvm_set_memory_region:
slot = 用户划拨的 [guest_phys, guest_phys+size)
→ 关联 host 用户地址 (userspace_addr)
→ 重建 memslots (RCU 替换, 19.5 节双代思想)
// include/linux/kvm_host.h:593(节选要点)
struct kvm_memory_slot {
gfn_t base_gfn; /* 客户机物理起始页号 */
unsigned long npages; /* 页数 */
unsigned long __rcu *dirty_bitmap; /* 脏页位图 (迁移/回写) */
struct kvm_arch_memory_slot arch;
unsigned long userspace_addr; /* 宿主虚拟地址 (QEMU malloc!) */
...
};
kvm_memory_slot 是"客户机物理地址空间的一块划拨":base_gfn/npages 描述 GPA 区间,userspace_addr 指向 QEMU 进程内的宿主虚拟地址——GPA→HPA 的翻译第一跳就是查这张槽表(49.4 节 EPT 缺页时经 gfn_to_pfn 使用)。dirty_bitmap 记录客户机写过哪些页(热迁移增量传输的数据源)。
49.1.3 misc 设备与能力协商
/dev/kvm 的注册 (40.3 节 misc 框架):
static struct miscdevice kvm_dev = {
.minor = KVM_MINOR,
.name = "kvm",
.fops = &kvm_chardev_ops,
};
kvm_chardev_ops.unlocked_ioctl = kvm_dev_ioctl
kvm_init() → misc_register(&kvm_dev)
能力协商 (QEMU 启动时逐项探测):
ioctl(KVM_GET_API_VERSION) UAPI 版本
ioctl(KVM_CHECK_EXTENSION, KVM_CAP_*)
KVM_CAP_USER_MEMORY 内存槽模型
KVM_CAP_COALESCED_MMIO 合并 MMIO (virtio 用, 51 章)
KVM_CAP_IRQCHIP 内核态中断芯片
KVM_CAP_HYPERV_* / KVM_CAP_X86_DISABLE_EXITS PV 优化
(无对应能力 → QEMU 降级为用户态模拟)
模块加载链:kvm 模块(架构无关核心,virt/kvm/)+ kvm_intel/kvm_amd 模块(硬件后端)——后端模块 probe 时检查 CPU 的 VMX/SVM 能力(cpu_feature_enabled(X86_FEATURE_VMX)),失败则拒绝加载。kvm_init() 同时注册 preuninstall/notifier 保障"有 VM 在跑时模块不可卸载"(40.1.2 节 owner 锁的 kvm 版)。
49.1.4 QEMU 视角:一个 vCPU 线程的一生
QEMU 主循环 (用户态, 内核配合点标注):
pthread_create(vcpu_thread, vcpu_id):
loop:
ioctl(vcpu_fd, KVM_RUN) ←── 进入内核 (49.3 节循环)
← 返回 (exit_reason 已写入 kvm_run)
switch (kvm_run->exit_reason):
KVM_EXIT_IO → 模拟 PIO 设备 (串口/键盘...)
KVM_EXIT_MMIO → 模拟 MMIO 设备 ( virtio 的
notify 就是特殊 MMIO, 51 章)
KVM_EXIT_HLT → vCPU 空转, QEMU 决定停/等中断
KVM_EXIT_FAIL_ENTRY / INTERNAL_ERROR → 错误处理
KVM_EXIT_IRQ_WINDOW_OPEN → 注入 pending 中断
处理 QEMU 自身的信号/timer/热迁移请求
回到 KVM_RUN
线程即 vCPU: Linux 调度器 (11 章) 直接调度
客户机的 vCPU — 客户内的"进程调度"是三层
调度的最内层 (guest 调度器 → QEMU 线程 →
宿主调度器), 过度commit 时就是三层退让
小结
KVM 的接口模型是"三层 fd":/dev/kvm(misc 设备)→ KVM_CREATE_VM 得 struct kvm(memslots/总线/中断芯片)→ KVM_CREATE_VCPU 得 struct kvm_vcpu(kvm_run 共享页 + 硬件上下文);KVM_SET_USER_MEMORY_REGION 把 QEMU 的用户内存划拨为 GPA 槽(base_gfn/userspace_addr/dirty_bitmap);QEMU 的每个 vCPU 线程循环 KVM_RUN,退出原因分流到设备模拟或继续运行。下一节进入硬件上下文本体——VMCS。
49.2 VMCS (x86) / VTTBR (ARM64) / HGATP (RISC-V)
硬件虚拟化需要一个"硬件自动保存/恢复的上下文容器":VM Entry 时硬件从它装载客户机状态,VM Exit 时硬件把客户机状态存回。x86 叫 VMCS(一块 4KB 对齐的区域,字段编码访问),ARM64 的对应物是 VTTBR_EL2 + EL2 寄存器组,RISC-V 是 HGATP + hstatus/hedeleg。本节以 VMCS 为主解剖字段语义,并对照三架构。
49.2.1 VMCS:四段区域
// arch/x86/kvm/vmx/vmx.h(VMCS 包装, 节选要点)
struct vmcs {
u32 revision_id;
u32 abort;
char data[VMCS_SIZE]; /* 字段按编码存取, 非直接偏移 */
};
struct loaded_vmcs {
struct vmcs *vmcs;
...
};
// arch/x86/include/asm/vmx.h:307-380(字段编码示例)
VM_ENTRY_INTR_INFO_FIELD = 0x00004016, /* :307 */
GUEST_CR0 = 0x00006800, /* :356 */
HOST_CR3 = 0x00006c02, /* :380 */
VMCS 的四段区域 (65532 字节上限, 4KB 对齐):
┌────────────────────────────┐
│ 停止状态 (guest 已 halt?) │
├────────────────────────────┤
│ 客户机状态域 (GUEST_*) │ VM Exit: 硬件写入
│ CR0/CR3/CR4, RSP/RIP, │ VM Entry: 硬件装载
│ 段寄存器全套, RFLAGS, │
│ GDTR/IDTR, DR7, MSRs... │
├────────────────────────────┤
│ 宿主机状态域 (HOST_*) │ VM Exit: 硬件装载
│ HOST_CR3, HOST_RSP/RIP, │ (内核回到宿主即用)
│ 段基址, 预设 MSRs │
├────────────────────────────┤
│ 控制域 (决定"什么导致退出") │
│ pin-based: NMI_EXITING │ (vmx.h:90)
│ cpu-based: HLT_EXITING │ (vmx.h:30)
│ secondary: ENABLE_EPT │ (vmx.h:57)
│ exit/msk: 退出原因掩码 │
│ entry: 事件注入 (VM_ENTRY_INTR_INFO_FIELD :307)
└────────────────────────────┘
字段经 VMREAD/VMWRITE 指令按 32 位编码访问
(vmx.h 的枚举值就是编码) — 不是内存偏移!
控制域是虚拟化的"政策":CPU_BASED_HLT_EXITING 决定客户 HLT 是否退出(开着=QEMU 决定怎么睡,关着=硬件直接停);SECONDARY_EXEC_ENABLE_EPT 打开二级地址翻译;VM_ENTRY_INTR_INFO_FIELD 是中断注入的硬件通道(写它 = "下次 Entry 递这个事件给客户")。
49.2.2 三架构的上下文容器对照
x86 (VT-x) ARM64 RISC-V
上下文容器 VMCS (4KB 区域) EL2 寄存器组 hstatus/hedeleg/hsie
翻译根 EPTP (VMCS 字段) VTTBR_EL1/2 HGATP (hgatp CSR)
切换指令 VMLAUNCH/VMRESUME ERET (异常返回) SRET
退出入口 统一 VM-Exit (号) EL2 异常向量 (按类) vscause 编码
保存谁来做 硬件自动(大部分) 软件保存 (KVM 序言) 软件保存
注入事件 VM_ENTRY_INTR_INFO esr/elr 伪造+ERET vscause/vsepc
状态量级 ~20KB (含 VMCS12) 寄存器手存 (~1KB) 寄存器手存
ARM64 的"无 VMCS"哲学: EL2 是真实的异常级别,
客户退出=陷入 EL2 异常向量, KVM 的 __kvm_vcpu_run
汇编手工保存/恢复通用寄存器 — 更简单, 但每次
切换的指令更多 (硬件自动化的反面)
RISC-V 的 H 扩展 (6.8 节): hsstatus/hgatp 语义同构,
VS-mode 是"嵌在 HS 里的 S-mode"
VMCS 的 VPID/PCID 类比:VMCS 的 VPID(虚拟处理器 ID)给客户 TLB 项打标签,避免每次 Entry/Exit 全刷 TLB——与 19.2.4 节 PCID、19.3 节 ASID 完全同构,是"标签化 TLB"在虚拟化层的第三次出现。
49.2.3 控制域决定退出频率
"什么导致退出"的控制位全景 (vmx.h 的三大控制组):
pin-based (异步事件):
NMI_EXITING / posted-interrupt 处理...
cpu-based (指令/特权):
HLT_EXITING HLT 指令
CR3_LOAD_EXITING 写 CR3 (无 EPT 时需要影子页表更新!)
MOV_DR_EXITING 调试寄存器
IO_EXITING / MSR bitmap (位图: 每个 MSR 可单独定去留)
secondary:
ENABLE_EPT 二级翻译
ENABLE_VPID TLB 标签
ENABLE_RDTSCP / UNRESTRICTED_GUEST ...
调优直觉: 每个置位的控制位 = 一类退出 = 一次
世界切换 (~1-2μs, 49.3 节) — KVM 的优化史就是
"把控制位关掉"的历史 (MSR bitmap, EPT, APICv
把中断注入下沉硬件...)
小结
VMCS 是"硬件自动切换的上下文容器":四段区域(停止状态/客户状态/宿主状态/控制域),字段经 VMREAD/VMWRITE 编码访问,控制域决定什么导致退出;三架构对照显示 x86 硬件自动化最重(VMCS),ARM64/RISC-V 走"EL2 异常 + 软件保存"的简路径;VPID 与 PCID/ASID 是同一"标签化 TLB"思想的三层应用。控制位的每一次关闭都是一次退出消除——这是 49.3 节世界切换循环的性能主线。下一节进入执行循环本体。
49.3 VM Entry / VM Exit 与世界切换
vCPU 的执行是三层循环:QEMU 的 ioctl(KVM_RUN) 外循环 → 内核 vcpu_run() 中循环 → vcpu_enter_guest() 的单次世界切换。每次 VM Exit 代价 1-2μs(对照 40 章 ioctl 的纯软件开销,这里还叠加硬件保存/恢复),因此退出次数是虚拟化性能的第一指标——处理路径的全部设计都围绕"能不退出就不退出、退出了尽快回"。本节走读这条循环。
49.3.1 外循环:vcpu_run
// arch/x86/kvm/x86.c:11688(节选)
static int vcpu_run(struct kvm_vcpu *vcpu)
{
int r;
vcpu->run->exit_reason = KVM_EXIT_UNKNOWN;
for (;;) {
/*
* If another guest vCPU requests a PV TLB flush in the middle
* of instruction emulation, the rest of the emulation could
* use a stale page translation. Assume that any code after
* this point can start executing an instruction.
*/
vcpu->arch.at_instruction_boundary = false;
if (kvm_vcpu_running(vcpu)) {
r = vcpu_enter_guest(vcpu); /* 进入客户机 */
} else {
r = vcpu_block(vcpu); /* 无事可做: 睡 */
}
if (r <= 0)
break;
kvm_clear_request(KVM_REQ_UNBLOCK, vcpu);
...
if (kvm_cpu_has_pending_timer(vcpu))
kvm_inject_pending_timer_irqs(vcpu); /* 定时器注入 */
if (dm_request_for_irq_injection(vcpu) &&
kvm_vcpu_ready_for_interrupt_injection(vcpu)) {
r = 0;
vcpu->run->exit_reason = KVM_EXIT_IRQ_WINDOW_OPEN; /* 中断窗口 */
++vcpu->stat.request_irq_exits;
break;
}
...
}
...
}
外循环的三种分支:vcpu_enter_guest(正常进客户)、vcpu_block(客户 HLT 且无待发中断——vCPU 线程睡在内核,等注入事件唤醒)、以及注入窗口检查(客户关中断期间积压的中断,等它开中断的"窗口"再注入)。
49.3.2 单次切换:vcpu_enter_guest
// arch/x86/kvm/x86.c:11105(节选要点)
static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
{
...
/* 退出期间积累的请求 (KVM_REQ_*) 逐一兑现 */
if (kvm_check_request(KVM_REQ_MMU_RELOAD, vcpu))
kvm_mmu_unload(vcpu);
...
if (kvm_check_request(KVM_REQ_EVENT, vcpu))
kvm_apic_accept_events(vcpu); /* 中断/NMI 注入准备 */
...
preempt_disable(); /* 世界切换期间不许抢占 */
kvm_x86_ops.vcpu_run(vcpu); /* = vmx_vcpu_run: VMRESUME */
/* ──── 客户机在这里运行 ──── */
/* VM Exit: 硬件已保存客户状态/装载宿主状态 */
...
exit_fastpath = vmx_run... /* 硬件/快速路径判定 */
...
r = kvm_x86_ops.handle_exit(vcpu, exit_fastpath); /* :6937 */
...
}
一次世界切换的完整时序:
vcpu_enter_guest
├─ [1] 兑现退出期间积累的 KVM_REQ_*:
│ MMU 重载 / 时钟同步 / 中断注入准备 ...
├─ [2] 本地禁止抢占 (vCPU 即线程, 跑在当前 CPU)
├─ [3] VM Entry (VMRESUME):
│ 硬件从 VMCS 装载 GUEST_* 状态
│ RIP 跳回客户上次退出的指令
│ ...客户机执行... (可能是亿条指令, 也可能立即再退)
├─ [4] VM Exit (敏感事件):
│ 硬件保存 GUEST_* → 装载 HOST_* (含 HOST_RIP
│ 指向 KVM 的退出处理汇编)
└─ [5] 处理退出:
fastpath? (可立即重进 — 如 EPT 忙等)
是 → 直接回到 VMRESUME (省一次完整循环)
否 → vmx_handle_exit → 按原因分发
→ 返回值决定: 重进/退到 QEMU/睡
退出代价的构成 (~1-2μs):
硬件保存/恢复 (~200ns) + VMCS 装载 + HOST 状态
(CR3 换回宿主 = TLB 影响, PCID/VPID 缓解)
+ KVM 的请求兑现 + 处理逻辑
49.3.3 退出原因分发:vmx_handle_exit
// arch/x86/kvm/vmx/vmx.c:6937 与 :6781
int vmx_handle_exit(struct kvm_cpu_vcpu *vcpu, fastpath_t exit_fastpath)
static int __vmx_handle_exit(struct kvm_vcpu *vcpu, fastpath_t exit_fastpath)
// arch/x86/include/uapi/asm/vmx.h:42-75(退出原因枚举)
#define EXIT_REASON_CPUID 10
#define EXIT_REASON_HLT 12
#define EXIT_REASON_EPT_VIOLATION 48
高频退出原因与处置 (kvm_run->exit_reason 的上游):
EPT_VIOLATION (48) 客户访问无 EPT 映射的 GPA
→ kvm_mmu_page_fault → 建 EPT 表项 (49.4 节)
→ 重进客户 (QEMU 无感知! 最常见的"内核自愈")
CPUID (10) CPUID 指令总是退出 (语义必须虚拟化)
→ KVM 按特性掩码伪造结果 → 重进
HLT (12) 客户自旋门等事件
→ vcpu_block 睡眠; 注入事件到达时唤醒
→ QEMU 无感知 (内核直接睡, 49.1.4 节外循环的
vcpu_block 分支)
IO / MMIO 客户碰了无内核后端的设备区间
→ 上抛 QEMU (KVM_EXIT_IO/MMIO) → 设备模拟
CR_ACCESS / MSR 控制寄存器/受限 MSR
异常 (NMI/MCE/...) 注入回客户或宿主处理
"上抛 vs 内吞"的分界: 涉及设备状态/策略 → QEMU;
纯地址翻译/指令语义 → KVM 内核自愈
49.3.4 kvm_run 共享页与退出计数
// include/uapi/linux/kvm.h:224(kvm_run 结构)
struct kvm_run {
...
__u8 request_interrupt_window;
...
__u32 exit_reason; /* 为什么回来 (154-169: KVM_EXIT_*) */
...
union {
struct kvm_io_run io; /* KVM_EXIT_IO 详情 */
struct { /* KVM_EXIT_MMIO */
__u64 phys_addr;
__u8 data[8];
__u32 len;
__u8 is_write;
} mmio;
...
};
};
// include/uapi/linux/kvm.h:154-169
#define KVM_EXIT_IO 2
#define KVM_EXIT_HLT 5
#define KVM_EXIT_MMIO 6
#define KVM_EXIT_INTERNAL_ERROR 17
kvm_run 是"内核写给用户态的退出报告":exit_reason + 详情联合体。QEMU 读它决定下一步——virtio 磁盘的 MMIO notify(51 章)、串口的 PIO、fb 的 MMIO 帧缓冲全部走这条上抛通道。vcpu->stat.* 计数族(request_irq_exits 等)经 KVM_GET_STATS_FD 暴露——虚拟化性能分析的原始数据。
退出频率的工程史 (每代优化的目标都是"少退出"):
原始: 每条敏感指令退出
+ MSR bitmap: 按 MSR 粒度决定去留
+ EPT: 内存访问不退出 (影子页表退役, 49.4)
+ APICv/posted interrupts: 中断注入不退出
+ VE (虚拟化异常) / EPT violates 细分: 缺页类不退
剩余硬退出: CPUID/IO/MMIO/HLT/异常注入...
每个 1-2μs 的退出 × 高频路径 = 微服务启动慢
几秒的主要构成 — 这就是 49.1.4 节"三层调度"
之外虚拟化的第二个固有成本
小结
世界切换是三层循环:QEMU 的 KVM_RUN 外循环、内核 vcpu_run 中循环(进入/阻塞/注入窗口三分支)、vcpu_enter_guest 的单次切换(兑现请求→禁止抢占→VMRESUME→Exit→fastpath 或 handle_exit);退出原因的分界是"翻译语义内核自愈、设备策略上抛 QEMU",EPT_VIOLATION 与 CPUID 分别是两类的代表;kvm_run 共享页是退出报告的零拷贝通道。每一次 VM Exit 1-2μs——虚拟化性能工程就是退出消除工程。下一节看最大的退出源:客户机内存翻译(EPT/影子页表)。
49.4 KVM 内存管理 —— EPT / S2PT / G-stage
客户机的虚拟地址要经过两级翻译才能到达物理内存:客户页表(GVA→GPA)由客户内核自管,GPA→HPA 的第二级由 KVM 借硬件 IOMMU 式的二级翻译(x86 EPT / ARM64 S2PT / RISC-V G-stage)或软件影子页表完成。二级翻译是虚拟化性能的心脏——也是 23.4 节"缺一级大页成本翻倍"警告的出处。本节解剖影子页表的代价、EPT 的缺页驱动填充与 TDP MMU。
49.4.1 两级翻译与影子页表的代价
两种第二级翻译方案:
方案 A: 影子页表 (无硬件 EPT 时, 软件合成)
客户页表 (GVA→GPA) 影子页表 (GVA→HPA)
│ 监视客户写 CR3/pte │ 硬件实际使用
└────── KVM 读客户页表, 翻 GPA→HPA, 合成 ──┘
代价: 必须把客户页表设为只读(写保护) → 客户每次
写 pte 都触发退出 → KVM 更新影子表 → 双倍缺页 + 退出风暴
方案 B: EPT/S2PT (硬件二级, 现代 CPU 默认)
GVA ──[客户页表]──> GPA ──[EPT]──> HPA
客户页表归客户管 (CR3 随便换, 无退出)
EPT 由 KVM 惰性填充: 客户碰到未映射 GPA →
EPT_VIOLATION 退出 → KVM 翻译并填表 → 重进
EPT 页表本身: 客户物理帧为单位的四级表 (PML4→...→EPT PTE)
挂在 EPTP (VMCS 字段, 49.2 节)
影子页表的历史代价解释了为什么 EPT(2008+ 硬件)是虚拟化的分水岭:影子方案下每条客户页表写都是一次退出(1-2μs),且双重缺页(客户缺页→退出→宿主缺页)让缺页成本×2——49.3 节"退出消除工程"的最大单项胜利。
49.4.2 内存槽到 EPT 的翻译链
// include/linux/kvm_host.h:593(kvm_memory_slot, 49.1 节)
struct kvm_memory_slot {
gfn_t base_gfn; /* GPA 区间 */
unsigned long npages;
unsigned long __rcu *dirty_bitmap;
unsigned long userspace_addr; /* 宿主虚拟地址 */
...
};
EPT 缺页 (EPT_VIOLATION) 的翻译链:
客户访问 GPA=0x1000_0000 → EPT 无映射 → VM Exit
└─ kvm_mmu_page_fault (arch/x86/kvm/mmu/)
├─ [1] gfn = GPA >> 12 = 0x10000
├─ [2] 查 memslots (RCU, 45 章 RCU 纪律):
│ slot 含 gfn? → userspace_addr + 偏移
│ = 宿主虚拟地址 (QEMU malloc 区内!)
├─ [3] get_user_pages_fast (GUP, 25 章):
│ 宿主虚拟地址 → 物理 pfn (并 pin 住)
├─ [4] 填 EPT PTE: iova=0x1000_0000 → pfn
│ 权限 = 客户 pte 权限 ∧ 槽位权限 (写保护:
│ 只读槽/日志追踪时降权)
└─ [5] 重进客户 (retry, 无需 QEMU 参与)
GPA 的三段式对照 (与 46 章路由/22 章 VMA 同构):
GPA = 槽位号 + 槽内偏移 (分段)
宿主 va = userspace_addr + 偏移 (线性换算)
HPA = GUP/页表 (二段翻译)
第三跳 GUP 的角色是 KVM 与 22 章 VMA 子系统的接口:客户页最终就是 QEMU 进程的页缓存/匿名页——它们参与宿主的回收(24 章)、迁移(18.5 节)、KSM 合并——但被 EPT 映射期间被 GUP 引用(迁移必须先拆 EPT 项,18.5 节 pin 纪律的 KVM 版)。
49.4.3 TDP MMU:EPT 的内存态管理
// arch/x86/kvm/mmu/mmu_internal.h:44
struct kvm_mmu_page { ... /* 一页 EPT/影子表的元数据: gfn 覆盖区间/角色/链接 */ };
// arch/x86/kvm/mmu/mmu.c:111
bool __read_mostly tdp_mmu_enabled = true;
module_param_named(tdp_mmu, tdp_mmu_enabled, bool, 0444);
// arch/x86/kvm/mmu/tdp_mmu.c:253
void kvm_tdp_mmu_alloc_root(struct kvm_vcpu *vcpu, bool mirror)
TDP MMU (Two-Dimensional Paging MMU) 的设计:
传统 MMU 树: 每 kvm_mmu_page 用 rmap/链表管理,
大 VM 下锁与遍历成本高 (百 GB 客户内存)
TDP MMU: EPT 页表节点组织成 RCU 保护的 radix 式
迭代树 (tdp_mmu.c) — 并发填表/遍历/清位
可多线程进行 (vCPU 各自填各自的缺, 无全局锁)
两棵树的关系 (mirror root, tdp_mmu.c:253 的参数):
mirror root: 影子方案备份 (撤销 EPT 时切换)
direct root: EPT 真树
记账: EPT 页表本身占客户内存的 ~0.2% (4 级×512),
计入 kvm->stat 与 memcg — 20.4 节 vmemmap 思想的
KVM 版 ("翻译结构的内存也要算账")
大页与 THP 的联动(23.4 节的机制面):QEMU 用 THP 后端时宿主侧 2MB 连续 → KVM 尝试填 EPT 大页项(一次映射 2MB);宿主 THP 被分裂(23.3.4)→ EPT 必须降级为 4K ×512 项——/sys/kernel/debug/kvm/*/(37.3 节 debugfs)的 mmu 页分布统计直接反映这个塌方。
49.4.4 脏页追踪与热迁移联动
dirty_bitmap (49.1 节 memory_slot 字段) 的两个消费者:
[1] 热迁移 (QEMU live migration):
迭代复制脏页: bitmap 扫描 → 只传变过的页
→ KVM_GET_DIRTY_LOG ioctl 拿位图 (原子清位)
[2] 宿主回写联动: 客户写的页经 GUP 是宿主页缓存/
匿名页 — 36.3 节回写系统照常工作
EPT 的 A/D 位 (accessed/dirty in EPT PTE):
硬件自动置位 (19.2.3 节 A/D 思想的 EPT 版)
→ KVM 用它精确记账 (比 bitmap 更细粒度)
→ nested 虚拟化时再翻译一层 (L1 的 EPT
由 L0 合成 — 影子思想在二级嵌套的回归)
三架构对照(23.4.5 节的展开):ARM64 S2PT 由 VTTBR 指向、权限独立于 S1(Stage-2 可降权客户映射)、无需软件影子(建表时直接查询宿主);RISC-V G-stage 与 HGATP 同构;x86 EPT 最成熟且独有"影子/EPT 双方案"的历史包袱。三者共享本章的槽表/GUP/TDP 思想——架构差异被压到页表格式适配层(与 19 章 pgtable 抽象同构)。
小结
KVM 内存管理的两方案:影子页表(软件合成 GVA→HPA,写保护客户页表导致退出风暴)已被 EPT(硬件二级翻译,缺页驱动惰性填充)取代;EPT 缺页的翻译链"槽表查 userspace_addr → GUP 定位物理页 → 填 EPT → 重进"把客户内存锚定在 QEMU 进程的普通页上,从而天然参与宿主的回收/迁移/KSM;TDP MMU 以 RCU 树支撑多 vCPU 并发填表,EPT 大页与宿主 THP 联动、EPT A/D 位与热迁移位图联动。虚拟化内存的性能史就是"翻译结构的填充时机与粒度"的优化史。KVM 四章至此完成;下一章对照三架构的硬件虚拟化扩展本体。