Skip to content

第十一章:虚拟机

虚拟机需要约束包含内核在内的整台客体机器。容器中的进程请求宿主内核服务;虚拟机里的进程先请求客体内核服务,客体内核再使用虚拟 CPU、内存和设备。

客体应用使用客体内核提供的接口。客体内核以为自己管理的是 CPU、物理内存和设备,但这些资源由虚拟机监控器提供并约束,最终落实到宿主资源上。因此客体中的“物理地址”和宿主物理地址也需要区分。

这种控制与资源归属关系不表示每条客体指令都被软件解释。硬件辅助虚拟化允许许多客体指令直接执行,在配置的事件上才退出到监控器;QEMU 也可以采用软件模拟。运行 xv6 的具体方式取决于环境,不能把 QEMU 与 KVM 当成同一机制。

本章分别分析 CPU、内存和设备如何映射,并检查两层调度、地址翻译和状态保存之间的相互影响。

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. 客体线程已就绪,但其 vCPU 尚未得到宿主 CPU 时间。两层分别维护什么状态,客体调度器能否独立解决这个等待?

  2. 比较进程虚拟地址、客体物理地址和宿主物理地址:谁建立各层映射,谁执行访问检查?

  3. 为什么 guest kernel 不能直接运行在真实最高特权级?

  4. 两层调度为什么会让自旋锁表现变差?

  5. 二级页表限制了什么,代价又是什么?

  6. virtio 和设备直通在性能与治理上如何交换?

  7. VM 快照为什么未必是应用一致快照?

上次更新: