keelOS: 强化VM的硬核技术,PCIe直通,SR-IOV,一个都不能少!
为什么 VM 主机离不开直通
一台放在家里或小办公室的服务器,往往同时跑着 NAS、路由、Windows 桌面和开发机。大多数设备用虚拟网卡、虚拟磁盘(virtio)就够了,但有两类需求绕不开真实硬件:
- 存储要整块给 NAS:ZFS 这类文件系统要直接看到每一块盘,看得到 SMART、写缓存和真实的错误。隔着一层虚拟磁盘,这些都会打折扣。
- 网络要接近线速:10G、25G 的网卡,经过虚拟交换机转发会吃掉大量 CPU,延迟也更高。
PCIe 直通把一块物理设备整个交给一台 guest,由 guest 自己的驱动直接操作。SR-IOV 则把一块网卡在硬件里切成多个“虚拟功能”(VF),每个 VF 都能单独直通,一块卡就能同时服务多台 guest。
keelOS 的目标很明确:直通不是附加功能,而是一等公民。更进一步,它要和 keelOS 的招牌能力一起工作:宿主系统重启或升级时,guest 不停机。
keelOS 是什么,直通在这里为什么特别难
keelOS 是一个最小的 hypervisor(hyp)加一个 FreeBSD 宿主系统(root)。guest 由 root 里的 bhyve 运行,但 guest 的内存来自 hyp 的内存池,VMX 指令也由 hyp 执行。
这样拆分的好处是:root 可以重新加载或升级内核,而 guest 只暂停二十几秒。root 重启前把 guest 挂起,内存留在 hyp 的池里;新 root 起来后再恢复,TCP 连接不断。
直通恰恰和这件事冲突:
- 直通设备会自己做 DMA,靠 IOMMU(Intel VT-d)把它的访问限制在 guest 的内存里。普通做法是宿主系统管 IOMMU,但 keelOS 的 root 随时会重启。
- 设备在 root 重启期间还在跑:它随时可能写内存、发中断。新 root 接手时还不能复位它,否则 guest 里的磁盘 I/O 就丢了。
- 业界的现状是:ESXi 的 DirectPath 和 Hyper-V 的 DDA 都不能挂起带直通设备的 VM,Xen 不能 save,KVM 只有少数有厂商迁移驱动的设备(如 mlx5)可以迁移。
整块设备直通:NVMe、SATA 控制器,认准了再交出去
开发机上已经直通运行的有:Intel P3600 NVMe(整块)、板载的 Intel C62x SATA 控制器、X710 网卡的 VF。一台叫 nas 的 FreeBSD guest 拿着整块 NVMe 和一个 VF,在上面跑 ZFS 并用 NFS 共享出去。
几个关键设计:
- 按身份认设备,不按插槽。
keel.json里的直通设备用 vendor、device、subvendor、subdevice 加序列号来认。换了插槽照常工作;换了一块盘,它不会被误交给 guest,用它的 guest 不启动并写明原因。 - 开机就预留。内核模块
keelppt.ko在开机早期按身份接管这些设备,让 root 的驱动没机会先挂上。否则 root 可能自动导入 NAS guest 的 ZFS 池,两边同时写,数据就毁了。 - 一个设备只归一台 guest。第二台要同一块设备时,创建或启动直接被拒绝。
- 小于一页的 BAR 也能直通。C62x SATA 控制器有一个只有 256 字节的 BAR,原版 bhyve 直接拒绝。keelOS 像 QEMU/VFIO 那样把这一段拦下来,由 bhyve 代为访问,而且只放行这 256 字节。
VT-d 归 hyp 管
IOMMU 交给了不会重启的 hyp,root 重启期间,直通设备的地址映射始终有效。原来 FreeBSD 的 vmm.ko 自己驱动 VT-d;现在它的 IOMMU 操作改成调用 hyp,hyp 里是一个约 580 行的 VT-d 驱动。
- 宿主域:root 自己的设备看到的是除 hyp 以外的全部内存,由 hyp 建好并保管。
- guest 域:只映射这台 guest 在池里的内存,永远碰不到 hyp 自己。
- 停放的域:guest 挂起、root 重启时,它的域交给 hyp 保管,设备的 DMA 一直有效;新 root 恢复 guest 时再接回来。
这也是 Amazon 在 Xen Live Update 里采用的思路:更换底层软件时,IOMMU 页表在交接期间保持有效。
最硬核的一步:带直通设备的 guest,跨 root 重启不停
拿着整块 NVMe 的 NAS guest,在 root 重新加载期间只暂停 21–23 秒,磁盘 I/O 不丢、不超时、不复位。做法是让设备在整个交接过程中一直运行:
- guest 挂起时,只屏蔽设备的 MSI-X 中断。设备照常完成手上的 I/O,中断暂存在它自己的 PBA 里。
- guest 的内存和 IOMMU 域都留在 hyp 里,设备的 DMA 一直有效。
- 新 root 的 ppt 驱动发现设备在停放的域里,就“接管”而不初始化:不做 FLR 复位,不关总线主控。
- bhyve 按 guest 原来的 MSI-X 设置重新接好中断,暂存的中断随即送进 guest。
设备从没停过:整个 root 重启期间它的 DMA 一直有效,只是中断先存在设备里,等 guest 恢复后再送进去。
实测(开发机,2026-09-29):
| 测试 | 负载 | root 重启次数 | 结果 |
|---|---|---|---|
| NVMe 直通的 NAS guest | 不停地 fsync 写盘 | 4 | 每次暂停 21–23 秒,无错误 |
| 同上,重负载 | 4 个写 + 1 个读,写约 600–700 MB/s | 10 | 无超时、无复位,ZFS scrub 0 错误 |
| 一台 guest 带两块设备 | P3600 + 板载 SATA 控制器 | 5 | 通过 |
累计跨过 14 次 root 重启没有一次出错之后,整块设备直通的 guest 在 root 重启时默认就是“挂起”,而不是关机。
SR-IOV:一块网卡分给多台 guest,老平台也不落下
X710 的 VF 已经在开发机上直通运行。guest 的配置里可以写固定的 VF 编号,也可以写 auto:启动时自动挑一个空闲的 VF,调整 VF 数量后 guest 不用改配置。VF 被重建后 MAC 地址不变,guest 里的网络配置照常有效。
SR-IOV 最容易出问题的是异常情况,所以我们列了 13 项测试:VF 不够、两台 guest 抢同一个 VF、运行中调整 VF 数量、网卡坏了或换了、PF 在 root 里被关掉、root 重启等。其中 9 项实测通过,2 项已有处理,还剩真拔网线和“VF 创建失败后的恢复”两项待测。
i350:FreeBSD 没有的,我们自己写
Intel i350 是服务器上最常见的千兆网卡之一,很多老平台板载就是它。Linux 下它能开 SR-IOV(每个口最多 7 个 VF),但 FreeBSD 只有 guest 里用的 VF 驱动,没有宿主这一侧的 PF 功能,iovctl 根本找不到设备。
keelOS 把它补上了:
- 照 Intel 数据手册写,不用 Linux 代码。Linux 的 igb 驱动是 GPLv2;我们依据 i350 数据手册和 FreeBSD 自己的代码写了约 700 行的 PF 驱动,包括虚拟化模式、PF 与 VF 之间的邮箱协议、VF 复位、MAC 防伪造和端口 VLAN。
- 不换内核。它是一个开机时加载的模块,按更高的优先级接管内核自带的 igb 驱动。
- 状态:在模拟的 82576(同一家族、同样的 PF 逻辑)上全部测试通过:VF 与外部、VF 与 PF、VF 与 VF 之间都能通,9000 字节巨帧能通,VF 申请 VLAN 能通,7 个 VF 反复创建销毁 5 轮正常。真机 i350 的验证正在进行。
我们做了哪些工作:修正 8 个 FreeBSD 的问题,补上 5 项缺失的功能
keelOS 建立在 FreeBSD 和 bhyve 之上。把直通、挂起恢复和 root 重启凑到一起时,我们碰到了原版里的不少问题。每一处修改都是对 FreeBSD 源码的一个补丁脚本,源码一变就会报错,不会静默地失效。
原版行为有错、已经修正的 8 个问题:
| 模块 | 原版的问题 | 后果 |
|---|---|---|
| vmm 本地 APIC | 挂起时丢掉已到期、还没触发的定时器中断 | Linux guest 恢复后定时器停摆,约十次挂起碰到一次 |
| vmm 虚拟 HPET | 计数器跳过比较值后,下一次中断要等计数器绕一圈(最多 256 秒) | Windows 的一个 vCPU 空转读 HPET,guest 卡死 |
| bhyve 电源管理 | ACPI 电源状态不在快照里 | 恢复后按“电源键”会直接杀掉 bhyve |
| bhyve PCI | 恢复时 BAR 还在旧地址,没有按快照里 guest 的设置移过去 | UEFI 移到 4 GB 以上的设备(如 NVMe)恢复后访问不到 |
| bhyve 传统中断(INTx) | 中断线的状态不在快照里 | 恢复后设备中断风暴(e1000、AHCI、virtio) |
| bhyve 串口(LPC) | 不管有没有配置,总向 ACPI 声明 COM1–COM4,两两抢同一个独占 IRQ | Windows 里 4 个串口全是代码 12 |
| igb VF 驱动 | VF 和 PF 共用一个 iflib 描述,没有标成 VF | iovctl -D 删不掉 VF(Device busy) |
| igb VF 驱动 | VF 去读只有 PF 才有的统计寄存器,越过了 VF 的寄存器区 | 宿主里直接 panic,或读到垃圾数据 |
FreeBSD 没有、我们补上的 5 项功能:
- 虚拟 NVMe 控制器的快照:原来带 NVMe 盘的 guest 根本不能挂起。
- 小于一页的 BAR 的直通:原来 Intel C62x SATA 控制器直通不了。
- 直通设备跨 root 重启:ppt 驱动的“接管”模式、bhyve 直通设备的快照。
- VT-d 交给 hyp:vmm.ko 的 IOMMU 操作改成调用 hyp。
- i350 / 82576 的 SR-IOV PF 驱动。
还没做完的
- 中断重映射还没开。直通设备理论上可以被 guest 驱动去伪造中断、打扰 root。个人和小办公室的场景里 guest 都是自己人的,暂时可以接受。
- 带 VF 的 guest 在 root 重启时还是关机再开。让它也能挂起的代码(原样重建 VF,由 guest 驱动自己恢复)已经写好,待实测。
- VT-d 的收尾:VT-d 硬件的寄存器还对 root 可见;“root 的设备无法用 DMA 写进 hyp”还要实测一遍;需要 RMRR 的设备还不能直通。
- i350 真机还在验证。另外,FreeBSD 自己的 igb VF 驱动在 PF 复位后不会自动重新握手,要在 guest 里把网卡 down/up 一次;Linux 的 igbvf 没有这个问题。
- X710 的一个原版问题还在绕开:VF 创建失败后再执行
iovctl -D会让宿主 panic,keel 不这样操作,改为提示经固件重启。
结语
直通和 SR-IOV 本身不是新技术,难的是让它们和“宿主重启、guest 不停”同时成立。keelOS 现在做到了:拿着整块 NVMe 的 NAS,在满负载写盘时跨过 14 次 root 重启,没有一次超时或复位,ZFS scrub 零错误。
下一步是让带 VF 的 guest 也能跨重启不停,并在更多硬件上验证:i350、板载 SATA 带盘、HBA 卡。