跳到主要内容

文件系统:从 Inodes 到分布式存储

文件系统提供了长期、持久存储的主要抽象。它将磁盘块的混乱数组组织成人类和应用程序能够理解的逻辑结构(文件和目录)。

本章探讨现代文件系统的内部数据结构、虚拟文件系统 (VFS) 的机制,以及不同存储策略之间的权衡。


1. 虚拟文件系统 (VFS) 层

为了同时支持多种文件系统(如 Ext4, NTFS, NFS),现代内核使用了 虚拟文件系统 (VFS) 抽象。应用程序与 VFS 交互,后者随后将调用分发给特定的文件系统驱动程序。

1.1 VFS 的四个核心对象

  1. 超级块 (Superblock):包含关于整个文件系统的元数据(块大小、空闲计数、挂载状态)。
  2. Inode (索引节点):代表单个文件或目录。它存储除文件名以外的所有内容(权限、大小、时间戳、数据块指针)。
  3. Dentry (目录项):代表路径的一个组件(例如在 /home/user 中,homeuser 都是 dentry)。它将文件名链接到 inodes。
  4. 文件对象 (File Object):代表一个进程中打开的文件。它存储当前的“偏移量”(光标)和访问模式(读/写)。

1.2 打开文件的流程

当你调用 open("/var/log/syslog", O_RDONLY) 时:

  1. VFS 从根目录 / 的 dentry 开始。
  2. 它在 Dentry 缓存 中查找 var,然后是 log,最后是 syslog
  3. 如果未找到,它会请求特定的文件系统驱动程序(如 Ext4)从磁盘读取目录数据并填充缓存。
  4. 最终找到 syslog 的 inode,创建一个文件对象,并向进程返回一个文件描述符(整数索引)。

2. 磁盘布局:以 Ext4 为例

大多数 Linux 系统使用 Ext4 (第四代扩展文件系统)。其布局旨在提高速度和可靠性。

2.1 块组 (Block Groups)

一个大磁盘被划分为多个块组,以使相关的元数据(inodes 及其数据块)在物理上彼此靠近,从而减少磁盘头的移动(寻道时间)。

2.2 Extents (高效映射)

Ext4 不使用简单的块列表,而是使用 Extents

  • 一个 extent 是一范围连续的物理块(例如“从 1000 到 1500 号块”)。
  • 收益:一个巨大的文件只需几个 extents 即可描述,极大地减小了 inode 的大小并提升了性能。

2.3 日志 (Journal)

为了防止崩溃后数据损坏,Ext4 使用了日志

  1. 写入意图:文件系统先将元数据更改写入专用的日志区域。
  2. 提交:只有当日志写入安全后,它才更新实际的文件系统结构。
  3. 恢复:如果系统崩溃,内核只需“重放”日志即可确保状态一致。

3. 目录实现

操作系统如何将 "photo.jpg" 这样的字符串映射到 inode #12345?

3.1 简单线性列表

每个目录都是一个文件,包含 (文件名, Inode 号) 对的列表。

  • 问题:在一个拥有 10 万个文件的目录中进行搜索会变得极其缓慢 (O(N)O(N))。

3.2 B-树与哈希

现代文件系统(如 XFS 和启用了 dir_index 的 Ext4)使用 HtreeB+ 树来存储目录项。

  • 收益:查找复杂度为 O(logN)O(\log N),支持包含数百万个文件的目录。

4. 硬链接 vs. 符号链接

硬链接只是指向完全相同 inode 号的第二个文件名。

  • 规则:只要存在一个硬链接,删除原始文件就不会删除数据。Inode 有一个 link_count 字段。
  • 局限性:不能跨文件系统边界(因为 inode 号仅在单个文件系统内唯一)。

一个特殊文件,其内容只是一个路径字符串(如 ../data/file.txt)。

  • 规则:如果目标被移动或删除,链接将变为“断开”或“悬空”状态。
  • 收益:可以指向任何存储设备上的任何文件,甚至是远程网络路径。

5. 缓存与 I/O 性能

从磁盘读取比从 RAM 读取慢 10 万倍。操作系统使用几种技巧来隐藏这种延迟。

5.1 页缓存 (Page Cache)

内核使用所有“未使用”的 RAM 来缓存来自文件系统的数据块。

  • 统一缓存:在现代 Linux 中,页缓存和缓冲区缓存 (Buffer Cache) 已合并。
  • 回写 (Write-back):当应用写入数据时,它仅被写入页缓存(将页面标记为 Dirty)。内核的 pdflushkworker 线程每隔几秒异步将其刷新到磁盘。

5.2 预读 (Read-ahead)

如果应用读取了 1, 2, 3 号块,内核假设它很快将需要 4 到 10 号块,并预先将它们取入页缓存。


6. 伪文件系统:内核接口

在 Linux 中,“一切皆文件”意味着内核通过文件系统接口暴露其内部状态。

6.1 procfs (/proc)

提供关于进程和内核状态的信息。

  • /proc/cpuinfo:CPU 能力。
  • /proc/[pid]/maps:特定进程的内存布局。

6.2 sysfs (/sys)

提供硬件和驱动程序的结构化视图。

  • 用于在运行时调整内核参数(如 CPU 频率缩放)。

7. 高级存储:写时复制 (CoW)

ZFSBtrfs 这样的文件系统采取了不同的方法。

  • 不执行覆盖:当块被修改时,它永远不会被覆盖。相反,修改后的数据被写入一个块。
  • 原子性:顶级指针最后更新。这使得文件系统天生具备崩溃一致性,无需传统日志。
  • 快照 (Snapshots):创建快照是“瞬时”且无成本的,因为它只是指向当前树根的一个指针。

8. 核心概念复习清单

  • 描述超级块、Inode 和 Dentry 之间的关系。
  • 为什么 Extents 比简单的块列表更适合大文件?
  • 硬链接 vs 符号链接:哪一个会增加 inode 的链接计数?
  • 页缓存如何同时提升读写性能?
  • 内存中的“脏” (Dirty) 页面会发生什么?

第 05 章结束。继续前往:第 06 章:I/O 系统