ZFS 启动环境详解
发布时间:2025/11/25
作者:𝚟𝚎𝚛𝚖𝚊𝚍𝚎𝚗
这篇文章不会尝试给出 ZFS 的通用解释——我们只会专注于 ZFS 启动环境。关于它们存在许多误解和误会——有些人不知道 ZFS 启动环境里面有什么、ZFS 启动环境里面没有什么——但更糟的是——有些人完全不懂。
我假设读者确实了解什么是 ZFS 文件系统的概念和特性 —— 比如 ZFS 池或 ZFS 数据集之类的东西对读者来说是已知的……并且读者能够解释 ZFS 和 LVM 之间的区别。
ZFS 启动环境里有什么
这是首先经常被误解的主题……大概是因为没有理解 canmount=off 这个 ZFS 属性 —— 你可以在 man zfsprops(7) 中获得更多相关内容。
如果该属性被设置为
off,则文件系统无法被挂载,并且会被zfs mount -a忽略。将该属性设置为off类似于将mountpoint属性设置为none,但数据集仍然具有正常的mountpoint属性,该属性可以被继承。将此属性设置为off允许数据集仅作为继承属性的机制使用。设置canmount=off的一个例子是创建两个具有相同挂载点的数据集,这样两个数据集的子数据集会显示在同一目录中,但可能具有不同的继承特性。当设置为
noauto时,数据集只能显式地挂载和卸载。数据集在创建或导入时不会自动挂载,也不会被zfs mount -a命令挂载,或被zfs unmount -a命令卸载。该属性不可继承。——整理者加
这是我以前关于 “ZFS 启动环境” 的演讲中的幻灯片 —— 可以在链接 https://is.gd/BECTL 获得 —— 那是在 2018 NLUUG 会议上做的……颜色并不是随便选的……它们是特地准备成那样,以致敬 NLUUG 会议举办的国家 —— 荷兰。

对 /var 和 /usr 使用 canmount=off 背后的想法是这样的:
/var 和 /usr 这些 ZFS 数据集的内容确实位于 ZFS 启动环境中 —— 位于安装程序创建的 zroot/ROOT/default 这个 ZFS 启动环境中 —— 而其余像下面这些这样的内容:
zroot/tmp
zroot/usr/home
zroot/usr/ports
zroot/usr/src
zroot/var/audit
zroot/var/crash
zroot/var/log
zroot/var/mail
zroot/var/tmp这些内容 被排除在这个 ZFS 启动环境之外。
因此,在默认情况下,一切都包含在 ZFS 启动环境中 —— 如果你想从 /usr 或 /var 中排除某些部分 —— 你就为这些需要排除的路径添加相应的 ZFS 数据集。
下面是你在 bsdinstall(8) 安装程序中选择 Auto(ZFS) 选项后,FreeBSD 默认的 ZFS 布局。
root@freebsd:~ # zfs set mountpoint=none zroot
root@freebsd:~ # zfs list
NAME USED AVAIL REFER MOUNTPOINT
zroot 385M 18.5G 96K none
zroot/ROOT 383M 18.5G 96K none
zroot/ROOT/default 383M 18.5G 383M /
zroot/home 96K 18.5G 96K /home
zroot/tmp 96K 18.5G 96K /tmp
zroot/usr 288K 18.5G 96K /usr
zroot/usr/ports 96K 18.5G 96K /usr/ports
zroot/usr/src 96K 18.5G 96K /usr/src
zroot/var 600K 18.5G 96K /var
zroot/var/audit 96K 18.5G 96K /var/audit
zroot/var/crash 96K 18.5G 96K /var/crash
zroot/var/log 120K 18.5G 120K /var/log
zroot/var/mail 96K 18.5G 96K /var/mail
zroot/var/tmp 96K 18.5G 96K /var/tmp
root@freebsd:~ # zfs get canmount
NAME PROPERTY VALUE SOURCE
zroot canmount on default
zroot/ROOT canmount on default
zroot/ROOT/default canmount noauto local
zroot/home canmount on default
zroot/tmp canmount on default
zroot/usr canmount off local
zroot/usr/ports canmount on default
zroot/usr/src canmount on default
zroot/var canmount off local
zroot/var/audit canmount on default
zroot/var/crash canmount on default
zroot/var/log canmount on default
zroot/var/mail canmount on default
zroot/var/tmp canmount on default关键点在于,zroot/usr 和 zroot/var 这两个 ZFS 数据集都具有 canmount=off 这个 ZFS 属性。这意味着 /usr 文件系统 并不存放在 zroot/usr 这个 ZFS 数据集中……它是存放在 zroot/ROOT/default 这个 ZFS 启动环境里的。
你可以用传统的 df(1) 命令进行验证如下。
root@freebsd:~ # df -g /usr
Filesystem 1G-blocks Used Avail Capacity Mounted on
zroot/ROOT/default 18 0 18 2% /
root@freebsd:~ # df -g /var
Filesystem 1G-blocks Used Avail Capacity Mounted on
zroot/ROOT/default 18 0 18 2% /如你所见,/usr 和 /var 都只是 zroot/ROOT/default 这个 ZFS 数据集中的目录。数据并 不 保存在 zroot/usr 或 zroot/var 这些 ZFS 数据集中……再次强调,这是因为它们具有 canmount=off 这个 ZFS 属性。
现在,这个 FreeBSD 默认设置的主要理念 / 概念是:所有内容都保存在 zroot/ROOT/default 这个 ZFS 数据集中,而所有其他 ZFS 数据集都是从它中 排除(exclude) 出去的。比如 zroot/var/audit 具有 canmount=on 这个 ZFS 属性……其他所有的也都是完全相同的情况。
说实话,为了让事情变得傻瓜式地简单,我会把 zroot/usr 和 zroot/var 这两个 ZFS 数据集换成一个能准确说明用途的名称 —— zroot/exclude,因为在它下面的所有 ZFS 数据集,本质上都是被排除在 ZFS 启动环境 之外的。
root@freebsd:~ # zfs list
NAME USED AVAIL REFER MOUNTPOINT
zroot 385M 18.5G 96K none
zroot/ROOT 383M 18.5G 96K none
zroot/ROOT/default 383M 18.5G 383M /
zroot/home 96K 18.5G 96K /home
zroot/tmp 96K 18.5G 96K /tmp
zroot/usr 288K 18.5G 96K /usr
zroot/usr/ports 96K 18.5G 96K /usr/ports
zroot/usr/src 96K 18.5G 96K /usr/src
zroot/var 600K 18.5G 96K /var
zroot/var/audit 96K 18.5G 96K /var/audit
zroot/var/crash 96K 18.5G 96K /var/crash
zroot/var/log 120K 18.5G 120K /var/log
zroot/var/mail 96K 18.5G 96K /var/mail
zroot/var/tmp 96K 18.5G 96K /var/tmp
root@freebsd:~ # zfs create -o canmount=off -o mountpoint=none zroot/exclude
root@freebsd:~ # zfs rename -u zroot/usr zroot/exclude/usr
root@freebsd:~ # zfs rename -u zroot/var zroot/exclude/var
root@freebsd:~ # zfs rename -u zroot/home zroot/exclude/home
root@freebsd:~ # zfs rename -u zroot/tmp zroot/exclude/tmp
root@freebsd:~ # zfs list
NAME USED AVAIL REFER MOUNTPOINT
zroot 385M 18.5G 96K none
zroot/ROOT 383M 18.5G 96K none
zroot/ROOT/default 383M 18.5G 383M /
zroot/exclude 1.16M 18.5G 96K none
zroot/exclude/home 96K 18.5G 96K /home
zroot/exclude/tmp 96K 18.5G 96K /tmp
zroot/exclude/usr 288K 18.5G 96K /usr
zroot/exclude/usr/ports 96K 18.5G 96K /usr/ports
zroot/exclude/usr/src 96K 18.5G 96K /usr/src
zroot/exclude/var 612K 18.5G 96K /var
zroot/exclude/var/audit 96K 18.5G 96K /var/audit
zroot/exclude/var/crash 96K 18.5G 96K /var/crash
zroot/exclude/var/log 132K 18.5G 132K /var/log
zroot/exclude/var/mail 96K 18.5G 96K /var/mail
zroot/exclude/var/tmp 96K 18.5G 96K /var/tmp现在更容易理解了吗?我希望是的……即使在上面进行了所有这些重命名,ZFS 的挂载点也 没有任何变化,因为 mountpoint 是个独立的 ZFS 属性——所以即使我们对 ZFS 数据集的命名逻辑进行了重新组织,只要没有从上层继承,这个 ZFS mountpoint 属性在逻辑重组后依然保持不变。
之前使用 df(1) 的测试结果与之前完全相同。
root@freebsd:~ # df -g /usr
Filesystem 1G-blocks Used Avail Capacity Mounted on
zroot/ROOT/default 18 0 18 2% /
root@freebsd:~ # df -g /var
Filesystem 1G-blocks Used Avail Capacity Mounted on
zroot/ROOT/default 18 0 18 2% //usr 和 /var 的数据都保存在 default ZFS 启动环境内。
我们甚至可以移除 /usr 和 /var 的挂载点,这样会更加清晰。
root@freebsd:~ # zfs set -u mountpoint=/var/tmp zroot/exclude/var/tmp
root@freebsd:~ # zfs set -u mountpoint=/var/mail zroot/exclude/var/mail
root@freebsd:~ # zfs set -u mountpoint=/var/log zroot/exclude/var/log
root@freebsd:~ # zfs set -u mountpoint=/var/crash zroot/exclude/var/crash
root@freebsd:~ # zfs set -u mountpoint=/var/audit zroot/exclude/var/audit
root@freebsd:~ # zfs set -u mountpoint=none zroot/exclude/var
root@freebsd:~ # zfs set -u mountpoint=/usr/src zroot/exclude/usr/src
root@freebsd:~ # zfs set -u mountpoint=/usr/ports zroot/exclude/usr/ports
root@freebsd:~ # zfs set -u mountpoint=none zroot/exclude/usr
root@freebsd:~ # zfs list
NAME USED AVAIL REFER MOUNTPOINT
zroot 385M 18.5G 96K none
zroot/ROOT 383M 18.5G 96K none
zroot/ROOT/default 383M 18.5G 383M /
zroot/exclude 1.16M 18.5G 96K none
zroot/exclude/home 96K 18.5G 96K /home
zroot/exclude/tmp 96K 18.5G 96K /tmp
zroot/exclude/usr 288K 18.5G 96K none
zroot/exclude/usr/ports 96K 18.5G 96K /usr/ports
zroot/exclude/usr/src 96K 18.5G 96K /usr/src
zroot/exclude/var 612K 18.5G 96K none
zroot/exclude/var/audit 96K 18.5G 96K /var/audit
zroot/exclude/var/crash 96K 18.5G 96K /var/crash
zroot/exclude/var/log 132K 18.5G 132K /var/log
zroot/exclude/var/mail 96K 18.5G 96K /var/mail
zroot/exclude/var/tmp 96K 18.5G 96K /var/tmp
root@freebsd:~ # df -g
Filesystem 1G-blocks Used Avail Capacity Mounted on
/dev/gpt/efiboot0 0 0 0 0% /boot/efi
devfs 0 0 0 0% /dev
zroot/ROOT/default 18 0 18 2% /
zroot/exclude/tmp 18 0 18 0% /tmp
zroot/exclude/home 18 0 18 0% /home
zroot/exclude/var/log 18 0 18 0% /var/log
zroot/exclude/var/tmp 18 0 18 0% /var/tmp
zroot/exclude/var/mail 18 0 18 0% /var/mail
zroot/exclude/var/crash 18 0 18 0% /var/crash
zroot/exclude/var/audit 18 0 18 0% /var/audit
zroot/exclude/usr/src 18 0 18 0% /usr/src
zroot/exclude/usr/ports 18 0 18 0% /usr/ports够清楚了吗?
新建 ZFS 启动环境时会发生什么
另一个经常被误解的谜题……或者说没有被清楚理解的部分。
通常,ZFS 启动环境 是由某个时间点创建的 ZFS 快照 克隆出来的可写 ZFS clone。
让我用我最喜欢的 Enterprise Architect ASCII Edition 软件来可视化一下。
|
ZFS DATASET | ZFS MOUNTPOINT => WHY
|
+-------------------+ |
| ZFS 'zroot' pool | | /sys => # zfs set mountpoint=/sys sys
| (dataset) | | canmount:on => # zfs set canmount=on sys
+---------+---------+ |
| |
+-------+-------+ |
| ROOT | | (none) => # zfs set mountpoint=none sys/ROOT
| (dataset) | | canmount:on => # zfs set canmount=on sys/ROOT
+-------+-------+ |
| |
+-----+-----+ |
| default | | / => # zfs set mountpoint=/ sys/ROOT/${DATASET}
| (dataset) | | canmount:noauto => # zfs set canmount=noauto sys/ROOT/${DATASET}
+--+-----------+ |
| |
+- @2025-11-11@10:10 | point-in-time => # zfs snapshot sys/ROOT/${DATASET}@2025-11-11@10:10
| (snapshot) | (read only) # beadm create sys/ROOT/${DATASET}@2025-11-11@10:10
| |
+- safe | clone => # zfs clone sys/ROOT/${DATASET}@2025-11-11@10:10 sys/ROOT/safe
| (clone) | (writable) # beadm create -e default@2025-11-11@10:10 safe
| |
+- test | clone => # zfs clone sys/ROOT/${DATASET}@2025-11-11@10:10 sys/ROOT/test
(clone) | (writable) # beadm create -e default@2025-11-11@10:10 test
|关于上面的 ASCII 图,beadm(8) 命令的输出将显示类似的信息。
root@freebsd:~ # beadm list
BE Active Mountpoint Space Created
default NR / 24.0G 2025-10-08 01:42
safe - - 1.3G 2025-06-10 09:47
test - - 6.4G 2025-08-22 23:02
root@freebsd:~ # zfs list -r -t all zroot/ROOT
NAME USED AVAIL REFER MOUNTPOINT
zroot/ROOT 29.8G 154G 96K none
zroot/ROOT/default 748K 154G 19.5G /
zroot/ROOT/default@2025-11-11@10:10 16.1G - 24.2G -
zroot/ROOT/safe 8K 154G 20.5G /
zroot/ROOT/test 810M 154G 20.9G /
root@freebsd:~ # beadm list -a
BE/Dataset/Snapshot Active Mountpoint Space Created
default
zroot/ROOT/default NR / 24.0G 2025-10-08 01:42
safe
zroot/ROOT/safe - - 748.0K 2025-06-10 09:47
default@2025-11-11@10:10 - - 1.3G 2025-10-08 01:42
test
zroot/ROOT/test - - 810.0M 2025-08-22 23:02
default@2025-11-11@10:10 - - 5.6G 2025-08-22 23:02
root@freebsd:~ # zfs get origin zroot/ROOT/default
NAME PROPERTY VALUE SOURCE
zroot/ROOT/default origin - -
root@freebsd:~ # zfs get origin zroot/ROOT/safe
NAME PROPERTY VALUE SOURCE
zroot/ROOT/safe origin zroot/ROOT/default@2025-11-11@10:10 -
root@freebsd:~ # zfs get origin zroot/ROOT/test
NAME PROPERTY VALUE SOURCE
zroot/ROOT/test origin zroot/ROOT/default@2025-11-11@10:10 -
root@freebsd:~ # zpool get bootfs
NAME PROPERTY VALUE SOURCE
zdata bootfs - default
zroot bootfs zroot/ROOT/default local现在……有趣的部分来了——你可以创建任意数量的额外 ZFS 启动环境,或者使用 ZFS 的 send|recv 功能。
下面是这种设置的示例。
|
ZFS DATASET | MOUNTPOINT => WHY
|
+-----------------------+ |
| ZFS 'zroot' pool | | /sys => # zfs set mountpoint=/sys sys
| (ZFS dataset) | | canmount:on => # zfs set canmount=on sys
+-----------+-----------+ |
| |
+---------+---------+ |
| ROOT | | (none) => # zfs set mountpoint=none sys/ROOT
| (ZFS dataset) | | canmount:on => # zfs set canmount=on sys/ROOT
+---------+---------+ |
| |
+--------------+----------+ |
| | | |
+----+----+ +-------+---+ +----+----+ |
| default | | 15.0-RC1 | | 12.2 | | / => # zfs set mountpoint=/ sys/ROOT/${DATASET}
|(dataset)| | (dataset) | |(dataset)| | canmount:noauto => # zfs set canmount=noauto sys/ROOT/${DATASET}
+---------+ +-----------+ +---------+ |
|……并且你可以从每一个这些 ZFS dataset 创建额外的 ZFS 启动环境。
在系统之间传输 ZFS 启动环境
……而且这些系统可以是 任意 系统。你可以在笔记本之间传输 ZFS 启动环境,或者从笔记本传到服务器……甚至从 Bhyve VM 传到服务器……随你喜欢。
下面你将看到一些示例,展示如何将现有的 ZFS 启动环境通过网络从一个 FreeBSD UNIX 系统复制到另一个系统。
这里的 mbuffer(1) 并非关键——它只是通过确保随时有数据可发送来加快传输过程。
root@freebsd:~ # beadm export 14.3 | mbuffer | ssh 10.26 doas beadm import 14.3.w520
summary: 30.3 GiByte in 34min 04.8sec - average of 15.2 MiB/s上面演示的是将我在 ThinkPad W520 上的 14.3 ZFS 启动环境发送到 ThinkPad T14 系统——这也是为什么我想在另一端将其命名为 14.3.w520,以确保我记得它的来源。
你也可以像我在文章 Other FreeBSD Version in ZFS Boot Environment 中描述的那样,从零创建新的 ZFS 启动环境。
总结
我希望现在 ZFS 启动环境 的主要原理更加清晰了。
……如果还有不明白的地方,欢迎提出你的疑问。
最后更新于
这有帮助吗?