05 - 文件系统¶
⏰ 建议学习时间:2.5 小时 🎯 难度等级:⭐⭐⭐ 📋 前置知识:内存管理基础概念、磁盘 I/O 基础
📋 本章目录¶
- 一、文件概念与属性
- 二、文件物理结构
- 三、目录实现
- 四、磁盘空间管理
- 五、VFS 虚拟文件系统层
- 六、日志文件系统
- 七、常见文件系统对比
- 八、Buffer Cache 与 Page Cache
- 九、Linux 文件系统实战
- 十、面试高频题
- 十一、练习题
一、文件概念与属性¶
1.1 文件的定义¶
文件(File) 是操作系统对存储设备上数据的逻辑抽象——一个命名的、按字节序列组织的持久化数据集合。对用户而言,文件是信息存储和组织的基本单元。
1.2 inode 详解¶
在 Unix/Linux 文件系统中,inode(Index Node,索引节点) 是文件系统的核心数据结构,存储文件的所有元数据(不含文件名)。
inode 结构
┌────────────────────────────────┐
│ 文件类型(普通文件/目录/链接…) │
│ 权限(rwxr-xr-x) │
│ 拥有者 UID / 组 GID │
│ 文件大小(字节) │
│ 时间戳: │
│ - atime(最后访问时间) │
│ - mtime(最后修改时间) │
│ - ctime(元数据变更时间) │
│ 硬链接计数 │
│ 数据块指针: │
│ - 12个直接指针 │
│ - 1个一级间接指针 │
│ - 1个二级间接指针 │
│ - 1个三级间接指针 │
└────────────────────────────────┘
💡 文件名存在目录文件中,而非 inode 中。目录是一张"文件名 → inode 号"的映射表。
1.3 inode 与数据块指针¶
以 ext4 为例(块大小 4KB,指针 4 字节):
| 指针类型 | 指向数据块数 | 寻址范围 |
|---|---|---|
| 12 个直接指针 | 12 块 | 48 KB |
| 一级间接 | 1024 块 | 4 MB |
| 二级间接 | 1024 × 1024 块 | 4 GB |
| 三级间接 | 1024^3 块 | 4 TB |
inode
┌──────────┐
│直接指针0 │──→ [数据块]
│直接指针1 │──→ [数据块]
│ ... │
│直接指针11 │──→ [数据块]
├──────────┤
│一级间接 │──→ [指针块] ──→ [数据块],[数据块],[数据块]...
├──────────┤
│二级间接 │──→ [指针块] ──→ [指针块] ──→ [数据块]...
├──────────┤
│三级间接 │──→ [指针块] ──→ [指针块] ──→ [指针块] ──→ [数据块]...
└──────────┘
💡 ext4 使用 Extent(区段) 替代传统的间接指针方式,一个 Extent 记录"起始块号 + 连续块数",大幅减少元数据开销,尤其对大文件效果显著。
1.4 硬链接与软链接¶
| 特性 | 硬链接 | 软链接(符号链接) |
|---|---|---|
| 创建命令 | ln file hardlink | ln -s file softlink |
| inode | 与原文件相同 inode | 新建一个独立 inode |
| 跨文件系统 | 不能 | 可以 |
| 原文件删除后 | 仍可访问(链接计数 > 0) | 失效(悬空链接) |
| 能否链接目录 | 不能(避免环路) | 可以 |
二、文件物理结构¶
文件在磁盘上的数据块如何组织?三种经典方案:
2.1 三种分配方式对比¶
| 特性 | 连续分配 | 链接分配 | 索引分配 |
|---|---|---|---|
| 原理 | 每个文件占据一组连续磁盘块 | 每个块包含指向下一块的指针 | 一个索引块存储所有数据块号 |
| 顺序访问 | ★★★ 极快 | ★★☆ 顺序遍历链表 | ★★☆ 需查索引 |
| 随机访问 | ★★★ 直接计算偏移 | ★☆☆ 必须遍历链表 | ★★★ 索引直接定位 |
| 空间利用率 | 差(外部碎片严重) | 好(无外部碎片) | 好 |
| 文件增长 | 困难(可能需要搬迁) | 简单(追加新块) | 中等(索引块可能溢出) |
| 可靠性 | 高 | 低(一个指针损坏则后续丢失) | 中 |
| 典型应用 | CD-ROM、磁带 | 早期 FAT 文件系统 | Unix/Linux(inode) |
2.2 连续分配¶
磁盘:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ A0 │ A1 │ A2 │ │ B0 │ B1 │ │ │
└────┴────┴────┴────┴────┴────┴────┴────┘
文件 A:起始块=0,长度=3
文件 B:起始块=4,长度=2
2.3 链接分配¶
文件 A 的块链:
block 2 → block 7 → block 4 → block 12 → NULL
│ │ │ │
[数据|7] [数据|4] [数据|12] [数据|NULL]
FAT(File Allocation Table) 是对链接分配的改进:将所有块的"下一块"指针集中存放在 FAT 表中,加速随机访问。
2.4 索引分配¶
索引块(索引节点):
┌─────────────┐
│ 块号: 4 │
│ 块号: 7 │
│ 块号: 2 │
│ 块号: 12 │
│ ... │
└─────────────┘
三、目录实现¶
目录本身也是一种特殊文件,存储"文件名 → 文件属性"的映射。
3.1 线性列表¶
最简单的实现:按顺序存储所有的 <文件名, inode号> 对。
目录文件内容(线性列表):
┌──────────────┬────────┐
│ 文件名 │ inode号 │
├──────────────┼────────┤
│ hello.c │ 1024 │
│ readme.txt │ 2048 │
│ Makefile │ 512 │
│ ... │ ... │
└──────────────┴────────┘
- 查找:O(n) 线性扫描
- 创建/删除:需检查重名,O(n)
3.2 哈希表¶
为目录文件名建立哈希表,加速查找。
- 查找:近似 O(1)
- 缺点:需处理哈希冲突,表大小固定时扩展麻烦
💡 ext4 使用 HTree(Hash Tree)——基于 B 树的哈希索引结构,在大目录(数万文件)中保持高效查找。
四、磁盘空间管理¶
如何跟踪磁盘上哪些块空闲、哪些已分配?
4.1 位图法(Bitmap)¶
每个磁盘块对应一个 bit:0 表示空闲,1 表示已占用。
- 优点:简单高效,适合查找连续空闲块
- 缺点:位图本身占用空间(1TB 磁盘 / 4KB 块 = 32MB 位图)
4.2 空闲链表¶
将所有空闲块链成一个链表,表头指针指向第一个空闲块。
- 优点:不浪费额外空间
- 缺点:分配连续块效率低,遍历开销大
4.3 成组链接法(Unix 经典方法)¶
将空闲块分组管理。超级块中存放第一组空闲块号,第一组的第一个块存放下一组的块号,依此类推。
超级块 组1首块 组2首块
┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ 空闲块数: 100 │ │ 空闲块数: 100│ │ 空闲块数:│
│ 块号列表: │ │ 块号列表: │ │ 块号列表 │
│ [201]→ │──tail──→ │ [301]→ │──→ │ ... │
│ [202] │ │ [302] │ └──────────┘
│ [203] │ │ [303] │
│ ... │ │ ... │
└──────────────┘ └──────────────┘
- 优点:兼顾位图法和链表法的优势,批量分配/释放高效
五、VFS 虚拟文件系统层¶
VFS(Virtual File System) 是 Linux 内核中的抽象层,为用户空间提供统一的文件操作接口,屏蔽底层不同文件系统的差异。
用户空间
─────────────────────────────────────
系统调用接口:open(), read(), write(), close()
─────────────────────────────────────
│
▼
┌────────────────────────────────┐
│ VFS 层 │ ← 统一抽象
│ super_block / inode / │
│ dentry / file 四大对象 │
└────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ ext4 │ │ XFS │ │ NFS │ ← 具体文件系统实现
└──────┘ └──────┘ └──────┘
│ │ │
▼ ▼ ▼
[本地磁盘] [本地磁盘] [网络]
VFS 四大核心对象¶
| 对象 | 说明 |
|---|---|
| super_block | 描述一个已挂载的文件系统(块大小、inode 总数、魔数等) |
| inode | VFS 层的 inode 抽象,代表一个文件的元数据 |
| dentry | 目录项缓存(路径组件 → inode 的映射),加速路径查找 |
| file | 已打开的文件对象,包含文件偏移量、访问模式等 |
六、日志文件系统¶
6.1 问题背景¶
如果在文件写入过程中突然断电或系统崩溃,文件系统可能处于不一致状态。传统方法(fsck)在系统重启后全盘扫描修复——在大磁盘上可能耗费数小时。
6.2 WAL(Write-Ahead Logging)原理¶
日志文件系统借鉴数据库的 WAL 思想:
写操作流程:
① 将修改操作写入日志区(Journal) ← Write-Ahead
② 日志写入完成后,提交事务(Commit)
③ 将数据实际写入文件系统对应位置
④ 数据写入成功后,标记日志条目完成(删除/回收)
崩溃恢复: - 系统重启后只需检查日志区 - 已提交但未写入实际位置的操作 → 重做(Redo) - 未提交的操作 → 丢弃 - 恢复速度:秒级(vs 传统 fsck 的小时级)
6.3 日志模式¶
以 ext4 为例:
| 模式 | 说明 | 性能 | 安全性 |
|---|---|---|---|
| journal | 元数据 + 数据都写日志 | 最慢 | 最高 |
| ordered(默认) | 仅元数据写日志,但保证数据先写 | 中等 | 较高 |
| writeback | 仅元数据写日志,数据无序写入 | 最快 | 较低 |
七、常见文件系统对比¶
| 特性 | ext4 | XFS | NTFS | ZFS | Btrfs |
|---|---|---|---|---|---|
| 操作系统 | Linux | Linux | Windows | FreeBSD/Linux | Linux |
| 最大文件大小 | 16 TB | 8 EB | 16 TB | 16 EB | 16 EB |
| 最大卷大小 | 1 EB | 8 EB | 256 TB | 256 ZB | 16 EB |
| 日志支持 | ✅ | ✅ | ✅ | ✅(ZIL) | ✅ |
| 快照 | ❌ | ❌ | ✅(VSS) | ✅(COW) | ✅(COW) |
| 数据校验 | 元数据 | 元数据 | ❌ | ✅(端到端) | ✅ |
| 压缩 | ❌ | ❌ | ✅ | ✅ | ✅ |
| 去重 | ❌ | ❌ | ❌ | ✅ | ✅(离线) |
| RAID 支持 | 需 mdadm | 需 mdadm | 需外部 | 原生(RAID-Z) | 原生 |
| 在线扩容 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 典型场景 | 通用 Linux | 大文件/数据库 | Windows 桌面 | NAS/存储服务器 | 高级 Linux 存储 |
八、Buffer Cache 与 Page Cache¶
8.1 历史与统一¶
- Buffer Cache:缓存磁盘块(block),以块设备为单位
- Page Cache:缓存文件页(page),以文件页面为单位
💡 Linux 2.4 之后,两者已统一为 Page Cache。在
/proc/meminfo中看到的Buffers实际上是 Page Cache 中用于追踪块设备元数据的部分。
8.2 读写流程¶
读操作:
应用 read() → 内核检查 Page Cache
├── 命中 → 直接返回(零磁盘 I/O)
└── 未命中 → 从磁盘读入 Page Cache → 返回给应用
写操作(writeback 模式):
应用 write() → 写入 Page Cache(标记为 Dirty)→ 立即返回
│
└── 后台 pdflush/writeback 线程定期将脏页写回磁盘
脏页回写时机: - 脏页超过阈值(dirty_ratio,默认 20%) - 脏页存在时间超过阈值(dirty_expire_centisecs,默认 30 秒) - 用户显式调用 sync / fsync - 内存紧张时
# 查看脏页回写参数
cat /proc/sys/vm/dirty_ratio # 脏页占可用内存比例上限
cat /proc/sys/vm/dirty_background_ratio # 后台开始回写的阈值
cat /proc/sys/vm/dirty_expire_centisecs # 脏页过期时间(厘秒)
九、Linux 文件系统实战¶
9.1 inode 操作¶
# 查看文件的 inode 信息
stat myfile.txt
# 输出示例:
# File: myfile.txt
# Size: 1024 Blocks: 8 IO Block: 4096 regular file
# Device: 801h/2049d Inode: 1234567 Links: 1
# Access: (0644/-rw-r--r--) Uid: (1000/user) Gid: (1000/user)
# Access: 2025-01-15 10:30:00
# Modify: 2025-01-15 09:00:00
# Change: 2025-01-15 09:00:00
# 查看文件系统 inode 使用情况
df -i
# 通过 inode 号查找文件
find / -inum 1234567
# 列出文件的 inode 号
ls -i myfile.txt
9.2 df —— 磁盘空间使用情况¶
df -hT
# 输出:
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda1 ext4 50G 25G 23G 53% /
# tmpfs tmpfs 7.8G 1.2G 6.6G 16% /dev/shm
# /dev/sdb1 xfs 200G 120G 80G 60% /data
| 参数 | 说明 |
|---|---|
-h | 人性化显示(KB/MB/GB) |
-T | 显示文件系统类型 |
-i | 显示 inode 使用情况 |
9.3 du —— 目录/文件空间占用¶
# 查看当前目录各子目录大小
du -sh *
# 查看某目录总大小
du -sh /var/log
# 找出占空间最多的前10个目录
du -h --max-depth=1 / 2>/dev/null | sort -rh | head -10
9.4 mount —— 挂载文件系统¶
# 挂载磁盘分区
mount /dev/sdb1 /mnt/data
# 以只读方式挂载
mount -o ro /dev/sdb1 /mnt/data
# 查看所有挂载点
mount | column -t
# 挂载 ISO 文件
mount -o loop image.iso /mnt/iso
# 卸载
umount /mnt/data
9.5 lsof —— 列出打开的文件¶
# 查看某个文件被哪些进程打开
lsof /var/log/syslog
# 查看某个进程打开的所有文件
lsof -p 1234
# 查看某个端口关联的进程
lsof -i :8080
# 查看已删除但仍被占用的文件(磁盘不释放的元凶)
lsof +L1
9.6 stat —— 详细文件信息¶
# 查看文件详细元数据
stat /etc/passwd
# 自定义输出格式
stat --format='Name: %n, Size: %s, Inode: %i, Links: %h' myfile.txt
9.7 实用排查场景¶
# 场景1:磁盘满了但找不到大文件——检查已删除但未释放的文件
lsof +L1 | grep deleted
# 场景2:inode 耗尽(df 显示有空间但无法创建文件)
df -i # 检查 IUse%
# 场景3:检查文件系统类型
lsblk -f
十、面试高频题 📋¶
题目 1:什么是 inode?它与文件名的关系?¶
答:inode 是文件系统中存储文件元数据的数据结构,包含文件大小、权限、时间戳、数据块指针等一切信息——除了文件名。文件名存储在目录文件中,目录是一张"文件名 → inode 号"的映射表。一个 inode 可对应多个文件名(硬链接),但一个文件名只对应一个 inode。
题目 2:硬链接和软链接的区别?¶
答:硬链接与原文件共享同一个 inode,删除原文件后硬链接仍可访问,不能跨文件系统、不能链接目录。软链接(符号链接)有独立的 inode,存储的是目标文件的路径字符串,可跨文件系统、可链接目录,但原文件删除后软链接失效(悬空链接)。
题目 3:连续分配、链接分配、索引分配的优缺点?¶
答:连续分配——顺序和随机访问都快,但有外部碎片且文件难以增长。链接分配——无外部碎片、文件可任意增长,但随机访问需遍历链表、可靠性差。索引分配——支持高效随机访问、无外部碎片,但索引块有额外空间开销。现代文件系统(ext4)多采用索引分配的变种。
题目 4:什么是 VFS?为什么需要它?¶
答:VFS(Virtual File System)是 Linux 内核中的抽象层,为所有文件系统(ext4、XFS、NFS 等)提供统一的接口。用户程序调用 open()/read()/write() 等系统调用时,VFS 将请求分发到具体文件系统的实现。这样应用程序无需关心底层文件系统类型,实现了"一切皆文件"的理念。
题目 5:什么是日志文件系统?为什么比传统文件系统更安全?¶
答:日志文件系统在实际修改文件系统元数据之前,先将修改操作写入日志区(Write-Ahead Logging)。如果系统崩溃,重启后只需重放日志即可恢复一致性(秒级),无需像传统 fsck 那样全盘扫描修复(可能数小时)。ext4 默认使用 ordered 日志模式。
题目 6:解释 Page Cache 的作用和脏页回写机制¶
答:Page Cache 是内核在内存中缓存文件数据的机制,将最近访问的文件页保留在内存中。读操作优先从 Page Cache 获取(命中则零磁盘 I/O);写操作先写入 Page Cache 并标记为脏页,后台线程定期或在特定条件下将脏页写回磁盘。这极大提高了 I/O 性能,但异常断电可能丢失未回写的数据。
题目 7:磁盘空间满了但 du 显示占用不多,可能是什么原因?¶
答:最常见的原因是已删除但仍被进程打开的文件。文件已在目录中移除(du 看不到),但由于进程仍持有文件描述符,磁盘空间未释放。用 lsof +L1 | grep deleted 查找此类文件。解决方法:重启相关进程或让其关闭文件描述符。另一种可能是 inode 耗尽(df -i 检查)。
题目 8:ext4 与 XFS 如何选择?¶
答:ext4 成熟稳定、兼容性好、适合通用场景,是大多数 Linux 发行版的默认文件系统。XFS 在大文件、高并发 I/O 场景表现更好(如数据库、视频存储),支持在线碎片整理和并行 I/O。一般桌面和中小服务器使用 ext4,大规模存储和高 I/O 需求选 XFS。
题目 9:什么是 ZFS 的 COW(Copy-On-Write)?有什么好处?¶
答:COW 是 ZFS/Btrfs 的核心写入策略:修改数据时不覆盖原有块,而是写入新位置,然后更新指针。好处:① 天然的快照支持——快照只需保留旧指针;② 数据一致性——崩溃后旧数据完整;③ 端到端校验——每个块有校验和,读取时自动校验。缺点是随机写性能可能下降、碎片化。
题目 10:解释 "一切皆文件" 的含义¶
答:这是 Unix/Linux 的核心设计哲学。不仅普通文件和目录,设备(/dev/sda)、进程信息(/proc)、网络套接字、管道等都被抽象为文件,通过统一的 open/read/write/close 接口操作。这简化了系统设计和编程——程序只需掌握一套 I/O 接口即可与各种资源交互。
十一、练习题 ✏️¶
练习 1:inode 计算¶
一个 ext4 文件系统块大小为 4KB,指针大小为 4 字节,每个 inode 有 12 个直接指针、1 个一级间接指针、1 个二级间接指针、1 个三级间接指针。 1. 单个文件最大可以有多少个数据块? 2. 单个文件最大大小约为多少?
练习 2:空间管理¶
假设一个磁盘有 100,000 个块,使用位图法管理空闲块: 1. 位图需要多少存储空间? 2. 如果要分配连续的 10 个空闲块,描述查找过程。
练习 3:文件操作实战¶
在 Linux 系统上完成以下操作: 1. 创建一个文件 test.txt 并写入内容 2. 创建一个硬链接和一个软链接 3. 用 stat 命令分别查看三者的 inode、链接数 4. 删除原始文件后,分别测试硬链接和软链接是否可用
练习 4:日志模式对比¶
分别用 journal、ordered、writeback 三种日志模式挂载一个 ext4 文件系统,用 dd 写入 1GB 文件,比较写入时间。思考为什么 journal 模式最慢。
练习 5:排查磁盘问题¶
编写一个 Shell 脚本,实现以下功能:
#!/bin/bash
# 磁盘健康检查脚本
# 1. 显示所有文件系统使用率
echo "=== 磁盘空间 ==="
df -hT
# 2. 显示 inode 使用率
echo "=== inode 使用 ==="
df -i
# 3. 找出占空间最大的前10个目录
echo "=== 最大目录 ==="
du -h --max-depth=1 / 2>/dev/null | sort -rh | head -10 # |管道:将前一命令的输出作为后一命令的输入
# 4. 检查已删除但未释放的文件
echo "=== 已删除未释放 ==="
lsof +L1 2>/dev/null | grep deleted # grep文本搜索:按模式匹配行
📚 延伸阅读¶
- 《操作系统概念》(恐龙书)第 11-13 章 —— 文件系统
- 《深入理解 Linux 内核》第 12 章 —— 虚拟文件系统
- 《Linux 内核设计与实现》第 13 章 —— 虚拟文件系统
- ext4 官方文档
- Linux VFS 内核文档
- ZFS on Linux