文件系统与 I/O
1. 文件抽象
1.1 文件的本质
文件是操作系统提供的持久化数据抽象——对用户呈现为命名的字节序列,屏蔽底层存储介质的物理细节。
文件元数据:
| 属性 | 内容 |
|---|---|
| 名称 | 人类可读的标识符 |
| 类型 | 普通文件、目录、符号链接、设备文件等 |
| 大小 | 字节数 |
| 权限 | owner/group/others 的 rwx |
| 时间戳 | 创建时间、修改时间、访问时间 |
| 所有者 | UID / GID |
| 位置 | 数据块在磁盘上的位置 |
1.2 文件操作
基本系统调用:
open() → 打开文件,返回文件描述符 fd
read() → 从 fd 读取数据
write() → 向 fd 写入数据
lseek() → 移动文件偏移量(随机访问)
close() → 关闭 fd
stat() → 获取元数据
文件描述符(fd):进程级整数索引,指向内核中的打开文件表条目。
进程 fd 表 → 系统打开文件表(偏移量、访问模式)→ inode 表(元数据+数据位置)
2. 目录结构
2.1 目录的本质
目录是一种特殊文件,内容是文件名 → inode 号的映射表。
2.2 目录组织
| 结构 | 特点 |
|---|---|
| 单级目录 | 所有文件在同一目录(命名冲突) |
| 两级目录 | 每用户一个目录 |
| 树形目录 | 层级结构(现代标准) |
| 无环图目录 | 支持硬链接和符号链接 |
2.3 路径解析
以 /home/user/file.txt 为例:
- 从根目录 inode(inode 2)开始
- 读取根目录数据块,查找
home→ 得到 inode 号 - 读取
home的 inode,获取数据块 - 在数据块中查找
user→ 得到 inode 号 - 重复直到找到
file.txt的 inode
每一级都需要读取 inode + 读取目录数据块→ 深层路径的访问代价高。
2.4 链接
| 类型 | 机制 | 特点 |
|---|---|---|
| 硬链接 | 多个目录项指向同一 inode | 不能跨文件系统,不能链接目录 |
| 符号链接 | 特殊文件,内容是目标路径字符串 | 可跨文件系统,可链接目录 |
inode 的链接计数(link count)归零时才释放数据块。
3. 磁盘空间分配
3.1 连续分配(Contiguous Allocation)
每个文件占用一段连续的磁盘块。
| 优势 | 劣势 |
|---|---|
| 顺序和随机访问都很快 | 外部碎片严重 |
| 实现简单(起始块 + 长度) | 文件增长困难 |
适用于只读介质(如 CD-ROM 的 ISO 9660)。
3.2 链表分配(Linked Allocation)
每个磁盘块包含指向下一块的指针。
| 优势 | 劣势 |
|---|---|
| 无外部碎片 | 随机访问极慢(\(O(n)\) 寻道) |
| 文件增长容易 | 指针占用空间 |
FAT(File Allocation Table):将链表指针集中存储在一张表中,改善随机访问。
3.3 索引分配(Indexed Allocation)
每个文件有一个索引块,存放所有数据块的地址。
Unix inode 的多级索引:
inode:
├── 直接指针 ×12 → 直接指向数据块
├── 一级间接指针 ×1 → 指向索引块 → 数据块
├── 二级间接指针 ×1 → 索引块 → 索引块 → 数据块
└── 三级间接指针 ×1 → 索引块 → 索引块 → 索引块 → 数据块
假设块大小 4 KB、指针 4 字节(每块可存 1024 个指针):
| 级别 | 可寻址块数 | 可寻址大小 |
|---|---|---|
| 直接(12 个) | 12 | 48 KB |
| 一级间接 | 1024 | 4 MB |
| 二级间接 | \(1024^2\) | 4 GB |
| 三级间接 | \(1024^3\) | 4 TB |
优势:小文件访问快(直接指针),大文件也能支持。
3.4 Extent-Based 分配
现代文件系统(ext4、XFS、Btrfs)使用 extent——连续块的描述符 (起始块, 长度):
- 一个 extent 可描述大段连续空间
- 减少元数据开销
- 提升顺序 I/O 性能
4. 空闲空间管理
| 方法 | 数据结构 | 特点 |
|---|---|---|
| 位图(Bitmap) | 每位对应一个块(0=空闲,1=占用) | 紧凑,查找效率取决于搜索算法 |
| 空闲链表 | 空闲块链接成链表 | 分配快,但遍历慢 |
| 空闲块组 | 记录连续空闲块段 | 适合 extent-based 文件系统 |
| B+ 树 | 以空闲区间为键 | XFS、Btrfs 使用,查找 \(O(\log n)\) |
5. 现代文件系统
| 文件系统 | 特点 | 使用场景 |
|---|---|---|
| ext4 | 日志、extent、延迟分配 | Linux 默认 |
| XFS | B+ 树索引、高并发、大文件优化 | 大数据、服务器 |
| Btrfs | COW、快照、校验和、RAID | 高级 Linux 特性 |
| ZFS | 存储池、端到端校验、快照、去重 | FreeBSD、存储服务器 |
| NTFS | MFT、ACL、加密 | Windows |
| APFS | COW、快照、加密、空间共享 | macOS/iOS |
日志文件系统(Journaling)
防止崩溃导致文件系统不一致:
| 模式 | 日志内容 | 性能 | 安全性 |
|---|---|---|---|
| Journal(全日志) | 元数据 + 数据 | 最慢 | 最高 |
| Ordered(有序) | 仅元数据(先写数据再写日志) | 中等 | 高 |
| Writeback(回写) | 仅元数据(数据和日志顺序不定) | 最快 | 中等 |
ext4 默认使用 ordered 模式。
COW(Copy-on-Write)文件系统
Btrfs、ZFS 的核心设计:
- 永不原地修改数据——总是写到新位置
- 天然支持快照(只需保留旧指针)
- 天然支持数据一致性(写入是原子的)
- 代价:可能产生碎片化
6. RAID
RAID(Redundant Array of Independent Disks):用多块磁盘组合实现高性能和/或高可靠性。
| RAID 级别 | 结构 | 容量利用率 | 容错 | 读性能 | 写性能 |
|---|---|---|---|---|---|
| RAID 0 | 条带化(Striping) | 100% | 无 | \(N\times\) | \(N\times\) |
| RAID 1 | 镜像(Mirroring) | 50% | 1 盘故障 | \(N\times\) | \(1\times\) |
| RAID 5 | 分布式奇偶校验 | \((N-1)/N\) | 1 盘故障 | \((N-1)\times\) | 写放大(需读-改-写校验) |
| RAID 6 | 双重校验(P+Q) | \((N-2)/N\) | 2 盘故障 | \((N-2)\times\) | 写放大更严重 |
| RAID 10 | 先镜像再条带化 | 50% | 每对中 1 盘 | \(N\times\) | \(N/2\times\) |
RAID 不是备份
RAID 提供硬件故障容错,但不防止人为删除、软件 bug、勒索软件。始终需要独立备份。
7. I/O 系统
7.1 I/O 方式
| 方式 | 机制 | CPU 参与度 | 适用场景 |
|---|---|---|---|
| 程序控制 I/O(Polling) | CPU 轮询设备状态寄存器 | 持续占用 | 低延迟、简单设备 |
| 中断驱动 I/O | 设备完成后发中断通知 CPU | 仅在开始和完成时 | 通用 |
| DMA(Direct Memory Access) | DMA 控制器直接在设备和内存间传输 | 仅在开始和完成时 | 大块数据传输 |
DMA 流程:
- CPU 设置 DMA 控制器(源地址、目标地址、传输大小)
- DMA 控制器执行数据传输(CPU 可做其他工作)
- 传输完成 → DMA 控制器发中断通知 CPU
7.2 I/O 调度器
磁盘调度(HDD)
磁盘寻道时间是主要延迟来源。调度算法优化磁头移动:
| 算法 | 策略 | 特点 |
|---|---|---|
| FCFS | 按请求到达顺序 | 公平但寻道距离大 |
| SSTF | 选最近的请求(Shortest Seek Time First) | 可能饥饿 |
| SCAN(电梯算法) | 磁头单向扫描到底再反向 | 中等等待时间 |
| C-SCAN(循环扫描) | 单向扫描到底,跳回起点 | 更公平 |
| LOOK / C-LOOK | 只到最远请求而非磁盘边界 | SCAN 的改进版 |
现代 Linux I/O 调度器
- mq-deadline:保证请求在截止时间内完成(默认 HDD)
- BFQ(Budget Fair Queueing):桌面交互响应优化
- none(noop):适合 SSD(无需寻道优化)
SSD 特性
| 特性 | 与 HDD 的区别 |
|---|---|
| 无寻道延迟 | 随机读写性能远优于 HDD |
| 写入放大 | 写前需擦除(erase block > write page) |
| 磨损均衡 | FTL 控制器分散写入,延长寿命 |
| TRIM | 通知 SSD 哪些块不再使用,优化 GC |
| 并行通道 | 多通道/多 Die 并行提升带宽 |
理想 WAF = 1,实际通常 2-10(取决于工作负载和 GC 效率)。
7.3 I/O 软件栈
用户空间: 应用程序 → 标准库 (stdio)
─────── 系统调用边界 ───────
内核空间: VFS (虚拟文件系统层)
→ 具体文件系统 (ext4/XFS/...)
→ Page Cache (页面缓存)
→ Block I/O 层 (I/O 调度)
→ 设备驱动
→ 硬件控制器
硬件: 磁盘 / SSD / NVMe
7.4 Page Cache
Linux 使用空闲内存作为页面缓存:
- 读缓存:磁盘数据读入内存后保留,后续读命中缓存
- 写缓存:写操作先写入 Page Cache(dirty page),后台刷盘
- 预读(Read-ahead):检测顺序读模式,提前读入后续数据
读路径: 应用 → Page Cache 命中? → 是: 直接返回
→ 否: 从磁盘读入 Page Cache → 返回
写路径: 应用 → 写入 Page Cache (标记 dirty)
→ pdflush/writeback 线程定期刷盘
→ 或 fsync() 强制刷盘
7.5 零拷贝(Zero-Copy)
传统文件发送:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡(4 次拷贝)。
sendfile() 系统调用:磁盘 → 内核缓冲区 → 网卡(2 次拷贝),跳过用户空间。
mmap + write:另一种减少拷贝的方式。
8. 存储接口演进
| 接口 | 协议 | 带宽 | 延迟 | 队列深度 |
|---|---|---|---|---|
| SATA | AHCI | ~600 MB/s | ~100 μs | 32 |
| SAS | SCSI | ~2.4 GB/s | ~100 μs | 256 |
| NVMe (PCIe 4.0) | NVMe | ~7 GB/s | ~10 μs | 65535 |
| NVMe (PCIe 5.0) | NVMe | ~14 GB/s | ~10 μs | 65535 |
NVMe 的优势:专为闪存设计,多队列(每 CPU 核心独立队列)、低延迟、高并行。