什么是文件系统? 根据早期 Linux 贡献者和作者 Robert Love 的说法:“文件系统是数据的分层存储,遵循特定的结构。” 但是,此描述同样适用于 VFAT(虚拟文件分配表)、Git 和 Cassandra(一个 NoSQL 数据库)。那么,什么区分了文件系统?
文件系统基础
Linux 内核要求,一个实体要成为文件系统,它还必须在与它们关联的名称的持久对象上实现 open()、read() 和 write() 方法。 从 面向对象编程 的角度来看,内核将通用文件系统视为抽象接口,而这三个主要函数是“虚拟的”,没有默认定义。 因此,内核的默认文件系统实现称为虚拟文件系统 (VFS)。

VFS 是 Unix 类系统中“一切皆文件”这一著名观察的基础。 考虑一下,上面的小演示,其中包含字符设备 * /dev/console * 实际上有效,是多么奇怪。 该图像显示了虚拟电传打字机 (tty) 上的交互式 Bash 会话。 将字符串发送到虚拟控制台设备会使其显示在虚拟屏幕上。 VFS 具有其他甚至更奇怪的属性。 例如,可以在其中寻找。
像 ext4、NFS 和 /proc 这样的熟悉的文件系统都在 C 语言数据结构中提供了这三个主要函数的定义,称为 file_operations。 此外,特定的文件系统以熟悉的面向对象的方式扩展和覆盖 VFS 函数。 正如 Robert Love 指出的那样,VFS 的抽象使 Linux 用户可以随意地将文件复制到外来操作系统或管道之类的抽象实体,而无需担心其内部数据格式。 代表用户空间,通过系统调用,进程可以使用一个文件系统的 read() 方法从文件中复制到内核的数据结构中,然后使用另一种文件系统的 write() 方法输出数据。
属于 VFS 基类型本身的功能定义位于内核源代码的 fs/*.c 文件中,而 fs/ 的子目录包含特定的文件系统。 内核还包含文件系统之类的实体,例如 cgroups、/dev 和 tmpfs,这些实体在引导过程的早期需要,因此在内核的 init/ 子目录中定义。 请注意,cgroups、/dev 和 tmpfs 不调用 file_operations 这三个主要函数,而是直接从内存读取和写入内存。
下图大致说明了用户空间如何访问通常挂载在 Linux 系统上的各种类型的文件系统。 未显示的是诸如管道、dmesg 和 POSIX 时钟之类的结构,它们也实现了 struct file_operations,因此它们的访问也通过 VFS 层。

VFS 的存在促进了代码重用,因为与文件系统关联的基本方法不必由每种文件系统类型重新实现。 代码重用是一种广泛接受的软件工程最佳实践! 可惜的是,如果重用的代码 引入了严重的错误,那么所有继承通用方法的实现都会受到影响。
/tmp:一个简单的技巧
查找系统上存在哪些 VFS 的一种简单方法是键入 mount | grep -v sd | grep -v :/,这将列出大多数计算机上所有未驻留在磁盘上且不是 NFS 的已挂载文件系统。 列出的 VFS 挂载之一肯定会是 /tmp,对吗?

为什么不建议将 /tmp 保存在存储中? 因为 /tmp 中的文件是临时的 (!),并且存储设备比创建 tmpfs 的内存慢。 此外,与内存相比,物理设备更容易受到频繁写入造成的磨损。 最后,/tmp 中的文件可能包含敏感信息,因此在每次重新启动时都使它们消失是一项功能。
不幸的是,某些 Linux 发行版的安装脚本仍然默认在存储设备上创建 /tmp。 如果你的系统是这种情况,请不要绝望。 请按照始终优秀的 Arch Wiki 上的简单说明来解决此问题,请记住,分配给 tmpfs 的内存不能用于其他目的。 换句话说,具有巨大的 tmpfs 且其中包含大型文件的系统可能会耗尽内存并崩溃。 另一个提示:编辑 /etc/fstab 文件时,请确保以换行符结尾,否则你的系统将无法启动。 (猜猜我是怎么知道的。)
/proc 和 /sys
除了 /tmp 之外,大多数 Linux 用户最熟悉的 VFS 是 /proc 和 /sys。 (/dev 依赖于共享内存,没有 file_operations)。 为什么有两种风格? 让我们更详细地看一下。
procfs 为用户空间提供了内核及其控制的进程的瞬时状态的快照。 在 /proc 中,内核发布有关其提供的设施的信息,例如中断、虚拟内存和调度程序。 此外,/proc/sys 是用户空间可以通过 sysctl 命令 配置的设置的地方。 有关单个进程的状态和统计信息在 /proc/<PID> 目录中报告。

/proc 文件的行为说明了 VFS 与磁盘上的文件系统有多么不同。 一方面,/proc/meminfo 包含命令 free 提供的信息。 另一方面,它也是空的! 怎么会这样? 这种情况让人想起康奈尔大学物理学家 N. David Mermin 于 1985 年撰写的一篇著名文章,题为“当没有人看月亮时,月亮还在那里吗? 现实与量子理论。” 事实是,当进程从 /proc 请求时,内核会收集有关内存的统计信息,并且实际上,当没有人看时,/proc 中的文件中什么也没有。 正如 Mermin 所说,“一个基本的量子原则是,测量通常不会揭示被测量属性的先前存在的值。” (关于月亮的问题的答案留作练习。)

procfs 的明显空虚是有道理的,因为那里可用的信息是动态的。 sysfs 的情况有所不同。 让我们比较一下 /proc 与 /sys 中至少一个字节大小的文件数量。

Procfs 只有一个,即导出的内核配置,这是一个例外,因为它每个引导周期只需要生成一次。 另一方面,/sys 有很多较大的文件,其中大多数包含一页内存。 通常,sysfs 文件仅包含一个数字或字符串,这与通过读取 /proc/meminfo 之类的文件生成的表格信息相反。
sysfs 的目的是将内核称为“kobject”的可读和可写属性公开给用户空间。 kobject 的唯一目的是引用计数:当对 kobject 的最后一个引用被删除时,系统将回收与其关联的资源。 然而,/sys 构成了内核著名的“用户空间的稳定 ABI”的大部分,任何人不得在任何情况下“破坏”它。 这并不意味着 sysfs 中的文件是静态的,这将与易失对象的引用计数相反。
内核的稳定 ABI 而是约束可以出现在 /sys 中的内容,而不是在任何给定时刻实际存在的内容。 列出 sysfs 中文件的权限可以了解如何设置或读取设备、模块、文件系统等的配置、可调参数。 逻辑迫使人们得出结论,procfs 也是内核稳定 ABI 的一部分,尽管内核的 文档 没有明确说明。

使用 eBPF 和 bcc 工具侦听 VFS
学习内核如何管理 sysfs 文件的最简单方法是观察它的实际运行,而在 ARM64 或 x86_64 上观察的最简单方法是使用 eBPF。 eBPF(扩展的 Berkeley 数据包过滤器)由一个 在内核中运行的虚拟机 组成,特权用户可以从命令行查询它。 内核源代码告诉读者内核可以做什么; 在启动的系统上运行 eBPF 工具则展示了内核实际在做什么。
幸运的是,通过 bcc 工具可以很容易地开始使用 eBPF。 bcc 工具可以作为 主要 Linux 发行版的软件包 获取,并且 Brendan Gregg 已经对其进行了充分的记录。 bcc 工具是带有嵌入式 C 代码片段的 Python 脚本,这意味着任何熟悉这两种语言的人都可以轻松地修改它们。 目前,bcc/tools 中有 80 个 Python 脚本,因此系统管理员或开发人员很可能找到与他们需求相关的脚本。
要粗略地了解 VFS 在运行的系统上执行哪些工作,请尝试简单的 vfscount 或 vfsstat,它们显示每秒钟都会发生数十次对 vfs_open() 及其朋友的调用。

对于一个不太简单的例子,让我们观察在运行的系统上插入 USB 闪存盘时 sysfs 中发生了什么。

在上面的第一个简单示例中,trace.py bcc 工具脚本在 sysfs_create_files() 命令运行时打印一条消息。 我们看到 sysfs_create_files() 是由 kworker 线程响应 USB 闪存盘插入而启动的,但是创建了什么文件呢? 第二个示例说明了 eBPF 的全部功能。 在这里,trace.py 正在打印内核回溯(-K 选项)以及 sysfs_create_files() 创建的文件的名称。 单引号内的代码片段是一些 C 源代码,包括一个易于识别的格式字符串,提供的 Python 脚本 会诱导 LLVM 即时编译器 在内核虚拟机中编译和执行。 必须在第二个命令中重现完整的 sysfs_create_files() 函数签名,以便格式字符串可以引用其中一个参数。 在此 C 代码片段中犯错误会导致可识别的 C 编译器错误。 例如,如果省略了 -I 参数,则结果为“Failed to compile BPF text.”。 熟悉 C 或 Python 的开发人员会发现 bcc 工具易于扩展和修改。
插入 USB 闪存盘时,会出现内核回溯,显示 PID 7711 是一个 kworker 线程,它在 sysfs 中创建了一个名为“events”的文件。 相应的 sysfs_remove_files() 调用显示,删除 USB 闪存盘会导致删除 events 文件,这与引用计数的概念一致。 在 USB 闪存盘插入期间使用 eBPF 观察 sysfs_create_link()(未显示)显示创建了不少于 48 个符号链接。
无论如何,events 文件的目的是什么? 使用 cscope 查找函数 __device_add_disk() 显示它调用了 disk_add_events(),并且可以将“media_change”或“eject_request”写入 events 文件。 在这里,内核的块层正在通知用户空间有关“磁盘”的出现和消失。 考虑一下,与仅从源代码中弄清楚该过程相比,这种调查 USB 闪存盘插入工作原理的方法有多么快速。
只读根文件系统使嵌入式设备成为可能
可以肯定的是,没有人会通过拔掉电源插头来关闭服务器或桌面系统。 为什么? 因为物理存储设备上的已挂载文件系统可能具有待处理的写入,并且记录其状态的数据结构可能与存储设备上的数据不同步。 发生这种情况时,系统所有者将不得不在下次启动时等待 fsck 文件系统恢复工具 运行,并且在最坏的情况下,实际上会丢失数据。
但是,爱好者们会听说许多 IoT 和嵌入式设备,如路由器、恒温器和汽车,现在都在运行 Linux。 许多这些设备几乎完全没有用户界面,并且无法干净地“取消启动”它们。 考虑一下使用没电的电池启动汽车的情况,其中 运行 Linux 的主机 的电源会反复升高和降低。 当发动机最终开始运转时,系统如何在没有长时间的 fsck 的情况下启动? 答案是嵌入式设备依赖于 只读根文件系统(简称 ro-rootfs)。

ro-rootfs 提供了许多比不可破坏性不那么明显的优势。 其中之一是,如果没有 Linux 进程可以在 /usr 或 /lib 中写入,则恶意软件无法写入。 另一个是,对于远程设备的现场支持而言,很大程度上不可变的文件系统至关重要,因为支持人员拥有与现场设备名义上相同的本地系统。 也许最重要(但也是最微妙)的优势是 ro-rootfs 迫使开发人员在项目的设计阶段决定哪些系统对象是不可变的。 处理 ro-rootfs 通常可能不方便甚至很痛苦,就像 编程语言中的 const 变量 通常一样,但是这些好处很容易弥补额外的开销。
创建只读根文件系统确实需要嵌入式开发人员付出一些额外的努力,而这就是 VFS 发挥作用的地方。 Linux 需要 /var 中的文件可写,此外,嵌入式系统运行的许多流行的应用程序会尝试在 $HOME 中创建配置文件(以点开头的文件)。 一种针对主目录中配置文件的解决方案通常是预先生成它们并将它们构建到 rootfs 中。 对于 /var,一种方法是在单独的可写分区上挂载它,而 / 本身则作为只读挂载。 使用绑定或覆盖挂载是另一种流行的替代方法。
绑定和覆盖挂载及其在容器中的使用
运行 man mount 是了解绑定和覆盖挂载的最佳位置,这使嵌入式开发人员和系统管理员能够在一个路径位置创建文件系统,然后在第二个路径位置将其提供给应用程序。 对于嵌入式系统,这意味着可以将 /var 中的文件存储在不可写的闪存设备上,但在启动时覆盖或绑定挂载 tmpfs 中的路径到 /var 路径上,以便应用程序可以在那里随意涂写。 下次启动时,/var 中的更改将消失。 覆盖挂载提供了 tmpfs 和底层文件系统之间的联合,并允许对 ro-rootfs 中现有文件进行明显的修改,而绑定挂载可以使新的空 tmpfs 目录显示为在 ro-rootfs 路径上可写。 虽然 overlayfs 是一种正确的文件系统类型,但绑定挂载是由 VFS 命名空间工具 实现的。
根据对覆盖和绑定挂载的描述,没有人会对 Linux 容器 大量使用它们感到惊讶。 让我们通过运行 bcc 的 mountsnoop 工具来监视当我们使用 systemd-nspawn 启动容器时会发生什么。

让我们看看发生了什么

在这里,systemd-nspawn 正在将主机 procfs 和 sysfs 中的选定文件提供给容器在其 rootfs 中的路径。 除了设置绑定挂载的 MS_BIND 标志之外,"mount" 系统调用调用的其他一些标志还确定了主机命名空间和容器中更改之间的关系。 例如,绑定挂载可以将 /proc 和 /sys 中的更改传播到容器,也可以隐藏它们,具体取决于调用。
总结
理解 Linux 内部机制似乎是一项不可能完成的任务,因为内核本身包含大量的代码,更不用说 Linux 用户空间应用程序和 C 库(如 glibc)中的系统调用接口了。 一种取得进展的方法是阅读一个内核子系统的源代码,重点是理解面向用户的系统调用和标头,以及主要的内核内部接口,此处以 file_operations 表为例。 文件操作是使“一切皆文件”真正起作用的原因,因此掌握它们特别令人满意。 顶层 fs/ 目录中的内核 C 源文件构成了虚拟文件系统的实现,虚拟文件系统是 shim 层,它实现了流行的文件系统和存储设备的广泛且相对简单的互操作性。 通过 Linux 命名空间进行的绑定和覆盖挂载是使容器和只读根文件系统成为可能的 VFS 魔法。 结合源代码研究,eBPF 内核工具及其 bcc 接口使探测内核比以往任何时候都更加简单。
非常感谢 Akkana Peck 和 Michael Eager 的评论和更正。
Alison Chaiken 将于 3 月 7 日至 10 日在加利福尼亚州帕萨迪纳举行的第 17 届南加州 Linux 博览会 (SCaLE 17x) 上介绍 虚拟文件系统:为什么我们需要它们以及它们是如何工作的。
6 条评论