跳转到内容
← 返回核心概念
系统与架构计算机科学 · 操作系统17 分钟阅读

文件系统

File Systems

你电脑上有 100,000 个文件分散在 SSD 的数十亿个存储单元中,你用一秒钟就能找到并打开任何一个。 这个能力如此日常,以至于我们从未思考它背后的机制。但这背后是几十年的工程:如何在可能随时断电、可能出现坏块的存储硬件上,可靠地保存和检索数以亿计的命名数据对象?

文件系统存储inode日志持久化

你电脑上有 100,000 个文件分散在 SSD 的数十亿个存储单元中,你用一秒钟就能找到并打开任何一个。

这个能力如此日常,以至于我们从未思考它背后的机制。但这背后是几十年的工程:如何在可能随时断电、可能出现坏块的存储硬件上,可靠地保存和检索数以亿计的命名数据对象?

这就是文件系统(File System)解决的问题。

破除误解:文件不是"磁盘上的一块区域"

在用户看来,文件是有名字的、连续的字节序列。但在硬盘上,文件实际上是分散存储的——一个大文件可能分布在磁盘的数百个不连续的位置,称为碎片(Fragmentation)

文件系统的职责之一就是维护这种映射:文件名 → 若干个磁盘块的位置列表。这个映射结构就是元数据(Metadata)

破除误解:删除文件并没有"抹掉"数据

直觉上,删除文件等于把它从盘上擦除。事实并非如此。

绝大多数文件系统删除文件时只做两件廉价的事:把目录里那条"文件名 → inode 号"的记录抹掉,再把该文件占用的数据块在位图里标记为"空闲"。

数据块里的原始字节原封不动地留在盘上,直到某次新的写入恰好覆盖到它们。

这正是数据恢复软件和数字取证能"复活"已删文件的原因,也是为什么真正的安全删除必须主动覆写,而不是简单删除。

在 SSD 上情况更微妙:磨损均衡会把逻辑地址重新映射到不同的物理单元,操作系统层面的"覆写"未必落在原来的物理位置。因此可靠的彻底擦除往往要靠硬件级的 ATA Secure Erase,或全盘加密后直接丢弃密钥。

文件系统的基本结构

inode(索引节点)

Unix 和类 Unix 系统(Linux、macOS)使用 inode 作为文件的核心元数据结构。每个文件(和目录)对应一个 inode,存储:

  • 文件大小
  • 文件类型(普通文件/目录/符号链接/设备文件)
  • 权限(owner/group/other 的读写执行权限)
  • 时间戳(创建/修改/访问时间)
  • 硬链接数量
  • 数据块地址:指向存储实际数据的磁盘块

inode 不存储文件名。文件名存储在目录文件中,目录是一种特殊文件,内容是"文件名 → inode 号"的映射表。

这使得硬链接(Hard Link)成为可能:多个文件名可以指向同一个 inode,从而共享相同的数据块,而不占用额外存储空间。

磁盘块与空间管理

磁盘被划分为固定大小的块(Block),典型大小 4KB。文件系统用位图(Bitmap)追踪哪些块已使用、哪些空闲。

对于大文件,一个 inode 中的直接指针(通常 12 个)不够用,需要间接指针(Indirect Block): - 一级间接:一个指针指向一个块,该块全部存放数据块指针 - 二级间接:两层指针 - 三级间接:三层指针

以 4KB 块、4 字节指针为例(每个块可存放 1024 个指针),仅三级间接一项就能寻址 102431024^3 个数据块,约 4TB,足以支撑超大文件。

为什么块这么大:一段被验证的历史

块大小不是随意定的,它来自一次著名的性能教训。

1970 年代最早的 Unix 文件系统用 512 字节的小块,且把 inode 和数据分散摆放。随着使用,文件块被打散到全盘各处,磁头不停长距离寻道。McKusick 等人实测:吞吐从初装时的约 175 KB/s 跌到几周后的约 30 KB/s,只用到磁盘带宽的约 2%。

1984 年,他们在伯克利推出 FFS(Fast File System,快速文件系统):把块放大到 4096 字节及以上,引入"柱面组(cylinder group)"让相关的 inode 与数据块在物理上聚拢,再为小文件保留"碎片(fragment)"以免浪费空间。结果是文件吞吐提升约 10 倍。

今天 ext4、XFS、APFS 默认的 4KB 块,以及"让相关数据物理相邻"的布局思路,都直接继承自 FFS。

目录树

文件系统以树形结构(实际上是有向无环图,支持硬链接)组织目录和文件:

/(根目录)
├── etc/
│   ├── hosts
│   └── passwd
├── home/
│   └── alice/
│       └── documents/
└── usr/
    └── bin/
        └── ls
```

每个目录项(directory entry)存储文件名和对应的 inode 号。

主要文件系统格式

文件系统操作系统特点
ext4LinuxLinux 最主流,日志型
NTFSWindows支持大文件、权限、加密
APFSmacOS/iOS2017 年推出,针对 SSD 优化,写时复制
FAT32/exFAT跨平台简单,USB 存储设备通用
XFSLinux高性能,适合大文件
BtrfsLinux写时复制,支持快照和 RAID
ZFSSolaris/FreeBSD/Linux综合文件系统+卷管理,强完整性保证

日志文件系统:处理突然断电

一个文件系统的操作,如删除文件,需要修改多个数据结构(目录、inode、块位图)。如果在这些步骤中间断电,文件系统可能处于不一致状态——目录更新了但 inode 没更新,导致空间泄漏或数据损坏。

日志文件系统(Journaling File System) 在执行实际修改前,先将操作记录到一个独立的日志区(Journal)

  1. 把所有将要做的修改作为一个事务(Transaction)写入日志
  2. 提交日志(Journal Commit)——日志写入后,这是原子操作
  3. 执行实际的数据结构修改(Checkpoint)
  4. 从日志中清除已完成的事务

如果步骤 3 中途断电,重启后文件系统检测到未完成的事务,从日志重放(Redo)或撤销(Undo)操作,恢复到一致状态。

ext4、NTFS、XFS 都是日志文件系统。日志的位置和类型(只记录元数据 vs 同时记录数据)是性能与安全性的权衡。

写时复制(Copy-on-Write,CoW)

APFS、Btrfs、ZFS 使用的另一种保障一致性的方式。

写时复制:修改数据时,不在原位修改,而是写到新的位置,然后更新指针,让文件系统指向新数据。旧数据在没有引用后再释放。

好处: - 原子性:切换指针是原子操作,要么指向旧数据,要么指向新数据,不存在中间状态 - 快照(Snapshot):复制指针(而不是复制数据),瞬间创建文件系统快照。快照可以保存文件系统在某个时间点的完整状态,方便备份和回滚

APFS 在 macOS/iOS 上启用快照后,Time Machine 备份和 iOS 系统升级都用到了这个机制。

日志和 CoW 之外:另外两条路线

日志和写时复制不是仅有的两种保证崩溃一致性的方案。

软更新(Soft Updates):FreeBSD 的 UFS 文件系统采用的方案,由 Ganger 与 Patt 于 1994 年提出,1998 年进入 FreeBSD。它不写日志,而是精心排序所有元数据写入,保证任何时刻断电后盘上结构即使不完整,最多也只出现"空间被标记占用、实际没人使用"这种良性泄漏,绝不会损坏数据。代价是实现极其复杂,且崩溃后仍需后台 fsck 回收泄漏的空间。

日志结构文件系统(Log-structured File System,LFS):Rosenblum 与 Ousterhout 于 1992 年提出的激进思路——把整块磁盘当成一个只追加的日志,所有写入(数据和元数据)都顺序追加到日志末尾,从不原地修改。小文件的随机写因此变成顺序写,他们的 Sprite LFS 原型把写入时的磁盘带宽利用率从 Unix 的 5%–10% 提升到约 70%。代价是需要一个"清理器(cleaner)"在后台回收被覆盖作废的旧数据,当磁盘较满时清理开销会拖累性能。

这个思路在闪存时代复活了:SSD 内部的 FTL(Flash Translation Layer,闪存转换层) 本质上就是日志结构的。Samsung 的 F2FS(Flash-Friendly File System) 由 Jaegeuk Kim 主导,2012 年末并入 Linux 3.8 开发树(该内核于 2013 年 2 月正式发布),专为闪存设计成日志结构,如今是大量 Android 设备(包括 Google Pixel)数据分区的默认文件系统。

SSD 与文件系统的新挑战

机械硬盘(HDD)的寻道时间(磁头移动)决定了顺序读写远快于随机读写,文件系统传统上优化顺序访问、减少碎片。

SSD(固态硬盘)没有机械部件,随机读写速度接近顺序读写。但 SSD 有新的挑战:

写放大(Write Amplification):SSD 以页(Page,通常 4KB)为单位写入,但以块(Block,512KB - 4MB)为单位擦除。修改一个字节需要读整个块、擦除整个块、写整个新块——放大了写入量,缩短了 SSD 寿命。

磨损均衡(Wear Leveling):SSD 每个存储单元只能擦写有限次(MLC 约 3000-10000 次),控制器需要把写入均匀分散到所有单元,避免某些单元提前寿终。

TRIM 命令:允许操作系统通知 SSD 哪些块已被删除可以擦除,避免 SSD 在写时才发现需要先擦除旧数据。文件系统和 SSD 固件需要共同支持 TRIM 才能维持性能。

分布式文件系统

现代大规模系统需要文件系统跨越多台机器:

NFS(Network File System):把远程服务器的目录挂载到本地,透明访问,由 Sun Microsystems 1984 年设计。

GFS(Google File System):Google 2003 年论文,为大文件、顺序读写、数千客户端设计,文件分为 64MB 的 Chunk,存储于普通 Linux 机器,元数据由 Master 节点统一管理。HDFS(Hadoop Distributed File System)是开源版本。

Ceph:开源分布式存储系统,支持对象存储、块存储和文件系统三种接口,无中心元数据服务器,通过 CRUSH 算法确定性计算数据位置。

持久性的真相:write 之后,数据真的安全了吗?

一个被反复误解的点:程序调用 write() 返回成功,不代表数据已经落盘。数据通常只是进了内核的页缓存(Page Cache),稍后才异步刷写。要确保落盘,必须显式调用 fsync()

这条语义缝隙制造过两起著名事故。

ext4 的"零长度文件"事件(2009):为提升性能,ext4 默认启用延迟分配(delayed allocation)——数据块的实际分配可能推迟最多约 60 秒,而 ext3 通常约 5 秒。很多应用习惯用"写新文件 → rename 覆盖旧文件"来原子地更新配置,并默认 rename 之后旧内容至少还在。但在 ext4 上,如果 rename 已落盘而延迟分配的数据块还没写,崩溃后用户就会得到一个零字节的空文件

维护者 Theodore Ts'o 坚持 ext4 完全符合 POSIX——POSIX 从未承诺 rename 隐含 fsync。这场争论催生了讽刺词 "O_PONIES",指程序员想要、而标准从未承诺的那种"魔法持久性"。最终 Ts'o 在 2.6.30 内核加了折中:检测到 rename 覆盖或文件截断的场景时,自动先把延迟的数据块分配落盘。

fsyncgate(2018):PostgreSQL 社区发现一个潜伏二十年的隐患——在 Linux 上,当后台回写失败时,内核会丢弃那些脏页、把它们标记为"干净",并且只把错误报告给第一个调用 fsync() 的文件描述符。数据库若先前已有一次 fsync() 把错误"取走",后续重试就会拿到成功返回,以为数据安全,实则已永久丢失。PostgreSQL 的修复是:fsync() 一旦失败就直接 PANIC 崩溃,靠预写日志(WAL)重放来恢复,因为这是唯一安全的路径。Linux 内核也在 4.13 引入 errseq_t 机制,让回写错误对之后所有描述符都可见。

两次事故的教训一致:"写了"和"持久化了"是两回事。跨越文件系统、数据库与应用三层的正确性,都建立在对 fsync 语义的精确理解之上。

代价与争议

文件系统的碎片问题:传统 HDD 文件系统随着使用产生碎片,导致顺序读写退化为随机读写,性能下降。ext4 有在线碎片整理工具,NTFS 的碎片整理是 Windows 的内置功能。SSD 基本消除了这个问题。

文件系统作为数据库:复杂应用(如邮件客户端)有时把大量小文件存储在文件系统中(如 Maildir 格式,每封邮件一个文件)。文件系统在处理海量小文件时效率低下;而把所有邮件存在单个大文件或数据库中,又牺牲了文件系统的通用性。这是一个持续的工程权衡。

跨域连接

  • 数据库与事务:崩溃一致性与事务持久性是同一个问题的两个名字,都靠先写日志再改数据来实现。麻烦在于分层:数据库的持久性建立在文件系统与内核的语义之上,只要有一层在回写失败时撒谎,上层的保证就整条失效。推论是:写入返回成功并不等于数据落盘,跨层的持久性必须逐层确认。
  • 同一性:索引节点才是文件的身份,名字只是目录里一条指向它的记录。硬链接因此让同一个对象拥有多个名字,删掉一个名字并不等于删掉对象——"删除"这个词在这里指的是解除引用,而不是销毁。
  • 古生物学与地层学:删除只是停止记录,旧的字节仍留在原处,直到被新的写入覆盖。地层解读用的是同一条推理:痕迹能否保存,取决于覆盖速率与后期扰动,而不取决于是否有人宣布它已经结束。推论是:可靠的销毁必须主动覆写或换掉密钥,而不是把文件拖进回收站。
  • 刚体转动:机械硬盘的随机访问代价来自宏观机械运动——磁头寻道加上盘片转到位,量级是毫秒。这条物理限制解释了半个世纪的布局策略:把相关的元数据与数据强行摆在物理相邻处;也解释了固态盘普及后碎片整理为何失去意义。同理,把相关数据聚在一起的思路在固态盘上仍然有价值,只是理由换成了擦除粒度。
  • 国际人权体制:可被验证的删除是法律要求,而解除引用不是删除,磨损均衡还会让"覆写"落到别的物理单元上。工程上唯一可靠的答案是全盘加密后销毁密钥——于是一条权利条款,最终变成了密钥生命周期的管理问题。

参考文献

  • McKusick, M. et al. A Fast File System for UNIX. ACM Transactions on Computer Systems 2(3), 1984.
  • Rosenblum, M. & Ousterhout, J. The Design and Implementation of a Log-Structured File System. ACM Transactions on Computer Systems 10(1), 1992.
  • Ganger, G., McKusick, M., Soules, C., Patt, Y. Soft Updates: A Solution to the Metadata Update Problem in File Systems. ACM Transactions on Computer Systems 18(2), 2000.
  • Ghemawat, S., Gobioff, H., Leung, S. The Google File System. SOSP, 2003.
  • Bonwick, J. & Moore, B. ZFS: The Last Word in File Systems. Sun Microsystems, 2007.
  • Ts'o, T. Delayed Allocation and the Zero-Length File Problem. 2009.(ext4 维护者对延迟分配引发数据丢失的官方说明)
  • Lee, C., Sim, D., Hwang, J., Cho, S. F2FS: A New File System for Flash Storage. USENIX FAST, 2015.
  • Corbet, J. PostgreSQL's fsync() Surprise. LWN.net, 2018.(fsyncgate 始末与内核回写错误处理)

延伸阅读

  • Arpaci-Dusseau, R. & Arpaci-Dusseau, A. Operating Systems: Three Easy Pieces. (免费在线,文件系统章节极佳,ostep.org)