English日本語

文章

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 拒绝、修正后读到虚拟机里的一页空白内存
显示引擎取一帧的路径:正常的一帧、修正前读到没人写过的页表项被 VT-d 拒绝、修正后读到虚拟机里的一页空白内存

三行是同一条路径:显示引擎查核显的页表得到地址,再经 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 帮我们把这条路上的坑一个一个踩了出来。

← 文章 · 为什么选 keelOS