Skip to content

第十一章:虚拟机

xv6 本身运行在 QEMU 提供的 RISC-V virt machine 上:它看到的是虚拟 CPU、PLIC、UART 和 virtio disk,却把这些接口当作自己的硬件。本章把视角翻到 QEMU/KVM 一侧,研究这台“机器”如何被实现和约束。

容器中的进程共享宿主机内核;虚拟机则需要将不可信 guest kernel 也置于受控边界内。guest OS 可以管理其虚拟 CPU、guest physical memory 和虚拟设备,但其特权操作不能修改宿主机状态。

硬件虚拟化因此在进程级 CPU 与内存虚拟化之外,进一步虚拟化处理器特权级和设备接口。

Hypervisor

普通用户程序运行特权指令会进入内核;guest kernel 本来就需要改页表、处理中断和控制设备。如果直接给它真实最高权限,隔离便不存在。

硬件虚拟化增加 guest 模式和 host/hypervisor 控制状态。大多数普通指令可直接执行;敏感操作或配置的事件触发 VM exit,hypervisor 检查、模拟或拒绝,再 VM entry 返回 guest。

以 Intel VMX 为例,更准确的名称是 VMX root operation 和 VMX non-root operation;它们与 Ring 0/3 正交。guest kernel 可以在 non-root 中以 CPL 0 执行,仍不等于取得 host 的 VMX root 权限。VMCS 保存 guest state、host state、entry/exit control 和哪些事件需要截获。hypervisor 执行 VMLAUNCHVMRESUME 进入 guest;发生配置为截获的事件后,CPU 把 exit reason 写入 VMCS,装载 host state,回到 hypervisor。

text
guest user
  → guest syscall
guest kernel
  → 普通指令直接执行
  → 敏感事件 VM exit
hypervisor
  → 处理后 VM entry

硬件无需模拟每条指令,只需保证安全关键操作经过 hypervisor。由于 VM exit/entry 成本较高,现代虚拟化允许大多数非敏感指令直接执行。

Linux KVM 又把这条路径分成内核和用户态两层。VMM 打开 /dev/kvm,通过 KVM_CREATE_VMKVM_CREATE_VCPU 建立对象,再对 vCPU 执行 KVM_RUN。多数 guest 指令直接运行;KVM 能在内核处理的 exit 不离开内核,未在内核实现的 MMIO 或 port I/O 则通过共享的 struct kvm_runKVM_EXIT_MMIO/KVM_EXIT_IO 返回 QEMU 等 VMM。接口字段可直接查 KVM API

vCPU 调度

guest 看见的每个 vCPU,在 host 中通常对应一个可被调度的线程。guest 调度器把任务放到 vCPU,host 调度器又把 vCPU 放到物理 CPU,形成两层调度。

两层调度可能产生状态不一致:guest 调度器认为持锁线程正在另一 vCPU 上运行,而该 vCPU 实际已被 host 抢占,其他 vCPU 因而持续自旋。paravirtualized hint、steal time 和合理的 vCPU 配置用于缓解这类问题。

给 VM 配 64 个 vCPU 不代表它随时拥有 64 个物理核心。vCPU 越多,guest 内部同步和 host 同时调度的难度也越大。

二级页表

进程页表把 guest virtual address 翻译为 guest physical address;hypervisor 还要把 guest physical 翻译为 host physical。硬件的 EPT/NPT/Stage-2 translation 将两阶段组合:

text
guest virtual
  --guest page table--> guest physical
  --stage-2 table-----> host physical

guest 可以自由管理“自己的物理内存”,但二级页表限制它只能落在分配给 VM 的 host 页面。IOMMU 对设备 DMA 做类似限制。

在 Intel 上,guest CR3 指向的普通页表完成 GVA→GPA,VMCS 中的 EPT pointer 指向 EPT 完成 GPA→HPA。硬件 page walker 组合两次翻译;guest 修改自己的 PTE 通常不必 VM exit,但访问 EPT 不允许的 GPA 会产生 EPT violation,由 KVM 决定建立映射、模拟 MMIO 或向 guest 注入故障。这里不是 hypervisor 软件逐次查两张表,而是硬件按 hypervisor 配置的 EPT 强制第二层边界。

两级翻译增加 TLB miss 成本,硬件会缓存组合结果;大页能减少压力,却让内存碎片、迁移和脏页追踪更难。VM 内存 overcommit、balloon 与 host 回收还可能形成双重分页:guest 和 host 都在猜哪些页不重要,最坏时反复搬运同一工作集。

设备虚拟化

  1. 模拟设备:模拟真实硬件寄存器,兼容旧 guest,exit 多、实现复杂;
  2. 半虚拟化 virtio:guest 使用专用驱动与共享队列,性能更高;
  3. 设备直通:借助 IOMMU 将物理设备或虚拟功能交给 VM,接近原生性能,但迁移、共享和管理更难。

云平台常在不同设备和租户需求间混用。性能越靠近硬件,hypervisor 介入越少,动态迁移和统一治理通常越困难。

虚拟中断

guest 需要时钟中断做调度,却不能控制真实中断控制器。硬件与 hypervisor 负责向 guest 注入虚拟中断。

在 KVM 中,虚拟 local APIC 可在内核维护;VMM 也可通过 KVM_IRQ_LINE、irqfd 等接口提交中断。若 guest 当前不可接收,pending 状态必须保留,直到虚拟 IF、优先级和 interrupt window 允许注入。所谓“注入中断”因此不是直接调用 guest handler,而是建立一个待交付的虚拟 APIC/CPU 状态,让下一次 VM entry 按 guest 的中断规则发生控制转移。

虚拟时间

hypervisor 还需要虚拟化计时器。VM 被 host 暂停或迁移后,guest 如何理解墙钟、单调时钟和 CPU 时间是不同问题。

时间错误会破坏超时、租约和分布式协议。虚拟时钟不仅要“看起来会走”,还要尽量保持单调并处理 host 时间校正。对侧信道敏感的场景,精确计时器又可能帮助攻击者测量 cache 行为。

状态管理

VM 快照需要捕获虚拟磁盘、内存、vCPU 寄存器和设备状态。若只保存磁盘而不协调 guest 文件系统或数据库,所得状态只具有类似突然断电后的 crash consistency,不保证应用一致性。

在线迁移常先复制内存,同时让 VM 继续运行,再反复传输被写脏的页,最后短暂停机复制剩余状态。写入速率过高时,脏页可能追不上;系统要限速、停止或采用后拷贝等策略。

快速快照通常依赖 COW。其完整成本还包括首次写入时的复制、旧版本回收,以及底层空间不足时的处理。

VM 隔离边界

guest 攻击面从庞大的宿主系统调用集合,缩小到虚拟 CPU、设备、共享服务和 hypervisor 接口。它通常提供更强隔离,但仍可能遭遇:

  • 设备模拟与 hypervisor 漏洞;
  • CPU 推测执行和 cache 侧信道;
  • 共享页去重导致的信息泄漏;
  • 管理面、镜像和凭证配置错误;
  • 固件、微码和物理设备漏洞。

机密计算技术进一步限制 host 读取 guest 内存,但内存加密不自动保护 I/O、访问模式、可用性和恶意设备。使用此类技术时仍需逐项规定威胁模型。

MicroVM

传统 VM 携带完整设备模型和通用固件,启动与攻击面都较大。microVM 删除不必要设备,用极简 VMM 和 virtio 为短生命周期工作负载提供硬件虚拟化边界。

另一类方案使用用户态内核,在进程与宿主内核之间实现系统调用兼容层和沙箱。不同方案分别在兼容性、启动速度、系统调用覆盖率和隔离强度之间折中。

选择隔离技术时,不应只比较空载内存和启动毫秒数,还应比较攻击面、可观测性、升级模型、I/O 路径和失败半径。

实验

bash
# CPU 是否暴露硬件虚拟化能力(名称依架构)
lscpu | grep -E 'Virtualization|Hypervisor'

# 若正在 VM 中,查看 steal time
top

# KVM/虚拟化模块
lsmod | grep -E 'kvm|vhost'

在 guest 内执行 lscpulsblklspci,再与 host 比较。guest 看到的拓扑是 hypervisor 提供的合同,不一定对应物理插槽和真实设备。

练习

  1. 为什么 guest kernel 不能直接运行在真实最高特权级?
  2. 两层调度为什么会让自旋锁表现变差?
  3. 二级页表限制了什么,代价又是什么?
  4. virtio 和设备直通在性能与治理上如何交换?
  5. VM 快照为什么未必是应用一致快照?

上一章:容器隔离 · 下一章:内核可扩展性 →