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, &region)
   └─ 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 四章至此完成;下一章对照三架构的硬件虚拟化扩展本体。