34 - 文件系统设计

文件系统是操作系统中最持久化的子系统——它负责将数据组织存储在磁盘(或 SSD)上,并为上层应用提供统一的访问接口。Linux 的 VFS(虚拟文件系统)抽象层使得系统可以同时支持数十种不同的文件系统,而用户和应用程序完全无感。本章从文件系统的设计原理出发,逐步深入到 Linux 的具体实现。


34.1 文件的概念与属性

文件是什么

文件是命名的、存储在辅助存储器上的信息集合。从用户视角看,它是数据持久化的最小逻辑单元;从操作系统视角看,它是字节序列到磁盘块的映射。

# 查看文件的完整属性
stat /etc/passwd
# File: /etc/passwd
# Size: 2897 — 文件大小(字节)
# Blocks: 8 — 分配的 512B 块数
# IO Block: 4096 — 最优 I/O 块大小
# Device: 259,2 — 所在设备(主:次 设备号)
# Inode: 12345678 — inode 编号
# Links: 1 — 硬链接数
# Access: (0644/-rw-r--r--) — 权限
# Uid: (0/root) — 所有者
# Gid: (0/root) — 所属组
# Access: 2024-01-15 — atime
# Modify: 2023-12-01 — mtime
# Change: 2023-12-01 — ctime
# Birth: - — 创建时间(ext4 不支持)
 
# 查看 inode 编号
ls -i /etc/passwd
# 12345678 /etc/passwd
 
# 查看打开该文件的进程
lsof /etc/passwd
fuser -v /etc/passwd

文件类型

# Linux 中的文件类型(通过 ls -l 第一个字符识别)
# - 普通文件
# d 目录
# l 符号链接
# c 字符设备
# b 块设备
# p 命名管道 (FIFO)
# s Unix 域套接字
 
# 创建各种文件类型
touch regular_file
mkdir my_dir
ln -s regular_file symlink
mknod my_fifo p # 命名管道
# 字符/块设备由 mknod 和 udev 管理,通常不需要手动创建

34.2 文件访问方法

三种访问方法

方法描述实现适用场景
顺序访问从文件头到文件尾按顺序读写文件指针维护当前位置日志文件、流媒体
直接访问(随机访问)跳到任意位置读写lseek() 系统调用数据库、索引文件
索引访问通过索引表查找记录位置在文件之上构建索引结构(如 B+ 树)大型数据库、ISAM 文件
# 顺序访问示例
cat access.log # 从头到尾
 
# 直接访问(随机访问)
dd if=/dev/sda of=backup.bin bs=512 count=1 skip=2048
# skip=2048: 从第 2048 个扇区开始读取(随机访问)
 
# 通过 lseek 进行随机 I/O(C 语言)
# lseek(fd, offset, SEEK_SET); // 跳到 offset 位置
# lseek(fd, 0, SEEK_END); // 跳到文件末尾

34.3 目录结构

目录结构的演化

graph TD
 subgraph "单级目录"
 S["root/"]
 S --> F1["a.txt"]
 S --> F2["b.txt"]
 S --> F3["c.txt"]
 end
 
 subgraph "树形目录(现代文件系统)"
 R["/"]
 R --> ETC["etc/"]
 R --> HOME["home/"]
 R --> VAR["var/"]
 ETC --> PASSWD["passwd"]
 HOME --> ALICE["alice/"]
 ALICE --> DOCS["docs/"]
 ALICE --> CODE["code/"]
 end

acyclic graph directory(非循环图目录)

通过符号链接和硬链接,目录结构实际上是一个有向图:

# 硬链接:共享同一个 inode
echo "data" > original
ln original hardlink
ls -i original hardlink
# 12345 hardlink
# 12345 original ← 相同的 inode 号
stat original | grep Links
# Links: 2 ← 同一个 inode 有两个名字
 
# 符号链接:独立的 inode,内容为目标路径
ln -s original symlink
ls -i original symlink
# 12345 original
# 54321 symlink ← 不同的 inode 号
readlink symlink
# original
 
# 硬链接的局限性
# - 不能链接到目录(防止循环)
# - 不能跨文件系统(inode 号是文件系统内的)

34.4 文件系统挂载

挂载原理

graph TD
 subgraph "VFS (虚拟文件系统)"
 V["/ (根文件系统 ext4)"]
 V --> V_H["/home"]
 V --> V_V["/var"]
 V --> V_M["/mnt/data ← 挂载点"]
 end
 
 subgraph "物理文件系统"
 EXT["/dev/sda2 (ext4)<br/>即 /"]
 XFS["/dev/sdb1 (xfs)<br/>即 /mnt/data"]
 end
 
 V_M -.-> XFS
 EXT --> V
# 查看当前挂载
mount | column -t
findmnt --real # 类似但更结构化
 
# 查看 /etc/fstab(启动时自动挂载配置)
cat /etc/fstab
# 格式:设备 挂载点 文件系统类型 选项 dump fsck_order
 
# 手动挂载
sudo mount -t ext4 /dev/sdb1 /mnt/data
sudo mount -o ro,noatime /dev/sdc1 /mnt/backup
 
# 卸载
sudo umount /mnt/data
# 如果设备被占用("target is busy")
sudo lsof +D /mnt/data # 查看哪些进程在使用
 
# 强制卸载(在 NFS 不可达时特别有用)
sudo umount -l /mnt/data # lazy unmount
sudo umount -f /mnt/data # force unmount

34.5 文件分配方法

三种分配策略对比

graph TD
 subgraph "连续分配"
 C1["文件 A 块0"] --- C2["文件 A 块1"] --- C3["文件 A 块2"] --- C4["空闲"] --- C5["文件 B 块0"]
 end
 
 subgraph "链式分配"
 L1["块0 → 块12"] --> L2["块12 → 块5"] --> L3["块5 → null"]
 end
 
 subgraph "索引分配 (inode)"
 I["inode<br/>━━━━━━━<br/>块0 → 15<br/>块1 → 8<br/>块2 → 22<br/>块3 → 31"]
 I --> D15["数据块 15"]
 I --> D8["数据块 8"]
 I --> D22["数据块 22"]
 I --> D31["数据块 31"]
 end
分配方法优点缺点在 Linux 中的使用
连续分配访问快(一次 seek)、简单外部碎片、文件大小不易扩展几乎不用
链式分配无外部碎片、文件易扩展随机访问慢(需遍历链)、可靠性差FAT 文件系统
索引分配支持直接访问、无外部碎片inode 占用空间、大文件需要多级索引ext4、XFS、Btrfs 等现代文件系统

34.6 空闲空间管理

四种方法

方法数据结构查找空闲Linux 实现
位图 (Bit Vector)每个块用 1 bit 表示空闲/占用O(n) 扫描,但可加速ext4 的块位图
空闲链表将空闲块串联为链表O(1) 分配,O(1) 释放少量使用
分组将空闲块地址集中存储快速找到相邻空闲块作为辅助
计数记录连续空闲块的首地址和长度支持大块连续分配ext4 的 extent tree(延伸树)
# 查看 ext4 文件系统的空闲空间
sudo dumpe2fs /dev/sda2 2>/dev/null | grep -E "Block count|Free blocks|Block size|Inode count|Free inodes"
# Block count: 26214400 (总块数)
# Free blocks: 10485760 (空闲块数)
# Block size: 4096 (块大小)
# Inode count: 6553600 (总 inode 数)
# Free inodes: 5000000 (空闲 inode 数)
 
# 文件系统级的使用情况
df -h /
df -i /
# -h: 块使用率
# -i: inode 使用率
 
# 查看 extent-based 分配(ext4 的 extent tree)
sudo debugfs -R "stat /etc/passwd" /dev/sda2 2>/dev/null | grep -A 20 EXTENTS
# Extents:
# (0-3): 123456-123459
# 表示文件从逻辑块 0 到 3,映射到物理块 123456 到 123459

34.7 磁盘调度算法

为什么需要磁盘调度

对于机械硬盘(HDD),磁头移动(seek time)是最大的性能瓶颈。磁盘调度算法通过重排序 I/O 请求来最小化总的寻道时间。SSD 没有机械部件,但仍需要 I/O 调度来管理并发请求。

算法策略特点
FCFS按请求到达顺序服务公平,但寻道距离可能很长
SSTF最短寻道时间优先贪婪,可能饥饿远端请求
SCAN(电梯算法)磁头往复扫描避免饥饿,但两端请求等待长
C-SCAN单向扫描,到头后跳回起点等待时间更均匀
LOOKSCAN 改进,只到最远请求而非最边缘更高效
C-LOOKC-SCAN 改进,只到最远请求更高效

Linux I/O 调度器

# 查看当前 I/O 调度器
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none
# 方括号表示当前使用的调度器
 
# 查看所有块设备的调度器
for dev in /sys/block/sd* /sys/block/nvme*; do
 [ -f $dev/queue/scheduler ] && echo "$(basename $dev): $(cat $dev/queue/scheduler)"
done
 
# 更改调度器
echo bfq | sudo tee /sys/block/sda/queue/scheduler
调度器全称适用场景
mq-deadlineMulti-Queue Deadline通用(SSD 和 HDD),确保请求不会超时
bfqBudget Fair QueueingHDD / 桌面系统,防止单个进程独占磁盘
kyber低延迟场景,用令牌桶调节请求速率
noneNVMe SSD,多队列并行,无需块层调度
# NVMe SSD 的最佳实践:设置 none + 高队列深度
cat /sys/block/nvme0n1/queue/nr_requests # 队列深度
echo none | sudo tee /sys/block/nvme0n1/queue/scheduler
 
# 查看 read-ahead 设置
cat /sys/block/sda/queue/read_ahead_kb
# 预读可以提高顺序读性能

34.8 Linux VFS(虚拟文件系统)

VFS 的架构

graph TD
 A["用户程序 (read/write/open/close)"] --> B["系统调用层"]
 B --> C["VFS (虚拟文件系统)"]
 C --> D1["ext4 驱动"]
 C --> D2["XFS 驱动"]
 C --> D3["Btrfs 驱动"]
 C --> D4["NFS 客户端"]
 C --> D5["procfs"]
 C --> D6["tmpfs"]
 D1 --> E1["block I/O"]
 D2 --> E1
 D3 --> E1
 D4 --> E2["网络 I/O"]
 D5 --> E3["内核内存"]
 D6 --> E3
 
 style C fill:#e1f5fe,stroke:#333,stroke-width:3px

VFS 定义了四个核心对象,它们的操作通过函数指针表实现多态:

对象数据结构作用对应内核代码
super_blockstruct super_block表示一个已挂载的文件系统fs/super.c
inodestruct inode表示一个文件(元数据+数据块指针)fs/inode.c
dentrystruct dentry目录项缓存,加速路径解析fs/dcache.c
filestruct file表示一个打开的文件(进程视角)fs/file_table.c
# 查看当前挂载的文件系统
cat /proc/mounts | column -t
 
# 查看已注册的文件系统类型
cat /proc/filesystems
 
# VFS 缓存的统计
cat /proc/sys/fs/dentry-state
# 输出:dentry entries, unused, age_limit, want_pages, ...
grep dentry /proc/slabinfo | head -5

34.9 Inode 结构详解

Inode 的直接/间接块

graph TD
 INODE["inode<br/>━━━━━━━━<br/>mode, uid, size, time...<br/>━━━━━━━━<br/>直接块0 → 数据块<br/>直接块1 → 数据块<br/>...<br/>直接块11 → 数据块<br/>━━━━━━━━<br/>间接块 → ...<br/>二级间接 → ...<br/>三级间接 → ..."]
 
 INODE --> D0["数据块"]
 INODE --> D11["数据块"]
 
 subgraph "间接块"
 INDIRECT["单级间接块<br/>指向 1024 个数据块"]
 end
 INODE --> INDIRECT
 
 subgraph "二级间接块"
 DINDIRECT["二级间接块<br/>指向 1024 个单级间接块<br/>= 1024² = 1M 个数据块"]
 end
 INODE --> DINDIRECT

以 4KB 块大小为例,ext4 inode 的可寻址范围:

索引级别指针数量可寻址块数可寻址文件大小
直接块121248 KB
单级间接10241,0244 MB
二级间接1024²1,048,5764 GB
三级间接1024³1,073,741,8244 TB
# 查看文件的 inode 信息
sudo debugfs -R "stat <12345>" /dev/sda2 2>/dev/null
# 或用 stat 查看高级属性
stat /etc/passwd
 
# 查看 ext4 文件系统的 inode 大小
sudo tune2fs -l /dev/sda2 | grep "Inode size"
# Inode size: 256 (ext4 默认为 256 字节,ext3 为 128)
 
# ext4 的 Extent Tree(替代了间接块,效率更高)
# Extent 直接记录 (逻辑块起始, 物理块起始, 连续块数) 三元组
# 比间接块更紧凑、更快

34.10 日志文件系统(Journaling)

为什么需要日志

考虑一个操作:向文件追加 4KB 数据。需要同时更新:

  1. 数据块(写入新数据)
  2. inode(更新文件大小、修改时间、块指针)
  3. 块位图(标记新块为已占用)

如果系统在步骤 2 和 3 之间崩溃 → 文件系统不一致

日志的三种模式

graph TD
 subgraph "Journal 区域"
 J["事务 T1:<br/> 1. 修改 inode 12345<br/> 2. 更新块位图<br/> 3. 修改数据块<br/>━━━━━━━━<br/>事务 T2: ..."]
 end
 
 J -->|commit| DS["数据写入<br/>实际位置"]
 
 style J fill:#fff9c4,stroke:#333
模式mount 选项记录什么安全性性能
data=journal-o data=journal元数据 + 数据都记日志最高最低(数据写两次)
data=ordered-o data=ordered (默认)仅元数据记日志;数据先于元数据写回适中
data=writeback-o data=writeback仅元数据记日志;数据可晚于元数据写回低(崩溃后文件可能含垃圾数据)最高
# 查看挂载选项(是否使用 journal)
findmnt / | grep -o 'data=[a-z]*'
# 输出 data=ordered
 
# 查看 ext4 文件系统的 journal 信息
sudo dumpe2fs /dev/sda2 2>/dev/null | grep -i journal
# Journal inode: 8
# Journal backup: inode blocks
# Journal features: journal_incompat_revoke journal_64bit
# Journal size: 128M
# Journal length: 32768 (4KB 块)
# Journal sequence: 0x00123456
 
# 查看 journal 的大小
sudo tune2fs -l /dev/sda2 | grep "Journal"
 
# XFS 日志类似但独立实现
# 查看 XFS 日志大小
sudo xfs_info /mnt/xfs 2>/dev/null | grep log

34.11 文件系统检查与修复

# fsck: 文件系统一致性检查(必须在未挂载或只读挂载时运行)
sudo fsck -n /dev/sda2 # 只检查不修复
sudo fsck -y /dev/sda2 # 自动修复所有问题
 
# 不同类型文件系统的专用工具
# ext4: fsck.ext4 或 e2fsck
# XFS: xfs_repair
# Btrfs: btrfs check
 
# 强制在下次启动时检查文件系统
sudo touch /forcefsck # 传统方式(可能不再支持)
sudo tune2fs -c 1 /dev/sda2 # 设置挂载 N 次后自动检查
 
# 查看文件系统最后一次检查时间
sudo tune2fs -l /dev/sda2 | grep "Last checked"
# Last checked: Tue Jan 15 10:30:00 2024
sudo dumpe2fs /dev/sda2 2>/dev/null | grep "Mount count"
# Mount count: 23
# Maximum mount count: -1 (禁用基于计数的检查)

34.12 文件系统命令速查

命令作用
stat FILE查看文件/目录的完整属性(inode、权限、时间戳)
ls -i显示文件的 inode 号
df -h / -i查看文件系统空间/inode 使用情况
mount | findmnt查看挂载信息
du -sh DIR查看目录/文件的磁盘占用
tune2fs -l /dev/sdx查看 ext 文件系统超级块参数
dumpe2fs /dev/sdx查看 ext 文件系统详细布局
xfs_info /mnt查看 XFS 文件系统信息
btrfs filesystem show查看 Btrfs 文件系统信息
lsof FILE查看哪些进程打开了该文件
fuser -v FILE同上(轻量级)
filefrag FILE查看文件的磁盘碎片情况

相关链接