English日本語

文章

keelOS: 虚拟机磁盘空间扩容,居然不用重启就可以?

先说结果

2026-10-19,一台 Windows 10 虚拟机开着,系统盘从 64 GiB 一路扩到 100 GB,没有关机,没有重启。 在 Windows 的磁盘管理里点一下"重新扫描磁盘",盘尾就多出了新空间。

虚拟机的盘快满了,以前在 keelOS 上要先关机:改大小、开机,guest 才看得到。keelOS 的盘默认是 ZFS 卷(zvol),而我们一直以为 zvol 变了大小,bhyve 收不到通知。这次查下来,通知的路早就是通的,keel 只是一直没去用它。

keelOS 是什么:一个小的 hypervisor(hyp)常驻内存,FreeBSD 作为它上面的管理系统(root)运行 bhyve 和虚拟机;root 可以升级、重启,虚拟机不停。keel 是 root 里管虚拟机的程序和网页。这篇讲的改动都在 keel 里,bhyve 和 ZFS 一行没改。

用起来是什么样

在网页的编辑页把盘的大小改大,保存,当场生效。 虚拟机开着也一样,结果里会写一行"disk0: 66 → 68 GiB"。

  1. 改大小:编辑页里盘的大小填新的数。条件是盘接在 virtio-blk 或 NVMe 上;文件盘和 zvol 都行。
  2. guest 看到新大小:Windows 10 要在磁盘管理里点"操作 → 重新扫描磁盘",网页的提示里写着;Linux guest 上的 zvol 还没测(见最后一节)。
  3. 用新空间:盘尾多出来的未分配空间由用户自己处理——扩展现有分区、建一个新分区,或者先留着。

keel 不替用户扩分区,也不在 guest 里自动重新扫描。用户的原话:"用户自己扩容,就应该让用户自己处理扩容的空间。比如他要添加一个分区呢。"

背后发生了什么

从改一个数到 guest 拿到新容量,中间是一条上游早就铺好的路。 keel 只管第一步,剩下的都是 OpenZFS 和 bhyve 自己做的。

改一个数,guest 当场拿到新容量:这条路上游早就铺好了
改一个数,guest 当场拿到新容量:这条路上游早就铺好了

关键是第二步:zvol 也会发 kqueue 通知。我们一直以为这一步是断的。

以为要改 bhyve,其实早就通了

文件盘早就能开着扩,zvol 不行——这是我们以前的结论,后半句是错的。 文件盘变大时,bhyve 靠 kqueue 听到文件的属性变了(EVFILT_VNODE / NOTE_ATTRIB)。当时在开发机上对 zvol 的设备节点手试 kqueue,得到 EINVAL,于是文档里记下"zvol 要重启",计划给 bhyve 加一条"重读大小"的命令。

2026-10-19 动手前先看了上游源码,发现两头都已经做好了:

  • OpenZFS:2022 年 2 月的提交 983096a1b4"FreeBSD: add kqfilter support for zvol cdev"(Rob Wing,openzfs#13773)给 FreeBSD 的 zvol 字符设备加了 kqueue。只接了 NOTE_ATTRIB 一种,在卷改大小时发;只对 volmode=dev 的卷。
  • bhyve:blockif_register_resize_callback() 对任何描述符都注册这个监听;blockif_resized() 遇到字符设备就用 DIOCGMEDIASIZE 读新大小。
  • keel:建的 zvol 本来就都是 volmode=dev。

当年的 EINVAL 多半是手试的方式不对:zvol 只要标志不是恰好 NOTE_ATTRIB 就回 EINVAL,或者试的是 volmode=geom 的卷。具体是哪个,已经查不到了。

实测(2026-10-19):Lenovo P340 Tiny(i5-10505,36 GB),keelOS 基于 FreeBSD 15.1-STABLE,OpenZFS 2.4.4。虚拟机 win10j:Windows 10,核显直通,系统盘是 NVMe 上的 64 GiB zvol。

  1. 先看 bhyve 有没有在 zvol 上挂好监听:procstat kqueue 里有一行 VNODE,对的正是 /dev/zvol/.../disk0 的描述符,没有 EINVAL。
  2. 10:55:19(日本时间)zfs set volsize=65G:宿主上 diskinfo 当场是新大小,bhyve 的日志里没有错误。之后打开 Windows 的磁盘管理,盘尾是 1.00 GB 未分配。
  3. 11:03:23 再扩到 66 GiB,这次在 guest 里盯着:一个 PowerShell 循环每 2 秒读一次驱动记着的容量(Win32_DiskDrive.Size,读它不会触发重新扫描)。一直是 69,791,500,800,45 秒没变;点了"重新扫描磁盘",11:04:08 变成 70,860,787,200。之后用户又把这块盘扩到 100 GB,同样没有问题。

所以从 ZFS 到 bhyve,再到 guest 重新读盘的大小,这条路是通的。Windows 10 自己不刷新:可能是它的 NVMe 驱动不理这个通知,也可能是通知没到,从 Windows 这边分不出来。

大小以盘为准,只能变大

配置里记的大小只是建盘时的数,网页显示的、编辑时拿来比的,都是盘实际的大小。 这条规矩是实测时发现一个 bug 以后定的。

P340 上那块盘我们是在宿主上直接 zfs set volsize 扩的,虚拟机的配置里还写着 64 GiB。读代码发现:这时在网页上把它改成 65,keel 只拿新旧配置比(65 > 64,只许变大,放行),然后照配置执行 zfs set volsize=65G——66 GiB 的盘被截成 65 GiB。尾部的数据、GPT 放在盘末的备份分区表都会丢。

现在的做法:

  • 显示实际大小:列表、详情页、编辑页读 zvol 的 volsize 或文件长度,不是整 GiB 时带小数。
  • 和实际大小比:新值比实际大才扩;等于实际大小(向上取整到 GiB)只更新配置里的数;比实际小就拒绝。
  • 永远不缩:真正动盘的那一步也先读实际大小,不比它大就什么都不做。

为什么不做缩小:从宿主看,盘只是一串块,分区、文件系统、数据落在哪里只有 guest 知道。缩小就是截掉盘尾,安全的前提是 guest 里先把分区缩进去,keel 既做不了也确认不了。拒绝时的提示里写着办法:先在 guest 里缩分区,关机,再在宿主上手工 zfs set volsize=(文件盘用 truncate -s);更稳的是建一块小的新盘把系统拷过去。因为显示以实际大小为准,手工缩完不用再改配置。

还没做完的

  • Linux guest 上的 zvol 没测。NVMe 和 virtio-blk 各测一次,看 guest 的内核日志有没有 capacity change。这也能分清:Windows 不自己刷新,是 bhyve 的通知没发出去,还是 Windows 不理。
  • Windows 要手动重新扫描。网页的提示里写着;反正接着要在磁盘管理里处理新空间,多点一下。
  • AHCI、IDE 的盘还要重启。这两种控制器只在 guest 探测盘时报容量,没有"容量变了"的通知。
  • 只认 volmode=dev 的 zvol。keel 建的都是;手工建的 volmode=geom 卷照旧等重启,网页会说原因。
  • 网页的改动还没在真机上看过。keel 的测试在 P340 上跑过;编辑页显示实际大小、开着扩容的提示,等新 ISO 装上再看。

别家也差不多:Proxmox VE 的 qm resize 和 VMware ESXi 都支持开着扩容、不支持缩小,Windows guest 在 ESXi 上也要重新扫描磁盘。这是我们对两家文档的印象,这次没有逐条核对。

结语

这次没写一行内核代码,功能本来就在上游里。 要做的是把一个旧结论在真机上重新验一遍,再把 keel 自己的规矩理顺:大小以盘为准,只能变大,多出来的空间交给用户。

← 文章 · 为什么选 keelOS