Appearance
第十一章:虚拟机
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 执行 VMLAUNCH 或 VMRESUME 进入 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_VM、KVM_CREATE_VCPU 建立对象,再对 vCPU 执行 KVM_RUN。多数 guest 指令直接运行;KVM 能在内核处理的 exit 不离开内核,未在内核实现的 MMIO 或 port I/O 则通过共享的 struct kvm_run 以 KVM_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 physicalguest 可以自由管理“自己的物理内存”,但二级页表限制它只能落在分配给 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 都在猜哪些页不重要,最坏时反复搬运同一工作集。
设备虚拟化
- 模拟设备:模拟真实硬件寄存器,兼容旧 guest,exit 多、实现复杂;
- 半虚拟化 virtio:guest 使用专用驱动与共享队列,性能更高;
- 设备直通:借助 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 内执行 lscpu、lsblk 和 lspci,再与 host 比较。guest 看到的拓扑是 hypervisor 提供的合同,不一定对应物理插槽和真实设备。
练习
- 为什么 guest kernel 不能直接运行在真实最高特权级?
- 两层调度为什么会让自旋锁表现变差?
- 二级页表限制了什么,代价又是什么?
- virtio 和设备直通在性能与治理上如何交换?
- VM 快照为什么未必是应用一致快照?