深入探讨Btrfs subvolume工作原理

126 阅读9分钟

介绍

子卷允许将 Btrfs 文件系统划分为单独的子文件系统。这意味着您可以从 Btrfs 文件系统挂载子卷,就好像它们是独立的文件系统一样。此外,您还可以通过 qgroups 定义子卷可能占用的最大空间(我们将在本系列的另一篇文章中讨论这个问题),或者使用子卷专门在快照中包含或排除文件(我们也将在本系列的另一篇文章中讨论这个问题)。自 Fedora Linux 33 以来,每个默认的 Fedora Workstation 和 Fedora Silverblue 安装都使用子卷。在本文中,我们将探讨它的工作原理。

下面您将看到许多与子卷相关的示例。如果您想继续操作,您必须有权访问某些 Btrfs 文件系统和 root 访问权限。您可以通过以下命令验证您的/home/目录是否为 Btrfs:

$ findmnt -no FSTYPE /home
btrfs

此命令将输出/home/目录的文件系统名称。如果它显示btrfs,则一切就绪。让我们创建一个新目录来执行一些实验:

$ mkdir ~/btrfs-subvolume-test
$ cd ~/btrfs-subvolume-test

在下面的文本中,您将在框中找到许多命令输出,如上所示。

创建并使用子卷

我们可以使用以下命令创建 Btrfs 子卷:

$ sudo btrfs subvolume create first
Create subvolume './first'

当我们检查当前目录时,我们将看到它现在有一个名为first 新文件夹。

$ ls -l
total 0
drwxr-xr-x. 1 root root 0 Oct 15 18:09 first

我们可以像处理任何常规文件夹一样处理它:我们可以重命名它,移动它,在里面创建新的文件和文件夹等。请注意,该文件夹属于 root,因此我们必须以 root 身份执行这些操作。

如果它的行为和外观都像文件夹,我们如何知道它是否是 Btrfs 子卷?我们可以使用_btrfs_工具列出所有子卷:

$ sudo btrfs subvolume list .
ID 256 gen 30 top level 5 path home
ID 257 gen 30 top level 5 path root
ID 258 gen 25 top level 257 path root/var/lib/machines
ID 259 gen 29 top level 256 path hartan/btrfs-subvolume-test/first

如果您使用的是最近安装的、未修改过的 Fedora Linux,您可能会看到与上面相同的输出。稍后我们将检查_home_和_root_以及所有数字的含义。现在,我们看到我们指定的路径上有一个子卷。我们可以将输出限制为当前位置下的子卷:

$ sudo btrfs subvolume list -o .
ID 259 gen 29 top level 256 path home/hartan/btrfs-subvolume-test/first

让我们重命名子卷:

$ sudo mv first second
$ sudo btrfs subvolume list -o .
ID 259 gen 29 top level 256 path home/hartan/btrfs-subvolume-test/second

我们还可以嵌套子卷:

$ sudo btrfs subvolume create second/third
Create subvolume 'second/third'
$ sudo btrfs subvolume list .
ID 256 gen 34 top level 5 path home
ID 257 gen 37 top level 5 path root
ID 258 gen 25 top level 257 path root/var/lib/machines
ID 259 gen 37 top level 256 path hartan/btrfs-subvolume-test/second
ID 260 gen 37 top level 259 path hartan/btrfs-subvolume-test/second/third

我们也可以删除子卷,就像删除文件夹一样:

$ sudo rm -r second/third

或者通过特殊的 Btrfs 命令:

$ sudo btrfs subvolume delete second
Delete subvolume (no-commit): '/home/hartan/btrfs-subvolume-test/second'

介绍中提到,Btrfs 子卷的作用类似于单独的文件系统

这意味着我们可以挂载子卷并向其传递一些挂载选项。首先,我们将创建一个小文件夹结构,以更好地了解发生了什么:

$ mkdir -p a a/1 a/1/b
$ sudo btrfs subvolume create a/2
Create subvolume 'a/2'
$ sudo touch a/1/c a/1/b/d a/2/e

其结构如下:

$ tree
.
└── a
    ├── 1
    │   ├── b
    │   │   └── d
    │   └── c
    └── 2
        └── e

4 directories, 3 files

验证现在是否有新的 Btrfs 子卷:

$ sudo btrfs subvolume list -o .
ID 261 gen 41 top level 256 path home/hartan/btrfs-subvolume-test/a/2

要挂载子卷,我们必须知道 Btrfs 文件系统子卷所在的块设备的路径。以下命令告诉我们:

$ findmnt -vno SOURCE /home/
/dev/vda3

现在我们可以挂载子卷了。请确保将参数替换为您的 PC 的值:

$ sudo mount -o subvol=home/hartan/btrfs-subvolume-test/a/2 /dev/vda3 a/1/b

请注意,我们使用-o标志为 mount 程序提供附加选项。在本例中,我们告诉它从设备_/dev/vda3上的 btrfs 文件系统挂载名为home/hartan/btrfs-subvolume-test/a/2 的子卷。这是 Btrfs 特定的选项,在其他文件系统中不可用。

我们看到目录结构发生了变化:

$ tree
.
└── a
    ├── 1
    │   ├── b
    │   │   └── e
    │   └── c
    └── 2
        └── e

4 directories, 3 files

请注意,文件_e_现在存在两次,而_d_已消失。我们现在可以通过两个不同的路径访问同一个 Btrfs 子卷。我们在任一路径中执行的所有更改都会立即反映在所有其他位置:

$ sudo touch a/1/b/x
$ ls -lA a/2
total 0
-rw-r--r--. 1 root root 0 Oct 15 18:14 e
-rw-r--r--. 1 root root 0 Oct 15 18:16 x

让我们再多玩一下挂载选项。例如,我们可以像这样将子卷挂载为_a/1/b_下的只读卷(插入您的 PC 的参数!):

$ sudo umount a/1/b
$ sudo mount -o subvol=home/hartan/btrfs-subvolume-test/a/2,ro /dev/vda3 a/1/b

我们使用与上面相同的命令,只是在末尾添加了_ro 。现在我们不能再通过此挂载创建文件了:_

$ sudo touch a/1/b/y
touch: cannot touch 'a/1/b/y': Read-only file system

但直接访问子卷仍然像以前一样有效:

$ sudo touch a/2/y
$ tree
.
└── a
    ├── 1
    │   ├── b
    │   │   ├── e
    │   │   ├── x
    │   │   └── y
    │   └── c
    └── 2
        ├── e
        ├── x
        └── y

4 directories, 7 files

在我们继续之前不要忘记清理一下:

$ sudo rm -rf a
rm: cannot remove 'a/1/b/e': Read-only file system
rm: cannot remove 'a/1/b/x': Read-only file system
rm: cannot remove 'a/1/b/y': Read-only file system

哦不,发生了什么?好吧,由于我们上面以只读方式_挂载了子卷,因此我们无法删除它。从文件系统的角度来看,删除是一种写入操作:要删除**a/1/b/e,**我们需要从其父目录(在本例中为 a/1/b)的目录内容中删除__e的目录条目。换句话说,我们必须_写入a/1/b以告诉它_e_不再存在。因此,我们首先再次卸载子卷,然后删除文件夹:

$ sudo umount a/1/b
$ sudo rm -rf a
$ tree
.

0 directories, 0 files

子卷 ID

还记得subvolume list 子命令的第一个输出吗?它包含很多数字,让我们看看它们到底是什么。我将输出复制到此处以便再次查看:

ID 256 gen 30 top level 5 path home
ID 257 gen 30 top level 5 path root
ID 258 gen 25 top level 257 path root/var/lib/machines
ID 259 gen 29 top level 256 path hartan/btrfs-subvolume-test/first

我们看到有三列数字,每列都以几个字母作为前缀来描述它们的作用。第一列数字是子卷 ID。子卷 ID 在 Btrfs 文件系统中是唯一的,因此可以唯一地标识子卷。这意味着名为home的子卷也可以通过其 ID 256来引用。在上面的 mount 命令中,我们写道:

$ sudo mount -o subvol=hartan/...

另一个完全合法的选择是使用子卷 ID:

$ sudo mount -o subvolid=...

子卷 ID 从256开始,每创建 1 个子卷,子卷 ID 增加 1。但是有一个例外:文件系统根目录始终具有子卷名称_/_和子卷 ID 5。没错,即使是 Btrfs 文件系统的根目录在技术上也是一个子卷。这只是隐式已知的,因此它不会显示在_btrfs subvolume list的输出中。如果您在没有subvol或subvolid参数的情况下挂载 Btrfs 文件系统,则subvolid=5的根子卷将被假定为默认值。下面我们将看到一个示例,说明何时可能需要明确挂载文件系统根目录。

第二列数字是生成计数器,每次 Btrfs 事务都会增加。这主要是内部计数器,这里不再进一步讨论。

最后,第三列数字是子卷_父卷_的子卷 ID 。在上面的输出中,我们看到子卷ome和root 的父子卷 ID 都是 5。请记住,ID 5 具有特殊含义:它是文件系统根。因此我们知道home和root是根子卷的子卷。另一方面,hartan/btrfs-subvolume-test/first是 ID 为 256 的子卷的子卷,在我们的例子中是home。

在下一节中,我们将了解子卷root和home来自哪里。

检查 Fedora Linux 中的默认子卷

当您从头开始创建新的 Btrfs 文件系统时,其中不会有任何子卷(当然,根子卷除外)。那么Fedora Linux 中的home子卷和root子卷来自哪里?

这些是由安装程序在安装时创建的。传统安装通常会为/和/home_目录包含一个单独的文件系统分区。在启动期间,这些分区会被适当地挂载以组装成一个完整的文件系统。但这种方法存在一个问题:除非您使用lvm之类的技术,否则很难在将来的某个时间点更改分区大小。因此,您可能会遇到这样的情况:您的/或/home空间不足,而相应的另一个分区则有大量未使用的可用空间。

由于 Btrfs 子卷都是同一文件系统的一部分,因此它们将共享底层文件系统提供的空间。还记得我们上面创建子卷的时候吗?我们从未告诉 Btrfs 它们有多大:子卷可以占用文件系统的所有空间,默认情况下没有什么可以阻止它这样做。但是,我们可以通过 Btrfs qgroups 动态施加大小限制,也可以在运行时修改这些限制(我们将在本系列的后续文章中了解如何修改)。

分离/和/home的另一个好处是,我们可以单独拍摄快照。子卷是快照的边界,快照永远不会包含拍摄快照的子卷下方的其他子卷的内容。本系列的下一篇文章将详细介绍快照。

理论讲得够多了!让我们看看这到底是怎么回事。首先确保您的根文件系统确实是 Btrfs 类型:

$ findmnt -no FSTYPE /
btrfs

然后获取其所在的分区:

$ findmnt -vno SOURCE /
/dev/vda3

记住,我们可以通过其特殊子卷 ID 5 来挂载文件系统根(调整文件系统分区!):

$ mkdir fedora-rootsubvol
$ sudo mount -o subvolid=5 /dev/vda3 ./fedora-rootsubvol
$ ls fedora-rootsubvol/
home  root

还有我们安装的 Fedora Linux 的子卷!但是 Fedora Linux 如何知道子卷root属于/,而home属于/home?

文件/etc/fstab包含有关文件系统的所谓静态信息。简单来说,在启动过程中,系统会逐行读取此文件,并挂载其中列出的所有文件系统。在我的系统上,该文件如下所示:

$ cat /etc/fstab
# [ ... ]
# /etc/fstab
# Created by anaconda on Sat Oct 15 12:01:57 2022
# [ ... ]
#
UUID=5e4e42bb-4f2f-4f0e-895f-d1a46ea47807 /                       btrfs   subvol=root,compress=zstd:1 0 0
UUID=e3a798a8-b8f2-40ca-9da7-5e292a6412aa /boot                   ext4    defaults        1 2
UUID=5e4e42bb-4f2f-4f0e-895f-d1a46ea47807 /home                   btrfs   subvol=home,compress=zstd:1 0 0

(请注意,上面的“UUID”行已换行成两行)

每行开头的 UUID 只是一种识别系统中磁盘和文件系统分区的方法(大致相当于我_上面使用的/dev/vda3_)。第二列是文件系统树中应挂载此文件系统的路径。第三列是文件系统类型。我们看到/和/home 的条目类型为btrfs,这正是我们所期望的!最后,在第四列中,我们看到了神奇之处:这些是挂载选项,它说使用选项_subvol=root挂载/。这正是我们在_btrfs subvolume list /的输出中看到的子卷!

利用这些信息,我们可以重建创建此文件系统条目的mount调用:

$ sudo mount -o subvol=root,compress=zstd:1 UUID=5e4e42bb-4f2f-4f0e-895f-d1a46ea47807 /

这就是 Fedora Linux 使用 Btrfs 子卷的方式!如果你好奇为什么 Fedora Linux 决定使用 Btrfs 作为默认文件系统,请参阅下面链接的变更提案[1]

有关 Btrfs 子卷的更多信息

Btrfs wiki 上有关于子卷的更多信息,最重要的是关于可以应用于 Btrfs 子卷的挂载选项。某些选项(如压缩)只能在文件系统范围级别应用,因此会影响 Btrfs 文件系统的所有子卷。您可以在下面找到链接的条目[2]

如果您觉得难以区分哪些目录是普通目录,哪些是子卷,您可以随意为子卷采用特殊的命名约定。例如,您可以在子卷名称前加上“@”,以便于区分。

现在您知道子卷的行为类似于文件系统,有人可能会问如何最好地将子卷放置在某个位置。假设您想要一个位于~/games下的 Btrfs 子卷,其中您的主目录 ( ~ ) 本身就是一个子卷,您该如何实现呢?根据上述示例,您可以使用类似sudo btrfs subvolume create ~/games 的命令。这样,您就可以创建所谓的嵌套子卷:在您的子卷~中,现在有一个子卷games。这是解决这种情况的完美方法。

另一个有效的解决方案是执行 Fedora 默认的操作:在根子卷下创建所有子卷(即其父子卷 ID 为 5),并将它们挂载到适当的位置。Btrfs wiki 概述了这些方法,并简要讨论了它们对文件系统管理的各自影响[5]

结论

在本文中,我们了解了 Btrfs 子卷,它们的作用类似于 Btrfs 文件系统中的独立 Btrfs 文件系统。我们学习了如何创建、挂载和删除子卷。最后,我们探索了 Fedora Linux 如何使用子卷——而我们却完全没有注意到。

本系列的下一篇文章将讨论:

  • 快照
  • 压缩——透明地节省存储空间
  • Qgroups – 限制文件系统大小
  • RAID – 替换 mdadm 配置

如果您想了解更多与 Btrfs 相关的其他主题,请查看 Btrfs Wiki [3]和文档[4]。如果您还没有看过本系列的第一篇文章,请不要忘记查看!如果您觉得本系列文章中缺少某些内容,请在下面的评论中告诉我们。下一篇文章再见!