keelOS: fnOS, 为了适配你,我做了N多调整!
为什么是 fnOS
2026-10-02,飞牛 fnOS 在一台 8 GB 内存的联想 P340 Tiny 上作为 keelOS 的虚拟机跑了起来,硬盘和核显都是直通的,飞牛影视的硬件转码走的是那块 Intel UHD 630。为了走到这一步,keelOS 从固件到内核一共改了十几处,这篇文章把它们逐项列出来。
keelOS 的定位是“虚拟机优先”:宿主只管把虚拟机跑好,NAS 不做在宿主里,而是作为一台虚拟机,直接拿到整块硬盘控制器。fnOS(飞牛私有云)是一个闭源、免费的家用 NAS 系统,基于 Debian,内核很新(这次测的 1.2.0701 是 Linux 6.18)。它正好是这种用法的典型对象:用户想要的是 fnOS 的相册、影视和应用,同一台小主机上再跑几台别的虚拟机。
读下去之前需要知道 keelOS 的结构。它分三层:最底下是一个很小的常驻层 hyp,机器开机后一直在;hyp 上面是 root,一个 FreeBSD 系统,用 bhyve 运行虚拟机,root 可以重启、升级;最上面是各台虚拟机,root 重启时它们只是暂停几十秒,不会关机。VT-d(IOMMU)由 hyp 管。
所谓“适配”,不是给 fnOS 写了专门的代码。fnOS 没有改一行,下面每一项改的都是 keelOS 自己:有的是 bhyve 缺的功能,有的是 FreeBSD 内核里一张表少了几行,有的是我们自己的 bug。fnOS 只是第一个把它们全都撞出来的系统。
调整一览
一共 14 项,分两批:2026-09-29 在开发机上第一次装 fnOS 时的 4 项,2026-10-02 在 P340 上做直通时的 10 项。其中 1 项没修。
| # | fnOS 里看到的 | 原因 | keelOS 改了什么 | 状态 |
|---|---|---|---|---|
| 1 | 安装程序起不来,X 报 no screens found | 安装程序要一个 DRM 显示设备,bhyve 的帧缓冲不是 | 加了 virtio-gpu 显示,fnOS 模板默认用它 | 已修 |
| 2 | 内存显示 Unknown | bhyve 的 SMBIOS 不填内存条的类型和速率 | 从宿主读出真实值填进去 | 已修 |
| 3 | 硬盘显示成机械盘 | Linux 的 virtio-blk 驱动把盘一律标成旋转介质 | 改用模拟的 NVMe 盘就显示 SSD | 可选 |
| 4 | 网卡显示半双工 | 报速率和双工要 virtio 的新接口,bhyve 只有旧接口 | 没改 | 没修 |
| 5 | 需要自己管一块盘 | — | 整块 NVMe 直通 | 能用 |
| 6 | 带核显的虚拟机启动不了 | 我们生成 bhyve 参数的顺序错了 | 改顺序,测试也查顺序 | 已修 |
| 7 | — | bhyve 14.5 的核显代码老,有一个长度算错的 bug | 换成 FreeBSD 15 的版本(10 个上游提交) | 已做 |
| 8 | — | Intel 驱动按位置找核显,按主板芯片组的型号认平台 | keel 自动把核显放在 0:2.0,芯片组型号用宿主的 | 已做 |
| 9 | — | 虚拟机的固件不知道哪两段内存要留给核显 | 固件读 bhyve 传来的内存图 | 已做 |
| 10 | 启动不了:核显保留内存的地址和大小都是 0 | FreeBSD 内核的设备表里没有 10 代酷睿 | 内核补丁,加上 10 代的设备号 | 已修 |
| 11 | 核显驱动每一帧报一次 plane fault | 核显的页表里有没人写过的项 | bhyve 交出核显之前把页表填满 | 已修,P340 上验证 |
| 12 | 转码程序找到的第一块显卡不是核显 | 模拟显卡先注册,占了第一个渲染节点 | 带核显的虚拟机自动改用普通帧缓冲 | 已改,真机上还没验 |
| 13 | — | 核显是宿主唯一的显卡,给了虚拟机宿主就没有屏幕 | hyp 在核显易手前停止写屏;另做一台显示小虚拟机把屏幕接回来 | 已做,P340 上验证过 |
| 14 | 每次升级 fnOS 都被关机再开 | hyp 的文件头里有编译时间,每次升级都被当成“换了 hyp” | hyp 改成可重复构建 | 已修,还没验证一次完整的升级 |
先说容易的:装系统和直通硬盘
第一批 4 项是 2026-09-29 第一次装 fnOS 时碰到的,都和直通无关。
- 安装程序要一块“显卡”。 fnOS 的图形安装程序是 X 加 modesetting 驱动,要
/dev/dri/card0。bhyve 给虚拟机的显示在 Linux 里只是一块固件帧缓冲,不是 DRM 设备,X 直接报 no screens found。keelOS 为此给 bhyve 加了一个 virtio-gpu 显示设备,现在 fnOS 的模板默认用它。装好以后的 fnOS 是网页管理的,不再需要它。 - 内存显示 Unknown。 fnOS 的系统信息页从 SMBIOS 读内存条的类型和速率,bhyve 不填。现在 keel 从宿主读出真实的值传给虚拟机,fnOS 里显示“1 条共 4 GB | 2666MHz | DDR4”。
- 硬盘显示成机械盘。 Linux 6.18 的 virtio-blk 驱动把盘固定标成旋转介质,virtio 协议里没有办法改。换成模拟的 NVMe 盘,fnOS 就显示 SSD。
- 网卡显示半双工,没修。 virtio-net 报速率和双工的特性位在第 63 位,bhyve 的 virtio 只有旧接口(32 位特性)。只影响显示,不影响速度,等以后统一换新接口时一起做。
直通硬盘是 keelOS 本来就有的功能,这次没有改代码。2026-10-02 在 P340 上,第二块 NVMe(三星 SM961)整块交给 fnOS,fnOS 在上面建了 Btrfs 存储空间,dd 直写 227 MB/s。宿主按设备的型号和序列号认这块盘,而不是按插槽位置,加卡、换槽以后不会认错;开机时就把它留给虚拟机,宿主的驱动从头到尾不碰它。
核显直通:从启动不了到硬件转码
核显直通在 P340 上一天内跑了三轮,每一轮卡在一个新地方,第三轮结束时 fnOS 用核显转码成功。bhyve 上游有核显直通的代码(Intel 叫 GVT-d),但从“有代码”到“fnOS 里能转码”中间隔着下面这些事。
核显和独立显卡不一样的地方:它有一部分状态不在设备上,而在主板固件留出来的内存里。一段是保留内存(stolen memory,P340 上 32 MiB),一段是描述显示接口的表(OpRegion),还有核显自己的页表(GTT)。这些都要在虚拟机里有对应的东西,每一样都出过问题。
上真机之前做的三件事
- 换 bhyve 的核显代码。 keelOS 基于 FreeBSD 14.5,它的核显代码有一个长度算错的 bug,也不认 11 代以后的核显。我们取了 FreeBSD 15 的 10 个提交。
- 固件要留出内存。 bhyve 在虚拟机的内存图里标出保留内存和 OpRegion 两段,经 fw_cfg 传给虚拟机的 UEFI 固件。上游的固件不读这张图,这两段会被当成普通内存用掉。我们的固件加了读它的补丁。
- 核显要在老地方。 Intel 的驱动只在 0:2.0 找核显,还按 ISA 桥的设备号认芯片组。keel 发现直通的是 Intel 显示设备,就自己把它放到 0:2.0,把宿主 ISA 桥的型号抄给虚拟机。用户只是在网页上勾选这块显卡。
第一轮:虚拟机起不来
bhyve 直接退出,日志是 pci slot 0:31:0 already occupied!。这是我们自己的 bug:给 ISA 桥设型号的参数写在了声明这个槽的参数前面,bhyve 按顺序读参数,槽还没声明就有了配置,声明时就当成已占用。单元测试只查了参数在不在,没查顺序;现在两样都查。
第二轮:保留内存的地址和大小都是 0
bhyve 的核显代码第一次真的运行,立刻退出:Unable to add Graphics Stolen Memory to E820 table (hpa 0x0 len 0x0)。保留内存的位置是 FreeBSD 内核在启动早期按一张设备号表去读的,这张表从 9 代的 Coffee Lake 直接跳到了 Cannon Lake,没有 10 代的 Comet Lake。P340 的 UHD 630 正好是 Comet Lake。修法是一个内核补丁,把 10 代的设备号加进表里。
第三轮:驱动起来了,每一帧报一次错
fnOS 的内核认出了核显,固件都加载成功,按显示器的 3840x2160 建了帧缓冲。但从点亮显示的那一刻起,内核日志每一帧刷一行 [CRTC:56:pipe A][PLANE:33:plane 1A] fault。hyp 这边的记录是:核显在读地址 0x7acbf6a000,被 VT-d 拒绝。这个地址在 491 GB 处,而这台机器只有 8 GB 内存。
原因在核显的页表。显示引擎取帧缓冲时会多读前后几页,经过的是帧缓冲旁边的页表项。这张表放在固件留出的内存里,固件只填它用到的项,复位设备也不会清它,其余的项里是残留的数据。裸机上读到哪里都无所谓;在虚拟机里,这就是一次越界的内存访问。
三行是同一条路径:显示引擎查核显的页表得到地址,再经 VT-d 去读内存。出问题的是中间一行,多读的那几页查到的是没人写过的项。
Linux 的 i915 驱动知道这件事:它认为 VT-d 开着的时候,会在帧缓冲前后留出指向空白页的保护项(源码里的注释:VT-d may overfetch before/after the vma, so pad with scratch)。作为虚拟机运行时,它的判断是“认出了一种 hypervisor 就当 VT-d 开着”(i915_vtd_active)。fnOS 的内核没有认出 bhyve,启动日志里写着 Booting paravirtualized kernel on bare hardware,于是驱动按裸机处理,没有留保护项。
别家也碰到过同一件事。QEMU 在把核显交给虚拟机前把整张页表写成 0,注释写的是 to avoid spurious DMA faults。keelOS 的做法是在 bhyve 里把全部项(P340 上 1048576 项)指向虚拟机自己的一页空白内存:有效、在这台虚拟机的 VT-d 范围里、没有人往里写。改完以后 fnOS 的日志里一条 fault 都没有,hyp 也不再记到故障。
最后一步:转码程序找到的不是核显
vainfo 默认打开第一个渲染节点 /dev/dri/renderD128,结果去加载 virtio 显卡的驱动,失败。网页控制台用的那块模拟显卡先注册,占了 128,核显是 129。指定 129 以后,Intel iHD 驱动列出了 H.264、HEVC(含 10 位)的硬件编码和 H.264、HEVC、VP9 的解码。
让用户去挑渲染节点不是办法。现在带 Intel 核显的虚拟机,keel 自动把模拟的显示换成不占渲染节点的普通帧缓冲,核显就是第一个。这一条改动还没有在 P340 上验证。
结果
2026-10-02 晚,P340(i5-10505、8 GB、UHD 630)上的 fnOS 1.2.0701:“硬件加速”设置里认出了 Intel UHD Graphics 630;飞牛影视转码时,核显的频率从空闲的 350 MHz 升到 950 MHz。这是当时核显还是第二个渲染节点的版本上测的,fnOS 自己能按设备名找到它。
宿主唯一的显卡给了虚拟机
P340 只有一块显卡,给了 fnOS,宿主就没有屏幕了。Proxmox 这类平台的做法是接受这一点,宿主靠网页和 ssh 管理。keelOS 在这里多两件事:一件是必须先修的隐患,一件是还在做的改进。
隐患:hyp 一直在往屏幕上写。 keelOS 的宿主控制台画在一块影子帧缓冲上,hyp 每 40 毫秒把变化的部分拷到真屏幕,root 重启时的提示画面也是 hyp 画的。核显交给虚拟机以后,这些写入会经过核显的页表落进虚拟机的内存;虚拟机关机以后,可能落进宿主的任意内存。这个问题是上真机之前读代码读出来的。
修法是一条规则:谁要把屏幕所在的设备从宿主手里拿走,先告诉 hyp;hyp 从此不再写那块屏幕,直到机器下一次经固件启动。宿主的控制台照常在影子里运行,网页终端、ssh 和宿主截图都不受影响。P340 第一轮测试里这条路径就在真核显上走到了,内核日志是 the host's screen is on this device: from now on nothing is shown on it。
还有两个细节。登记了核显直通但虚拟机还没启动时,宿主的屏幕照常可用,到带核显的虚拟机第一次启动为止。hyp 只在经固件启动时才换新,如果正在运行的还是不认识这条规则的旧 hyp,直通驱动会拒绝接手屏幕所在的设备,并提示先整机重启。
改进:把屏幕接回来。 带核显的虚拟机关机以后,屏幕不会自己回到宿主:宿主这边没有核显驱动,只有固件能把显示重新设起来。我们做了一台显示小虚拟机:不改的 Linux 6.12 内核加 i915 驱动,镜像 10 MB,没有别的虚拟机用核显时由它接管,把宿主的画面原样画到显示器上。QEMU 里用一块模拟显卡走通了完整的切换(8 轮)。
在 P340 上它能用了:fnOS 关机后,显示器上回到宿主的登录界面,键盘可以登录,也能切到其他虚拟机的画面。第一版在 4K 显示器上一秒只刷几帧,查出两个原因。一是 FreeBSD 把直通设备的内存一律按“不缓存”映射给虚拟机,并且不理会虚拟机自己选的类型,显卡驱动要的 write-combining 不起作用,每写 4 个字节就是一次总线写;我们改了内核,直通设备的内存按虚拟机的选择来,和真机上一样。二是小虚拟机漏掉一帧就整屏重画,4K 一屏约 30 MB,画完又漏了下一帧;现在它只写真正变了的像素。改完以后,用户的感受是速度和以前 hyp 直接画屏差不多。
还留着一件事:早先在 P340 上它启动后几秒宿主自己复位过一次。这台机器没有串口,复位没有留下任何现场,原因没查到,之后也没有再出现。
顺带查出来的
两个和 fnOS 无关、但因为这次在 P340 上反复升级才发现的问题。
- 每次升级都整机重启。 keelOS 的卖点是升级宿主时虚拟机不关机,只暂停几十秒。但 P340 的升级日志每次都写着“带来新的 hyp,经固件重启”,而 hyp 的源码根本没改。原因是链接器把编译时间写进了文件头,同一份源码编两次差 1 个字节,升级程序比较文件时就认为 hyp 变了。现在 hyp 是可重复构建的,两次编译的文件完全相同。一次完整的“hyp 没变的升级”还没有在真机上验证。
- reload 以后宿主没有屏幕。 第一轮的镜像里,负责不经固件重启宿主的那个程序是从一份过期的源文件编的,新内核拿不到帧缓冲。P340 这种只有 UEFI 显示的机器,reload 后屏幕会停住。过期的文件已从仓库里拿掉,改成每次生成。
还有一条教训没有变成代码:没有串口的机器,宿主复位时什么都留不下。hyp 需要一个能把最后几行日志留过复位的办法。
现在的状态
一台 8 GB 的 P340 上,fnOS 作为虚拟机拿到了整块 NVMe 和核显,能存文件,能硬件转码。下面按“测过的”“没验证的”“没做完的”分开列。
在 P340 上测过的(2026-10-02):
- fnOS 1.2.0701 安装、启动、网页管理。
- NVMe 整块直通,fnOS 里建存储空间,直写 227 MB/s。
- 核显直通:i915 驱动正常加载,没有 fault;VA-API 可用;飞牛影视转码时核显在工作。
- fnOS 关机、再开机三次,宿主正常,hyp 没有再记到核显的故障。
- 显示小虚拟机:fnOS 关机后显示器回到宿主的画面,键盘可用;4K 显示器上的刷新速度和 hyp 直接画屏时相当。
改了但还没在真机上验证的:
- 带核显的虚拟机自动用普通帧缓冲,让核显成为第一个渲染节点。
- hyp 没变的升级不再整机重启。
- 物理显示器上的画面:测试是远程做的,驱动认到了接在 HDMI 上的显示器,但没有人看过屏幕。
没做完的:
- P340 上那一次宿主复位的原因(只出现过一次,没有现场)。
- 虚拟机开机阶段显示器上没有画面,系统的显卡驱动加载后才有。开机画面要主板固件里 Intel 的显示驱动,那是私有的二进制,我们不能随镜像分发。
- HDMI 的声音没做,Windows 虚拟机用核显没测。
- 只测了这一台机器、这一代核显(10 代 UHD 630)。11 代以后的核显代码上支持,没有机器测。
- 网卡显示半双工。
回到题目。这 14 项里,没有一项是 fnOS 做错了什么:它用的是标准的 Linux 内核和标准的 Intel 驱动,在物理机上一切正常。让一个没有为虚拟机做过任何准备的系统,在虚拟机里和在物理机上一样工作,是 hypervisor 的事。fnOS 帮我们把这条路上的坑一个一个踩了出来。