06|文件系统
硬盘上原本只有一串串 0 和 1,是文件系统把它们组织成了我们熟悉的”文件夹+文件”结构。本章将从 inode 开始,带你理解 VFS 抽象层、ext4/XFS/Btrfs 的设计思想,以及日志文件系统如何保证数据安全。
📋 开篇自测:你已经知道多少?
- 文件名和文件内容在磁盘上是存在一起的吗?inode 是什么?
- 什么是 VFS?为什么 Linux 能同时支持 ext4、XFS、NFS 等不同文件系统?
- 日志文件系统解决了什么问题?
一、文件系统的基本概念
1.1 从磁盘到文件
一块硬盘在物理上就是一个巨大的字节数组。如果没有文件系统,你只能通过”从第 1048576 字节开始读取 4096 字节”这种方式来访问数据——这显然不适合人类使用。
文件系统的职责就是在这个字节数组上建立一套组织规则,让我们可以通过”/home/user/report.pdf”这样的路径来读写数据。
磁盘的物理视角:
+----+----+----+----+----+----+----+----+----+----+
|0000|0001|0002|0003|0004|0005|0006|0007|0008|0009| ...
+----+----+----+----+----+----+----+----+----+----+
块0 块1 块2 块3 块4 ...
文件系统的逻辑视角:
/
├── home/
│ ├── user/
│ │ ├── report.pdf
│ │ └── notes.txt
│ └── admin/
├── etc/
│ ├── passwd
│ └── fstab
└── var/
└── log/
└── syslog
1.2 磁盘的分区与格式化
物理磁盘布局:
+-------+----------+----------+----------+---------+
| MBR/ | 分区1 | 分区2 | 分区3 | 分区4 |
| GPT | (ext4) | (XFS) | (swap) | (数据) |
+-------+----------+----------+----------+---------+
每个分区可以格式化为不同的文件系统类型
一个 ext4 分区的内部结构:
+--------+----------+----------+----------+----------+
| 引导块 | 块组 0 | 块组 1 | 块组 2 | ... |
+--------+----------+----------+----------+----------+
块组 (Block Group) 的内部:
+-------+-------+-------+-------+---------+----------+
| 超级块 | 组描述 | 数据块 | inode | inode | 数据块 |
| | 符表 | 位图 | 位图 | 表 | |
+-------+-------+-------+-------+---------+----------+
二、inode:文件的身份证
2.1 inode 是什么
在 Unix/Linux 文件系统中,文件名和文件内容是分开存储的。文件的元数据(大小、权限、时间戳、数据块位置等)存储在一个叫做 inode(索引节点) 的数据结构中。
inode 结构 (简化版):
+-----------------------------------+
| inode #12345 |
+-----------------------------------+
| 文件类型: 普通文件 |
| 权限: rwxr-xr-x (755) |
| 所有者: uid=1000 |
| 所属组: gid=1000 |
| 文件大小: 52480 字节 |
| 硬链接数: 2 |
| 访问时间: 2025-03-15 10:30:00 |
| 修改时间: 2025-03-14 14:20:00 |
| 创建时间: 2025-01-10 08:00:00 |
+-----------------------------------+
| 数据块指针: |
| 直接指针[0]: 块 #8001 |
| 直接指针[1]: 块 #8002 |
| ... |
| 直接指针[11]: 块 #8012 |
| 一级间接指针: 块 #9000 |
| 二级间接指针: 块 #9500 |
| 三级间接指针: 块 #9800 |
+-----------------------------------+
注意: inode 中没有文件名!
2.2 文件名在哪里
文件名存储在目录中。目录本身也是一个文件,它的内容是一张”文件名 -> inode 号”的映射表:
目录 /home/user/ 的内容:
+------------+-----------+
| 文件名 | inode 号 |
+------------+-----------+
| . | 30001 | (当前目录)
| .. | 20001 | (父目录)
| report.pdf| 12345 |
| notes.txt | 12346 |
| photo.jpg | 12350 |
+------------+-----------+
读取 /home/user/report.pdf 的过程:
1. 查找 / 目录 (inode 2, 根目录固定为2)
2. 在 / 目录中找 "home" -> inode 20000
3. 读取 inode 20000 的数据块(目录内容)
4. 在 home/ 目录中找 "user" -> inode 30001
5. 读取 inode 30001 的数据块
6. 在 user/ 目录中找 "report.pdf" -> inode 12345
7. 读取 inode 12345 获取文件元数据和数据块位置
8. 读取数据块获得文件内容
2.3 硬链接与软链接
硬链接(Hard Link):多个文件名指向同一个 inode。
$ ln report.pdf report_backup.pdf
report.pdf --> inode 12345 (链接数=2)
report_backup.pdf --> inode 12345 (同一个inode!)
删除 report.pdf:
inode 12345 链接数变为 1, 数据不会删除
删除 report_backup.pdf:
inode 12345 链接数变为 0, 数据被回收
软链接(Symbolic Link / 软连接):一个特殊文件,内容是另一个文件的路径。
$ ln -s /home/user/report.pdf /tmp/shortcut
/tmp/shortcut (inode 99999, 类型=符号链接)
内容: "/home/user/report.pdf"
/home/user/report.pdf (inode 12345)
实际数据
删除 report.pdf:
shortcut 变成"悬空链接" (dangling symlink)
| 特性 | 硬链接 | 软链接 |
|---|---|---|
| 跨文件系统 | 不可以 | 可以 |
| 指向目录 | 不可以(防循环) | 可以 |
| 原文件删除 | 数据仍在 | 链接失效 |
| inode | 相同 | 不同 |
| 文件大小 | 和原文件相同 | 路径字符串的长度 |
# 查看 inode 号
ls -li /home/user/
# 12345 -rw-r--r-- 2 user user 52480 Mar 14 14:20 report.pdf
# 12345 -rw-r--r-- 2 user user 52480 Mar 14 14:20 report_backup.pdf
# 注意两个文件的 inode 号和链接数都是一样的
🤔 想一想 为什么 Linux 中每个目录至少有 2 个硬链接?(提示:考虑 ”.” 这个特殊目录项)
三、虚拟文件系统(VFS)
3.1 VFS 的角色
Linux 能同时挂载 ext4、XFS、NFS、procfs、tmpfs 等十几种不同的文件系统,应用程序用统一的 open/read/write/close 就能操作所有这些文件系统。这是怎么做到的?
答案是 VFS(Virtual File System,虚拟文件系统) 抽象层。
用户空间:
应用程序
open("/data/file.txt", O_RDONLY)
read(fd, buf, 1024)
|
------+---------- 系统调用边界 ----------
|
内核空间:
v
+--------------------+
| VFS 虚拟文件系统 | <-- 统一接口层
+----+----+----+-----+
| | | |
v v v v
+----+ +---+ +---+ +------+
|ext4| |XFS| |NFS| |procfs| <-- 具体文件系统实现
+----+ +---+ +---+ +------+
| | |
v v v
+----+ +---+ +--------+
|SSD | |HDD| |网络存储 | <-- 存储设备
+----+ +---+ +--------+
3.2 VFS 的四大核心对象
VFS 定义了四个核心数据结构,每种具体的文件系统都必须实现它们对应的操作方法:
+-------------------+
| superblock | 描述整个文件系统的信息
| (超级块) | 块大小、总块数、空闲块数、inode总数等
+-------------------+
|
v
+-------------------+
| inode | 描述一个文件的元数据
| (索引节点) | 大小、权限、数据块位置等
+-------------------+
|
v
+-------------------+
| dentry | 描述目录项(文件名 <-> inode 的映射)
| (目录项缓存) | 维护目录树结构,加速路径查找
+-------------------+
|
v
+-------------------+
| file | 描述一个打开的文件实例
| (文件对象) | 文件偏移量、访问模式等
+-------------------+
每个具体文件系统注册自己的操作函数:
ext4_read()
ext4_write()
xfs_read()
xfs_write()
nfs_read()
...
3.3 文件描述符与打开文件表
当进程调用 open() 时,内核会创建一系列数据结构:
进程 A 内核
+------------------+ +---------------------+
| 文件描述符表 | | 系统打开文件表 |
| (每个进程一份) | | (全局共享) |
| | | |
| fd 0 -> stdin --+----> | file 结构体 #100 |
| fd 1 -> stdout --+----> | file 结构体 #101 |---> inode
| fd 2 -> stderr --+----> | file 结构体 #102 |---> inode
| fd 3 -> 文件A --+----> | file 结构体 #200 |---> inode #12345
| fd 4 -> 文件B --+----> | file 结构体 #201 |---> inode #12346
+------------------+ +---------------------+
进程 B
+------------------+
| fd 0 -> stdin --+----> | file 结构体 #100 | (同一个stdin)
| fd 1 -> stdout --+----> | file 结构体 #101 |
| fd 3 -> 文件A --+----> | file 结构体 #300 |---> inode #12345
+------------------+ +---------------------+ (同一inode,
不同的 file 结构体, 不同的偏移)
fork() 之后子进程会继承父进程的文件描述符表,但 exec() 后标记了 close-on-exec 的文件描述符会被关闭。
四、主流文件系统对比
4.1 ext4
ext4 是 Linux 最广泛使用的文件系统,是 ext2/ext3 的演进版本:
ext4 的关键改进:
ext2 -> ext3: 增加日志 (journaling)
ext3 -> ext4: 增加 extent、延迟分配、更大的文件系统
ext4 的 extent 树:
传统方式: 文件的每个块都需要一个指针
块1 -> 物理块 100
块2 -> 物理块 101
块3 -> 物理块 102
块4 -> 物理块 103
(4个指针)
extent 方式: 连续的块只记录一个范围
[起始块=100, 长度=4]
(1个 extent 就够了!)
4.2 XFS
XFS 最初由 SGI 为 IRIX 系统开发,后来移植到 Linux。RHEL 7 及以后的默认文件系统。
XFS 特点:
- B+ 树管理元数据: 目录项、空闲空间、extent 都用 B+ 树
- 延迟分配 (delayed allocation): 数据先写入内存缓存,
等到真正要写磁盘时才分配物理块,减少碎片
- 并行 I/O: 内部按"分配组"(Allocation Group)划分,
不同 AG 可以并行操作,适合多核多线程
- 在线扩容: 可以在挂载状态下扩大文件系统
(但不支持缩小!)
4.3 Btrfs
Btrfs(B-tree File System)是 Linux 的”下一代”文件系统,借鉴了 ZFS 的设计理念:
Btrfs 核心特性:
- 写时复制 (COW): 修改数据不覆盖原始位置,写到新位置
- 快照 (Snapshot): 基于 COW,几乎零开销地创建文件系统快照
- 内置校验和: 检测和修复数据损坏 (bit rot)
- 内置 RAID: 不需要外部 RAID 控制器
- 子卷 (Subvolume): 一个文件系统中创建多个独立的文件树
- 透明压缩: 自动压缩数据节省空间
4.4 对比总结
+----------+----------+---------+----------+
| | ext4 | XFS | Btrfs |
+----------+----------+---------+----------+
| 成熟度 | 最高 | 高 | 中高* |
| 最大文件 | 16 TB | 8 EB | 16 EB |
| 最大分区 | 1 EB | 8 EB | 16 EB |
| 快照 | 不支持 | 不支持 | 原生支持 |
| 校验和 | 元数据 | 元数据 | 数据+元数据|
| 缩容 | 支持 | 不支持 | 支持 |
| 适用场景 | 通用 | 大文件 | 数据安全 |
| 默认发行版 | Ubuntu | RHEL | openSUSE/Fedora 33+ (桌面版) |
+----------+----------+---------+----------+
* Btrfs 在单盘场景已非常成熟,是 Fedora 和 openSUSE 的默认文件系统。
但其 RAID5/6 模式至今仍标记为不稳定(官方标注"not ready"),
Red Hat 也已从 RHEL 中移除 Btrfs。选用时需注意使用场景。
补充:ZFS 也是一种功能强大的写时复制文件系统,支持快照、数据校验和、RAID-Z 等特性。由于其 CDDL 许可证与 Linux 的 GPL 不兼容,ZFS 未纳入 Linux 内核主线,但通过 OpenZFS 项目在 Ubuntu 和 FreeBSD 上被广泛使用。
五、日志文件系统
5.1 为什么需要日志
假设你正在写一个文件,突然断电了。此时文件数据可能写了一半,inode 可能还没更新,目录项可能也不一致。开机后文件系统处于不一致状态。
在没有日志的年代(ext2),需要用 fsck 扫描整个磁盘来检查和修复不一致——对于大磁盘可能要花几个小时。
5.2 日志的工作原理
日志文件系统在执行写操作前,先把要做的事情记录到一块专门的日志区域:
写文件的步骤 (有日志):
第1步: 写日志 (Journal)
+--------------------------------------------------+
| 日志区域 |
| [事务开始] [inode变更] [数据块变更] [事务结束] |
+--------------------------------------------------+
第2步: 执行实际写入
- 更新 inode
- 写入数据块
- 更新目录项
第3步: 日志标记完成
+--------------------------------------------------+
| 日志区域 |
| [事务已提交] |
+--------------------------------------------------+
如果在第1步后断电:
重启后扫描日志,发现事务未完成 -> 丢弃,文件系统一致
如果在第2步中断电:
重启后扫描日志,发现事务已记录 -> 重放日志完成写入
如果在第3步后:
一切正常
5.3 三种日志模式
ext4 支持三种日志模式:
data=writeback (回写模式):
只对元数据做日志,数据直接写磁盘
最快,但数据可能不一致
风险: 断电后文件内容可能是旧数据
data=ordered (有序模式, 默认):
只对元数据做日志,但保证数据先于元数据写入磁盘
平衡了性能和安全
风险: 最小
data=journal (全日志模式):
数据和元数据都写入日志
最安全,但最慢(数据写了两次)
适用于对数据一致性要求极高的场景
🤔 想一想 数据库(如 MySQL)通常有自己的日志机制(redo log/undo log)。这种情况下,文件系统的日志模式应该选什么?为什么?
六、特殊文件系统
Linux 中还有一些”不存储在磁盘上”的特殊文件系统:
procfs (/proc):
进程和内核信息的虚拟接口
/proc/cpuinfo CPU 信息
/proc/meminfo 内存信息
/proc/<pid>/status 进程状态
/proc/<pid>/maps 进程内存映射
sysfs (/sys):
设备和驱动信息
/sys/class/net/ 网络接口
/sys/block/ 块设备
/sys/devices/ 设备树
tmpfs (/tmp, /dev/shm):
基于内存的文件系统
速度极快,重启后数据丢失
常用于临时文件和进程间共享内存
devtmpfs (/dev):
设备文件
/dev/sda 硬盘
/dev/null 黑洞设备
/dev/zero 零设备
/dev/random 随机数设备
七、文件操作实践
# 查看文件系统类型和挂载信息
df -Th
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda1 ext4 100G 45G 55G 45% /
# tmpfs tmpfs 8.0G 100M 7.9G 2% /dev/shm
# 查看 inode 使用情况
df -i
# 查看文件的 inode 信息
stat /etc/passwd
# 查看文件系统的超级块信息
sudo dumpe2fs -h /dev/sda1 | head -40
# 查看目录项
ls -lai /home/user/
# 查看文件的数据块分布
sudo filefrag -v /home/user/largefile
# 查看缓存使用情况
free -h
# 注意 buff/cache 列:这是文件系统缓存使用的内存
八、本章总结
+----------------------------------------------------+
| 文件系统核心知识 |
+----------------------------------------------------+
| |
| inode: 文件的元数据 (不含文件名) |
| 目录: 文件名 -> inode 号 的映射表 |
| 硬链接: 多个文件名 -> 同一 inode |
| 软链接: 特殊文件,内容是目标路径 |
| |
| VFS: 统一接口层 |
| superblock -> inode -> dentry -> file |
| |
| 文件系统对比: |
| ext4: 稳定通用 | XFS: 大文件高并发 |
| Btrfs: COW+快照+校验和 |
| |
| 日志文件系统: 写前记录, 断电可恢复 |
| writeback < ordered < journal |
| |
+----------------------------------------------------+
📝 结尾自测:检验你的收获
- inode 中存储了哪些信息?为什么文件名不在 inode 中?
- 打开文件 /home/user/file.txt 需要经过哪些步骤?涉及几次磁盘读取?
- 硬链接和软链接的核心区别是什么?为什么硬链接不能跨文件系统?
- VFS 的四大核心对象是什么?它们之间的关系是怎样的?
- 日志文件系统的三种模式分别是什么?默认模式为什么选择 ordered?
购买课程解锁全部内容
系统底层入门:10 章掌握操作系统核心
¥29.90