文件系统是操作系统中最关键的子系统之一。它将裸设备的物理存储块(block)抽象为逻辑上的目录和文件,让用户和应用程序可以方便地组织、读取和写入数据。理解文件系统的底层原理,对于排查磁盘空间泄漏、IO 性能瓶颈和数据一致性问题都至关重要。本文将从 inode 结构、目录解析、VFS 抽象层、ext4 深度实现以及 Copy-on-Write 等现代文件系统展开讨论。
一、什么是文件系统
文件系统本质上是用户空间与底层块设备之间的抽象层。磁盘以固定大小的数据块(通常是 4KB)存储信息,但这些块对用户毫无语义。文件系统负责定义:
- 文件名与数据块的映射关系
- 目录的层级结构
- 文件的元数据(权限、所有者、大小、修改时间等)
- 空闲块的管理和分配策略
文件系统对外暴露的核心 API 包括 create、open、read、write、seek、unlink(删除)和 truncate。用户通过路径名访问文件时,文件系统会将路径翻译为磁盘上的块地址,最终转为对 IO 控制器(如 NVMe、SATA)的读写请求。
文件在磁盘上由两部分组成:
- 数据(data):文件的实际内容
- 元数据(metadata):描述文件特征的信息,如属主、权限、时间戳、数据块指针等
二、inode:文件的内部标识
inode 存储了什么
在类 Unix 系统中,每一个文件或目录都对应一个 inode(index node)。inode 是文件系统内部用于标识和管理文件的数据结构,关键字段包括:
- 文件类型(普通文件、目录、符号链接、设备文件等)
- 访问权限(rwxrwxrwx 对应的位掩码)
- 文件所有者和所属组(UID/GID)
- 文件大小
- 三个时间戳:atime(访问)、mtime(修改内容)、ctime(修改 inode)
- 指向数据块的指针或 extent 信息
- 硬链接计数(link count)
inode 不存储文件名
inode 中不包含文件名。文件名存储在目录文件中,目录本质上是一张映射表,将人类可读的文件名映射为 inode 编号。这种设计允许同一个 inode 拥有多个不同的文件名,也就是硬链接(hard link)。硬链接共享同一个 inode,删除其中一个文件名只会减少 inode 的 link count,只有当 count 归零且没有进程打开该文件时,实际数据块才会被释放。
符号链接
符号链接(symbolic link / symlink) 是一个独立的文件,拥有独立的 inode。它的数据内容是一个指向目标路径的字符串。符号链接可以跨越文件系统边界(因为只依赖路径字符串),但如果目标路径被删除,符号链接就会变成"悬空链接"。
inode 表的限制
文件系统在格式化时会预先分配 inode 表或 inode 区域,inode 总数是固定的。即使磁盘还有空闲空间,inode 耗尽也会导致 No space left on device 的错误。ext4 使用基于块的 inode 分配,而 XFS 采用动态 inode 分配,理论上无上限(受限于文件系统大小)。
三、目录结构:从文件名到 inode 的映射
目录是一种特殊的文件
目录被当作一种特殊的文件类型(inode 类型位标记为 S_IFDIR)。它的数据内容是一系列目录项(directory entry),每一项至少包含:
- 文件名(或子目录名)
- 对应的 inode 编号
在 ext2/ext3 中,每个目录项的大小不固定,以文件名变长存储。ext4 引入了 hash tree 目录结构,将目录项组织为 B-tree,大幅提升了包含数万个文件的目录的查找性能。
Linux 中的 dentry
Linux 内核为路径解析引入了一层缓存:dentry cache(目录项缓存)。dentry 是将路径分量(如 home、user、file.txt)与 inode 关联起来的内存结构。dentry cache 存储了最近访问过的路径解析结果,避免每次访问文件都从磁盘读取目录内容。
一个有效的路径解析流程是:/home/user/doc.txt → 根 inode → 查找 home 的 inode → 查找 user 的 inode → 查找 doc.txt 的 inode。
. 和 ..
每个目录都包含两个特殊条目:
.指向当前目录自身的 inode..指向父目录的 inode
根目录的 .. 指向自己,形成文件系统树的终止条件。路径解析器就是利用这个机制逐层"行走"文件系统树的。
四、VFS:虚拟文件系统抽象层
为什么需要 VFS
Linux 支持数十种文件系统:ext4、XFS、Btrfs、NTFS、FAT、tmpfs、procfs、sysfs、cgroupfs 等。如果每种文件系统都要单独实现 open、read、write 等系统调用,内核代码将极度臃肿。
VFS(Virtual File System) 是一个中间抽象层,定义了一组通用的数据结构和方法,让所有文件系统都注册并实现对这一套接口。用户进程调用 read() 时,VFS 根据文件所属的挂载点和文件系统类型,调用对应文件系统驱动注册的 read 函数。
VFS 四大核心对象
| 对象 | 代表 | 说明 |
|---|---|---|
super_block | 超级块 | 代表一个挂载的文件系统实例 |
inode | 索引节点 | 代表文件系统中的一个对象 |
dentry | 目录项 | 描述路径分量与 inode 的映射关系 |
file | 打开的文件 | 代表进程打开的文件实例,含文件偏移量 |
每个对象都关联一个 operations 结构体:
super_operations:分配/释放 inode、写回超级块等inode_operations:创建文件、查找目录项、设置属性等file_operations:读、写、lseek、mmap、fsync 等dentry_operations:重新验证、哈希比较等
注册与挂载
文件系统驱动在内核中通过 register_filesystem() 注册,传入一个 file_system_type 结构体。例如 ext4 注册时指定其 mount 函数,当用户执行 mount -t ext4 /dev/sda1 /mnt 时,VFS 调用 ext4 的 mount 函数读取超级块,建立 super_block,并将该挂载点纳入全局挂载树。
无论底层是磁盘上的 ext4、内存中的 tmpfs 还是虚拟的 procfs,用户进程使用的 open()、read()、write() 系统调用接口完全一致。
五、ext4 文件系统深度解析
ext4 是 Linux 当前最常用的磁盘文件系统,它在 ext3 的基础上进行了多项关键改进。
Extent 替代传统间接块寻址
传统 ext2/ext3 使用 12 个直接块指针、1 个间接块指针、1 个双重间接和 1 个三重间接指针来定位文件数据块。对于大文件,这种方案会产生大量离散的元数据 IO。
ext4 引入了 extent 机制:一个 extent 用一个起始块号 + 块数表示一段连续的存储区域。100MB 的连续大文件只需一个 extent 描述即可,无需多层间接指针,极大减少了元数据开销和内存占用。
日志模式(Journaling)
ext4 支持三种日志模式,在性能和一致性之间做权衡:
- journal 模式:所有数据变更和元数据都先写入日志。最安全,但性能最低(相当于两次写入)。
- ordered 模式(默认):元数据先写日志,数据块在元数据提交前强制写盘。保证文件内容不会落后于元数据记录。
- writeback 模式:只记录元数据变更,数据块写盘无顺序保证。性能最高,但崩溃后可能看到旧数据存在于新文件中。
块组(Block Group)
ext4 将磁盘划分为多个块组,每个块组内含自己的 inode 表、数据块位图和 inode 位图。这种设计追求数据局部性:一个目录下的文件倾向于分配到同一块组,减少磁头移动。
孤儿 inode 列表
文件系统崩溃时,可能有一些 inode 已经被分配但尚未链接到任何目录(例如程序创建后、link() 之前崩溃)。ext4 维护一个孤儿 inode 列表,挂载时扫描并回收这些孤立 inode,回收它们占用的数据块。
延迟分配(Delayed Allocation)
ext4 不会立即为写入操作分配物理块,而是将数据先写入 page cache,在真正需要落盘前(如刷脏页或显式 fsync)再做块分配决策。这种策略让文件系统看到完整的写入范围,可以分配更长的连续 extent,显著减少碎片。
六、Copy-on-Write 文件系统
传统文件系统的写入方式是覆盖原位(in-place)。Copy-on-Write(COW)文件系统则保证:任何写操作都不会覆盖旧数据,而是将修改内容写入新位置,然后更新指针。旧数据保留不变,新数据形成新版本。
Btrfs
Btrfs(B-tree FS)是 Linux 原生设计的 COW 文件系统,主要特性包括:
- 子卷(subvolume):文件系统内的独立命名空间,可独立挂载和快照
- 快照:基于 COW 实现的瞬时快照,只占用差异部分的空间
- 校验和(checksum):对数据和元数据都计算 CRC32C,可在读取时检测静默数据损坏
- RAID 支持:内置 RAID0/1/10/5/6 逻辑,无需 LVM 或 mdadm
ZFS
ZFS 由 Sun 开发,以"存储池"(zpool)概念著称,特性包括:
- Pooled storage:将多个物理盘合并为一个逻辑池,逻辑卷从中弹性分配
- RAID-Z:类似 RAID5/6 但避免写 hole 问题(数据被部分写入时崩溃导致不一致)
- Copy-on-Write 保证:所有写入都是原子性的,不会出现半写状态
- 自愈能力:读取时自动校验,发现数据损坏时利用冗余信息自动修复
优势与权衡
优势:瞬时快照不阻塞 IO;数据完整性校验杜绝静默损坏;天然支持卷管理和精简配置。
权衡:COW 天然产生碎片(尤其是随机写工作负载);文件系统元数据复杂,内存占用较高;RAID-Z 的写放大和重建时间比传统 RAID 更长。
七、Page Cache 与 Buffer Cache
文件数据的内存缓存
Linux 将文件内容缓存在内存中的 page cache 中。每个缓存页对应一个文件偏移。当进程读取文件时,内核先检查 page cache,如果命中则直接从内存返回数据,无需磁盘 IO。
写回策略
当进程写入文件时,数据先复制到 page cache 中的脏页(dirty page),此时并未真正落盘。内核有一组后台线程(历史上叫 pdflush,现在由 kworker 中的 flush 任务承担),定期扫描脏页,将满足以下条件的页写回磁盘:
- 脏页在内存中存在时间超过阈值(
dirty_expire_centisecs) - 脏页占总内存比例超过阈值(
dirty_ratio/dirty_background_ratio)
fsync 与 fdatasync
后台写回不能保证数据的及时持久化。如果系统崩溃,尚未写回的脏页会丢失。
fsync(fd):将文件的数据和元数据强制写盘fdatasync(fd):只同步数据,不同步元数据(如果元数据不影响数据读取的正确性)
数据库事务日志通常会在提交时调用 fdatasync 或 fsync 来保证持久性。
清理缓存
在测试或性能基准场景中,可以通过以下命令清空 page cache:
echo 3 > /proc/sys/vm/drop_caches
1 释放 page cache,2 释放 slab(包括 buffer cache/dentry cache),3 同时释放两者。
八、实用工具与系统调用
stat 与 fstat
stat() 和 fstat() 用于获取文件的 inode 信息。stat 以路径为参数,fstat 以文件描述符为参数。返回的结构体包含设备号、inode 编号、模式、链接数、所有者、大小和时间戳等字段。
struct stat st;
stat("/etc/passwd", &st);
printf("inode: %lu, size: %ld\n", st.st_ino, st.st_size);
df 与 du
df -h:查看每个挂载点的磁盘使用情况(基于文件系统超级块中的统计信息)du -sh /path:递归统计目录下所有文件的实际磁盘占用,基于遍历目录和累加文件大小
两者结果可能不一致:du 计算的是可见文件的总大小,而 df 反映的是文件系统级别的块分配情况,包括已删除但仍被进程保持打开的文件。
/proc/filesystems
查看内核支持的文件系统类型:
cat /proc/filesystems
输出中标记为 nodev 的表示非块设备文件系统,如 tmpfs、proc、sysfs。
mount
查看当前挂载的文件系统:
mount | grep ext4
挂载新文件系统时,VFS 读取超级块并建立 mount 结构。通过 findmnt 可以更直观地展示挂载树的层级关系。
debugfs:深入 ext4 内部
debugfs 是 ext2/3/4 的交互式调试工具,可以查看和修改文件系统元数据(只读使用更稳妥):
# 以只读模式打开 dev/sda1
sudo debugfs -R "stat /home/user/file.txt" /dev/sda1
# 查看 inode 的 extent 信息
sudo debugfs -R "extent /home/user/file.txt" /dev/sda1
# 查看超级块信息
sudo debugfs -R "show_super_stats" /dev/sda1
总结
文件系统的核心使命是将物理存储转化为语义化的目录和文件。inode 是文件的身份标识但不存储文件名;目录是文件名到 inode 的映射表;VFS 则将各种异构文件系统统一在同一套系统调用接口下。ext4 通过 extent、延迟分配和日志机制在性能和可靠性之间取得了良好的平衡;而 Btrfs 和 ZFS 等 COW 文件系统则为快照、校验和和数据完整性提供了现代解决方案。理解 page cache 的写回机制和 fsync 的语义,是编写可靠 IO 程序的基础。
延伸阅读:
- Linux 内核源码:
fs/ext4/、fs/inode.c、fs/dcache.cman 2 stat、man 8 tune2fs、man 8 debugfsDocumentation/filesystems/vfs.rst
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。