什么是文件系统?早期 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(扩展伯克利数据包过滤器)由 在内核内部运行的虚拟机 组成,特权用户可以从命令行查询该虚拟机。内核源代码告诉读者内核 可以 做什么;在启动的系统上运行 eBPF 工具则显示内核实际 在做什么。
令人高兴的是,通过 bcc 工具可以非常轻松地开始使用 eBPF,这些工具可以作为 主要 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 文件系统恢复工具 运行,并且在最坏的情况下,实际上会丢失数据。
然而,爱好者们会听说许多物联网和嵌入式设备(如路由器、恒温器和汽车)现在都运行 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 源文件构成了其虚拟文件系统的实现,虚拟文件系统是一个垫片层,可实现流行的文件系统和存储设备的广泛且相对简单的互操作性。通过 Linux 命名空间的绑定挂载和覆盖挂载是使容器和只读根文件系统成为可能的 VFS 魔力。结合源代码研究,eBPF 内核工具及其 bcc 接口使探测内核比以往任何时候都更加简单。
非常感谢 Akkana Peck 和 Michael Eager 的评论和更正。
Alison Chaiken 将在 3 月 7 日至 10 日在加利福尼亚州帕萨迪纳举行的第 17 届年度南加州 Linux 展会 (SCaLE 17x) 上介绍 虚拟文件系统:为什么我们需要它们以及它们如何工作。
6 条评论