English日本語

文章

keelOS: 嵌套虚拟化,bhyve最后的短板我们补齐了~!虚拟机Windows可以跑WSL2了!

虚拟机里再开虚拟机

从今天的构建起,keelOS 的虚拟机可以再开虚拟机:Linux 客户机里能加载 KVM,Windows 客户机里能开 Hyper-V,于是 WSL2、Docker Desktop、Hyper-V 里的小虚拟机都能跑了。这叫嵌套虚拟化,在 KVM、VMware、Hyper-V、VirtualBox 和三家大云上都是老功能,在 bhyve 这一系里是第一次。

谁会用到它:

  • Windows 虚拟机里装 WSL2、Docker Desktop、Android 模拟器。Windows 11 的一些安全功能也要它(VBS,这一篇最后会说到哪一步)。
  • Linux 虚拟机里跑 KVM、Firecracker 这类 microVM,或者在一台 keelOS 上搭 Proxmox / KVM 的实验环境。
  • 做虚拟化本身的开发和测试:在虚拟机里反复装、崩、重来,不碰真机。
产品 嵌套虚拟化
Linux KVM 有,Intel 上 4.20 起默认开
VMware ESXi / Workstation 有
Microsoft Hyper-V 有
VirtualBox 有,6.0 / 6.1 起
AWS、GCE、Azure 有,限机型
Xen / XCP-ng 实验性质
FreeBSD bhyve(上游) 没有
keelOS(bhyve 一系) 从今天起有

bhyve 为什么一直没有

bhyve 的 vmm 在给客户机报 CPUID 时,直接把 VMX(Intel VT-x)和 SVM(AMD-V)的标志位删掉;客户机要是硬执行 VMXON,得到的是一个 #UD。论坛里开发者的说法是"VT-x 模拟在路线图上,但很不简单",我们没找到上游的补丁或 review。反过来(bhyve 跑在 KVM 或 VMware 的客户机里)一直是可以的。

难在哪里:客户机里的 hypervisor(叫它 L1)以为自己在操作真硬件。它写的 VMCS、它建的 EPT 页表、它执行的 VMLAUNCH,全部要由底下的 hypervisor(L0)接住、校验、合成成硬件真正跑的那一套,再把每次退出按规则转交回去。Intel SDM 里关于 VMX 的那几章都要还原一遍,而且 L1 写进来的东西一个字都不能信:KVM 的嵌套代码是它历史上出安全漏洞最多的地方之一。

keelOS 的结构让它变得可做

keelOS 不是把 bhyve 直接跑在 FreeBSD 上。机器上唯一的 VMX root 是我们自己的薄层 hypervisor(hyp),FreeBSD(root)是它的第一个客户机;root 里的 vmm.ko 自己不执行 VMXON,客户机的 VMCS 放在 hyp 里,vmm.ko 通过 hypercall 读写它。这是为了 root 内核能在客户机不停的情况下重新加载(keelOS 的热重启)而做的设计,结果顺带把嵌套变简单了。

客户机 L1 执行 VMXON、VMLAUNCH 这些指令时,和其他退出一样回到 hyp,hyp 原样交给 vmm.ko。vmm.ko 模拟这些指令,把 L1 写的 VMCS(vmcs12)和 keel 自己的那一份合成 vmcs02,而 vmcs02 在 hyp 看来只是又一个普通的 VMCS,用现成的 hypercall 就能建、写、跑。L2 跑起来的时候硬件只看到 hyp → vmcs02,不是"hyp 里再套一层"。所以代码几乎全在 vmm.ko 里,hyp 第一版一行没改,后来为了性能加了两个 hypercall。

L1 客户机(Linux KVM / Windows Hyper-V)         root(FreeBSD)里的 vmm.ko
  在自己的内存里写 vmcs12,执行 VMXON、VMLAUNCH      模拟 VMX 指令,校验 vmcs12
  ┌ L2:WSL2、Hyper-V 或 KVM 里的虚拟机 ┐           vmcs12 + keel 自己的部分 → vmcs02
      │ VMXON、VMLAUNCH 退出    ↑ 进 L2 运行          ↑ 原样交给 vmm.ko   │ hypercall
  hyp:机器上唯一的 VMX root
  所有 VMCS 都在这里:root 的、vmcs01(L1 的)、vmcs02(L2 的),vmm.ko 经 hypercall 读写、运行
  硬件 VT-x:一层嵌套,看到的只是 hyp → vmcs01 或 hyp → vmcs02

L1 的 VMX 指令像任何退出一样回到 hyp、交给 vmm.ko;vmm.ko 合成的 vmcs02 又经 hypercall 回到 hyp 去跑,硬件始终只有一层。

一步步做出来

从 10 月 14 日开工到 15 日出构建,五个阶段,每一步都先在控制机的 QEMU/KVM 三层嵌套床上跑通,再上真机(一台 X11SPW,Xeon Gold 5122,16 GB)验收。

  1. demo:按 VM 的开关;给 L1 看见 VMX 和一套最小的能力 MSR;VMXON 到 VMWRITE 全部模拟;vmcs12 合成 vmcs02;L2 的退出按 L1 设的控制位转交。L1 这时没有 EPT,KVM 退回影子页表。真机上 L2 Linux 18 秒到 shell。
  2. VMCS shadowing:L1 的 VMREAD/VMWRITE 不再退出,落在 hyp 管的一页影子 VMCS 上;vmm.ko 对 VMCS 的逐字段代理改成批量。一次 L2 退出经 hyp 的来回从约 240 次降到 16 次。
  3. 嵌套 EPT:给 L1 真正的 EPT、VPID 和 unrestricted guest。每个 vCPU 一棵 EPT02 = L1 的 EPT12 复合 keel 的 EPT01,按需建,L1 做 INVEPT 或 keel 自己改内存映射时整棵丢掉重建。
  4. Hyper-V 当 L1:Windows 的 Hyper-V 比 KVM 挑得多,而且缺什么都不说。靠 Windows 的事件 46 和在 KVM 上逐个去掉特性对照,找出它的硬要求:TPR shadow、描述符表和 RDRAND/RDSEED 退出、XSAVES 控制位,以及微码版本 MSR 不能是 0。最大的坑不是能力位:Windows 的根分区本身就是 L2,它直接用 L1 的设备,L2 的 I/O 端口、MSR、MMIO 访问都要走 bhyve 正常的设备模拟。
  5. 检查点:keelOS 的客户机跨宿主热重启靠检查点,嵌套状态(VMX 模式、vmcs12、是否正在 L2 里)是一段新格式。做完以后,WSL2 里跑着忙循环的 Windows 跨过一次宿主内核重载,4 个 vCPU 全在 L2 里恢复,WSL2 还是同一台,ssh 会话没断。

顺手抓到两个老 bug:上游 bhyve 的检查点不存 CR0/CR4 的 read shadow;给 L2 重投递事件的半路上合成了一次外部中断退出,真硬件不会这样做。

数字

同一台真机,同一台 Debian 13 客户机(L1,4 vCPU)里用 KVM 起一台最小的 Linux(L2),四个阶段的同一组测试:

测试 demo + VMCS shadowing + 嵌套 EPT
L2 启动到用户态 18 s 9 s 4–5 s
L2 里 2000 次 fork+exec 213 s 48 s 2.18 s
L2 里缺页密集负载(40 次 64 MB 的 dd) — 43.5 s 0.99 s
一次 L2 退出的代价 128 µs 32 µs 32 µs
一次 L2 退出经 hyp 的来回 约 240 16 16

fork+exec 和缺页负载的差距来自 L1 从影子页表换成 EPT:L2 的页缺失不再每次退出到 L1。一次退出 32 µs 是现在的底,计算型的 L2 接近原生,退出密集的 L2(大量小 I/O)会明显慢于直接跑在 keelOS 上。

Windows 10 22H2 客户机(4 vCPU,6 GB)开了 Hyper-V 以后:开机到能 ssh 31–35 秒(不开 Hyper-V 22 秒);WSL2 启动 4–5 秒,起停 10 次全过;Hyper-V 里一台 Gen 2 的 Alpine 虚拟机 60 秒内到登录提示。这台 Windows 跑着的时候,同一台主机上没开嵌套的客户机回归全过,它们的代码路径和以前完全一样。

Windows 里的 WSL2 长什么样

和在一台普通 PC 上一样:任务管理器显示"虚拟化:已启用",systeminfo 说满足 Hyper-V 要求,在"启用或关闭 Windows 功能"里勾上 Hyper-V 和虚拟机平台,重启,wsl --install 或者导入一个发行版就行。我们的测试机里:

PS> (Get-ComputerInfo).HyperVisorPresent
True
PS> wsl -l -v
  NAME      STATE           VERSION
* Alpine    Running         2
PS> wsl -e uname -a
Linux DESKTOP-JQJ584H 5.15.167.4-microsoft-standard-WSL2 #1 SMP x86_64 Linux

在 keelOS 这边要做的只有一件事:虚拟机设置里勾上"允许在这台虚拟机里再开虚拟机"(配置里的 "nested": true,命令行 keel vm create --nested)。开关是每台虚拟机单独的,默认关;主机不支持(AMD、没有 hyp)时开关置灰并说明原因。模板会自己处理一个细节:开了嵌套的 Windows 不再拿 keel 给的 Hyper-V 增强(它自己要当 hypervisor 了)。

限制和还没做的

  • 只有 Intel。AMD 的嵌套 SVM 还没做(VMCB 在内存里,比 VMX 简单,排在后面)。
  • 开了嵌套的虚拟机不能直通 PCI 设备,它自己也不用 APICv。别的虚拟机照常。
  • Windows 11 的 VBS/HVCI(内存完整性)还不行:开了以后根分区黑屏。同一块盘在 QEMU/KVM 下一样,缺的是 MBEC(按模式的执行控制)、虚拟 IOMMU 和 Secure Boot 变量,这是三个单独的项目。VBS 本身能起来,WSL2、Docker Desktop、Hyper-V 虚拟机不受影响。
  • 性能:计算型和缺页重的负载接近直接跑;退出密集的负载(大量小 I/O)每次退出还要约 16 次经 hyp 的来回。拿它跑生产虚拟化不合适,WSL2、开发环境、实验室足够。
  • 热重启:在试运行槽上(升级还没 apply)嵌套虚拟机仍然在宿主重载时关机,因为老槽读不了新的检查点格式;apply 以后和其他虚拟机一样挂起、恢复。
  • Linux 6.12 的 KVM 和 Windows 10 22H2 的 Hyper-V 是测过的 L1;Windows 11、Windows Server、Proxmox 当 L1 还没试。

怎么试、下一步

今天的构建 keelos-35d64d3-20261015T034126Z.iso 带上面的全部。装到一个空闲槽试运行,给一台 Linux 或 Windows 虚拟机勾上嵌套开关就能试;详细的说明在手册的"嵌套虚拟化"一页。

接下来按顺序:量清楚一台忙着的嵌套虚拟机对同主机其他虚拟机的影响(正在跑,并和原生 FreeBSD 15 bhyve、Debian KVM 对照);AMD 的嵌套 SVM;Hyper-V 的 enlightened VMCS(Windows 下更快);然后是 HVCI 要的 MBEC。

← 文章 · 为什么选 keelOS