跳到主要内容
预计阅读 30 分钟

06|文件系统

硬盘上原本只有一串串 0 和 1,是文件系统把它们组织成了我们熟悉的”文件夹+文件”结构。本章将从 inode 开始,带你理解 VFS 抽象层、ext4/XFS/Btrfs 的设计思想,以及日志文件系统如何保证数据安全。

📋 开篇自测:你已经知道多少?

  1. 文件名和文件内容在磁盘上是存在一起的吗?inode 是什么?
  2. 什么是 VFS?为什么 Linux 能同时支持 ext4、XFS、NFS 等不同文件系统?
  3. 日志文件系统解决了什么问题?

一、文件系统的基本概念

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                   |
|                                                    |
+----------------------------------------------------+

📝 结尾自测:检验你的收获

  1. inode 中存储了哪些信息?为什么文件名不在 inode 中?
  2. 打开文件 /home/user/file.txt 需要经过哪些步骤?涉及几次磁盘读取?
  3. 硬链接和软链接的核心区别是什么?为什么硬链接不能跨文件系统?
  4. VFS 的四大核心对象是什么?它们之间的关系是怎样的?
  5. 日志文件系统的三种模式分别是什么?默认模式为什么选择 ordered?

购买课程解锁全部内容

系统底层入门:10 章掌握操作系统核心

¥29.90