Skip to content

文件系统 ​

#系统 · #文件系统 · #VFS · #ext4 · #XFS · #COW · #inode

深入理解 Unix VFS、四大主流文件系统对比、COW 原理与实际使用指南。


1. Unix VFS 架构 ​

1.1 VFS 如何统一不同文件系统 ​

mermaid
graph TD
    APP["用户程序 (open/read/write/close)"]
    APP --> VFS["VFS 层<br/>file_operations 虚函数表"]
    VFS --> EXT4["ext4 实现<br/>ext4_file_operations"]
    VFS --> XFS["XFS 实现<br/>xfs_file_operations"]
    VFS --> BTRFS["Btrfs 实现<br/>btrfs_file_operations"]
    VFS --> NFS["NFS 实现<br/>nfs_file_operations"]
    VFS --> PROC["procfs 实现"]
    VFS --> FUSE["FUSE 用户态实现"]
c
// VFS 的核心抽象 — 每种文件系统实现自己的操作函数
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);  // 现代接口
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    // ... 更多
};

// 当用户调用 write(fd, buf, n) 时:
// sys_write → vfs_write → file->f_op->write_iter()
//   → generic_perform_write()
//     → 写入 page cache (用户页复制到内核页)
//     → 标记页面为 dirty
//   → 返回用户态 (异步!此时数据在内存中)

// 稍后 (由内核决定时机):
//   → writeback 线程
//     → 文件系统的 writepages()
//       → 构造 bio (block I/O 请求)
//         → submit_bio() 提交给块设备驱动
//           → 块设备驱动 → 磁盘

// 关键点: write() 返回成功 ≠ 数据已落盘!
// fsync(fd) / fdatasync(fd) 才确保落盘
// O_DIRECT 打开的文件绕过 page cache

1.2 VFS 的四层抽象 ​

mermaid
flowchart TB
    User["用户程序 write(fd, buf, n)"]
    User --> VFS["VFS 层<br/>file_operations 虚函数表<br/>(统一接口, 多态)"]
    VFS --> PageCache["Page Cache<br/>内存缓存, 延迟写回<br/>(buffer_head / folio)"]
    PageCache --> FS["具体文件系统<br/>ext4 / XFS / Btrfs"]
    FS --> Block["通用块层 (bio)<br/>I/O调度 → 合并/排序"]
    Block --> Driver["设备驱动<br/>NVMe / SCSI / virtio"]
    Driver --> Disk["物理磁盘"]

    PageCache -..->|"O_DIRECT<br/>绕过"| Block

1.3 Page Cache vs Direct I/O — 什么时候绕过缓存 ​

这是文件系统最常见的性能决策:

维度Page Cache (缓冲 I/O)Direct I/O (O_DIRECT)
缓存命中✅ 热数据极快(内存延迟 ~100ns)❌ 每次都读磁盘
写入延迟✅ 低(write 立即返回,异步落盘)🟡 中等(等待 DMA 完成)
数据一致性❌ write()≠持久化,需 fsync✅ DMA 完成后数据已落盘
内存占用❌ 占用大量页缓存(抢占应用内存)✅ 不消耗 Page Cache
预读优化✅ 内核自动顺序预读❌ 完全由用户控制
适用场景通用文件读写、小 I/O、热数据数据库(Buffer Pool 自己管)、大文件流
bash
# MySQL/PostgreSQL 的建议
# InnoDB 有 Buffer Pool → 用 O_DIRECT 避免双重缓存
# 否则同一页数据既在 DB 的 Buffer Pool,又在 OS 的 Page Cache
# 浪费内存,且 flush 时两次复制

# fio 测试对比
fio --name=test --rw=randread --bs=4k --size=1G \
    --direct=0  # 缓冲 I/O(通过 Page Cache)
fio --name=test --rw=randread --bs=4k --size=1G \
    --direct=1  # Direct I/O(绕过 Page Cache)

1.4 Page Cache 脏页回写 — 不可控的停顿(Stall) ​

问题场景:应用持续高速写入(日志、数据库批量导入),Page Cache 中累积大量脏页。当脏页比例超过内核阈值时,pdflush/flush 内核线程开始回写——但回写速度可能跟不上写入速度,最终导致 write() 系统调用直接阻塞,直到脏页降到安全水位。

text
写入过程:
  应用 write(fd, buf, 10MB)  → 数据写入 Page Cache → 标记脏页
  10 秒内写入 10GB → Page Cache 中 10GB 脏页
  pdflush 回写速度: 500MB/s → 需要 20 秒
  但阈值为 10% 内存(16GB 内存 → 1.6GB 就开始回写)
  → 写入速度(1GB/s) >> 回写速度(500MB/s) → 脏页持续增长
  → 触及 dirty_ratio = 20% → write() 被阻塞,等待脏页下降
  → 应用突然卡顿数秒甚至数十秒!
mermaid
flowchart TB
    subgraph Normal["正常运行"]
        A["应用 write()"] --> B["数据进入 Page Cache<br/>(脏页)"]
        B --> C["pdflush 后台回写<br/>不阻塞应用"]
    end

    subgraph Stall["触发 Stall"]
        D["应用持续高速写入"] --> E["脏页比例达到<br/>dirty_background_ratio"]
        E --> F["pdflush 全力回写"]
        D --> G["但写入速度 >> 回写速度"]
        G --> H["脏页达到 dirty_ratio"]
        H --> I["⚠️ write() 被阻塞<br/>应用卡顿 N 秒!"]
    end

    style I fill:#f44336,color:#fff

vm.dirty 内核参数调优 ​

bash
# 查看当前配置
sysctl -a | grep "vm.dirty"

# 关键参数
vm.dirty_background_ratio = 10   # 脏页达到 10% 内存 → 后台回写启动
vm.dirty_ratio          = 20    # 脏页达到 20% 内存 → 前台阻塞(write 卡顿)
vm.dirty_expire_centisecs = 3000 # 脏页超过 30 秒 → 强制回写
vm.dirty_writeback_centisecs = 500 # pdflush 每 5 秒唤醒检查一次

# 或者用字节数(更精确)
vm.dirty_background_bytes = 0     # 为 0 时使用 ratio(百分比)
vm.dirty_bytes           = 0
参数含义小值大值
dirty_background_ratio后台回写触发点更早开始回写,减少 stall 风险更少磁盘 IO,允许更多内存缓存
dirty_ratio前台阻塞触发点write 更容易阻塞更少阻塞,但 OOM 风险更高
dirty_expire_centisecs脏页过期时间数据更频繁落盘数据在内存中保持更久
dirty_writeback_centisecspdflush 唤醒间隔更频繁检查(CPU 开销)更少检查(可能延迟回写)

场景化调优建议 ​

bash
# 场景 1: 高吞吐写入(日志收集、Kafka)
# 目标:避免 write() 阻塞
sysctl -w vm.dirty_background_ratio=5    # 更早开始回写
sysctl -w vm.dirty_ratio=10             # 更早触发阻塞(但阈值低,容易触发)
# 或
sysctl -w vm.dirty_background_bytes=$((256*1024*1024))  # 256MB
sysctl -w vm.dirty_bytes=$((512*1024*1024))              # 512MB
# 用字节数对超大内存机器更精确

# 场景 2: 大内存机器(128GB+)
# 问题:10% = 12.8GB 脏页,回写需要很久
# 策略:用字节数设定绝对值上限
sysctl -w vm.dirty_background_bytes=$((1*1024*1024*1024))  # 1GB 开始后台回写
sysctl -w vm.dirty_bytes=$((2*1024*1024*1024))              # 2GB 硬上限

# 场景 3: 低延迟要求(Redis 持久化、数据库)
# 目标:最小化 stall
sysctl -w vm.dirty_background_ratio=3
sysctl -w vm.dirty_ratio=5
sysctl -w vm.dirty_expire_centisecs=1000  # 10 秒过期(更快刷新)

# 持久化
echo "vm.dirty_background_ratio=5" >> /etc/sysctl.conf
echo "vm.dirty_ratio=10" >> /etc/sysctl.conf
sysctl -p

监控脏页状态 ​

bash
# 实时查看脏页大小
watch -n 1 'grep -E "Dirty:|Writeback:" /proc/meminfo'

# 输出示例:
# Dirty:           5242880 kB   ← 5GB 脏页!
# Writeback:        1048576 kB   ← 1GB 正在回写中

# 如果 Dirty 持续增长 → 写入速度 > 回写速度 → 即将 stall

# 查看阻塞事件
cat /proc/vmstat | grep throttle
# nr_throttled_written: 被阻塞的写入次数

# 查看 I/O 等待
iostat -x 1
# %util 接近 100% 且 await 很高 → 磁盘是瓶颈 → 需要更早触发回写或换 SSD

最佳实践:数据库服务器(MySQL/PostgreSQL)使用 O_DIRECT 绕过 Page Cache 可以彻底避免脏页回写停顿。Kafka 依赖 Page Cache,需要仔细调优 dirty_background_ratio。

c
// 从用户态写到磁盘的完整路径:
// 用户程序: write(fd, buf, count)
//   → sys_write()
//     → vfs_write()
//       → file->f_op->write_iter()
//         → generic_perform_write()
//           → 写入 page cache (用户页复制到内核页)
//           → 标记页面为 dirty
//   → 返回用户态 (异步!此时数据在内存中)

// 稍后 (由内核决定时机):
//   → writeback 线程
//     → 文件系统的 writepages()
//       → 构造 bio (block I/O 请求)
//         → submit_bio() 提交给块设备驱动
//           → 块设备驱动 → 磁盘

// 关键点: write() 返回成功 ≠ 数据已落盘!
// fsync(fd) / fdatasync(fd) 才确保落盘
// O_DIRECT 打开的文件绕过 page cache

2. ext4 — 通用之选 ​

2.1 创建与调优 ​

bash
# 创建 ext4 文件系统
mkfs.ext4 /dev/sdb1

# 常用 mount 选项
mount -t ext4 -o defaults,noatime,nodiratime /dev/sdb1 /data

# 关键调优参数:
# noatime:        不记录访问时间 (降低 IO, 推荐!)
# data=ordered:   默认, 元数据日志, 数据先于元数据写回
# data=writeback: 仅元数据日志 (快但不安全)
# data=journal:   数据+元数据都记日志 (最安全, 最慢)
# barrier=1:      写屏障 (默认, 安全)
# discard:        启用 SSD TRIM

# 查看超级块信息
dumpe2fs /dev/sdb1 | head -30
tune2fs -l /dev/sdb1

2.2 inode 耗尽问题 ​

bash
# ext4 创建时分配固定数量 inode
# 小文件巨多的场景 (邮件服务器、容器镜像), inode 可能先用完!

# 查看 inode 使用情况
df -i

# 输出示例:
# Filesystem     Inodes  IUsed   IFree IUse% Mounted on
# /dev/sdb1      65536  65000    536   99%  /data

# 解决办法: 创建时指定更多 inode
mkfs.ext4 -i 4096 /dev/sdb1  # 每 4KB 数据分配一个 inode (默认 16KB)
# 注意: 每个 inode 256B, -i 4096 → 6.25% 空间用于 inode
# 小文件多: -i 2048, 大文件多: -i 65536

2.3 碎片化 ​

bash
# 检查碎片
e2fsck -fn /dev/sdb1
# fragments 数

# 碎片整理 (需要卸载或在线整理)
e4defrag /mount/point
e4defrag -c /mount/point  # 只看不整理

3. XFS — 大文件与高并发之王 ​

3.1 创建与调优 ​

bash
# 创建 XFS
mkfs.xfs /dev/sdb2

# 关键调优参数:
mkfs.xfs -d agcount=8 -l size=256m /dev/sdb2
# agcount:  分配组数量 (更多AG = 更高并发)
# log size: 日志大小 (大的日志 → 更好的写入性能)

# mount 选项
mount -t xfs -o defaults,noatime,nodiratime,largeio,inode64 \
      /dev/sdb2 /data

# inode64:        允许 inode 分布在 64 位空间 (必须!)
# largeio:        优化大 I/O (推荐用于流媒体/数据库)
# allocsize=1m:   预分配 1MB extent
# nobarrier:      关闭写屏障 (有电池备电的 RAID 卡可用, 提升性能)
# logbsize=256k:  日志缓冲区大小

3.2 XFS 为什么适合大文件 ​

bash
# XFS 的 extent 分配策略:
# - 延迟分配: 缓存数据, 延迟决定磁盘布局 → 分配大 extent
# - 预分配: 推测文件增长, 提前分配连续空间
# - 空间预分配 (fallocate):

# 预先分配 1GB 连续空间 (避免碎片)
fallocate -l 1G /data/largefile.dat

# ext4 也支持, 但 XFS 效果更好

3.3 XFS 不适合的场景 ​

XFS 的弱点:
- 小文件删除慢 (inode B+tree 操作开销)
- 无法缩小文件系统 (xfs_growfs 只能扩不能缩!)
- fsck 时间长 (设计时假设有日志, 不常需要 fsck)

不适合:
- 大量小文件 (邮件、容器镜像、包管理)
- 需要频繁缩容的场景

4. Btrfs — 现代特性集大成者 ​

4.1 核心特性 ​

bash
# Btrfs 的杀手锏特性:

# 1. 写时复制 (COW) — 快照的基石
# 2. 快照: 瞬间创建, 几乎不占空间
btrfs subvolume snapshot /mnt/data /mnt/data_snap_20260711

# 3. 子卷: 独立的命名空间
btrfs subvolume create /mnt/data/@home
btrfs subvolume create /mnt/data/@var
# 挂载子卷: mount -o subvol=@home /dev/sdb3 /home

# 4. 透明压缩
mount -o compress=zstd /dev/sdb3 /mnt/data
# 可用: no, zlib, lzo, zstd
# zstd 比 lzo 压缩率高 20-30%, 速度相当

# 5. 校验和
# 自动检测静默数据损坏 (bit-rot)
# scrub 扫描并修复
btrfs scrub start /mnt/data
btrfs scrub status /mnt/data

# 6. Send/Receive (增量备份)
btrfs send /mnt/data_snap_old | \
btrfs receive /backup/
btrfs send -p /mnt/data_snap_old /mnt/data_snap_new | \
btrfs receive /backup/

4.2 COW 的代价 — 数据库场景 ​

bash
# 数据库 (MySQL, PostgreSQL, etcd) 的写模式:
# 频繁随机小块写入, 每次写都会触发 COW:
# 旧数据块 → 保留 (直到清理)
# 新数据块 → 分配
# 元数据更新 → B-tree 重平衡 → 更多 COW → 写放大!

# 结果: 随机写性能严重下降 (可能 2-5× 衰减)

# 解决方案: 对数据库的数据目录关闭 COW
chattr +C /var/lib/mysql/          # 关闭 COW
lsattr /var/lib/mysql/             # 验证 C 标志
# 或 mount -o nodatacow

# 或者: 数据库直接用 ext4/XFS, Btrfs 用于容器/系统/备份

4.3 检查 Btrfs 健康状况 ​

bash
# 查看文件系统概况
btrfs filesystem show /mnt/data
btrfs filesystem df /mnt/data       # 不同于系统 df!
btrfs filesystem usage /mnt/data    # 详细的分配统计

# 检查是否有错误
btrfs device stats /dev/sdb3
# 输出: corruption_errs, flush_errs, generation_errs...

# Scrub: 校验所有数据和元数据
btrfs scrub start -B /mnt/data     # -B: 前台执行,等待完成

5. ZFS — 存储的终极方案 ​

5.1 ZFS 的设计哲学 ​

ZFS = 卷管理 + 文件系统 + RAID 一体化
传统: mdadm + LVM + ext4 → 三层分离, 各自为政
ZFS:  zpool + zfs → 一站式, 信息互知, 智能决策

// ZFS 核心数据结构: Merkle Tree 校验
// 每个数据块都有校验和, 校验和存储在父节点
// 自愈: 检测到损坏 → 自动从冗余副本修复
// 静默数据损坏 (bit-rot) 无处遁形!

5.2 创建与使用 ​

bash
# 创建池 (mirrored: 类似 RAID1)
zpool create tank mirror /dev/sdb /dev/sdc

# 创建数据集 (文件系统)
zfs create tank/data
zfs set compression=lz4 tank/data      # 透明压缩
zfs set atime=off tank/data            # 关闭 atime
zfs set recordsize=1M tank/data        # 大记录 (适合大文件)
zfs set recordsize=16K tank/data/mysql # 小记录 (适合数据库)

# 快照: 几乎零开销
zfs snapshot tank/data@before_upgrade
# 回滚
zfs rollback tank/data@before_upgrade

# 克隆: 快照的可写副本
zfs clone tank/data@before_upgrade tank/test_clone

# 增量发送 (备份)
zfs send tank/data@snap1 | ssh backup zfs receive backuppool/data

# ZFS Send 增量: 只发送变化的块, 比 rsync 高效得多!

5.3 ARC (自适应替换缓存) ​

bash
# ZFS 的内存缓存叫 ARC (不同于 Linux page cache)
# ARC 使用自己管理的内存, 不在 Linux page cache 中
# ARC 大小 = 物理内存 - 1GB (默认)

# 查看 ARC 效率
cat /proc/spl/kstat/zfs/arcstats | grep -E "^(hits|misses|size)"

# 调整 ARC 大小 (通过模块参数)
# /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=8589934592  # 8GB

6. 四大文件系统决策指南 ​

6.1 对比总览 ​

维度ext4XFSBtrfsZFS
成熟度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
最大卷1 EB8 EB16 EB256 ZB
最大文件16 TB8 EB16 EB16 EB
快照❌❌✅✅
压缩❌❌✅(zstd/lzo)✅(lz4/zstd/gzip)
校验和❌❌(仅元数据)✅✅(Merkle Tree)
RAID❌(需mdadm)❌(需mdadm)✅✅(RAID-Z)
去重❌❌✅(离线)✅(在线)
ACL/SELinux✅✅✅✅
在线扩容✅✅✅✅
在线缩容✅(offline)❌✅❌
fsck 速度慢(大卷)快较快N/A(scrub)
内存需求低低中极高(1GB+/TB)
Linux 主线✅✅✅❌(DKMS)

6.2 场景决策 ​

┌─────────────────────────────────────────────┐
│              场景 → 推荐文件系统              │
├─────────────────────────────────────────────┤
│ 通用桌面/服务器 (无特殊需求)  → ext4          │
│ 数据库 (MySQL/PostgreSQL)     → XFS          │
│ 大文件存储 (视频/备份/HDFS)   → XFS          │
│ 容器运行时 (Docker/Podman)    → Btrfs/ZFS    │
│ 开发机 (需要快照回滚)         → Btrfs         │
│ 文件服务器 / NAS             → ZFS           │
│ 大量小文件 (邮件/maven/npm)   → ext4          │
│ 虚拟机镜像存储               → XFS (raw)     │
│ 高可用存储集群               → ZFS           │
│ 嵌入式/低内存设备             → ext4          │
│ 需要在线缩容                 → ext4 / Btrfs  │
│ 数据安全第一 (防bit-rot)      → ZFS / Btrfs   │
└─────────────────────────────────────────────┘

6.3 实际选择例子 ​

bash
# 数据库服务器 (MySQL 数据目录)
# 选 XFS: InnoDB 使用 O_DIRECT, XFS 分配组并行, 大 extents
mkfs.xfs -d agcount=16 -l size=512m /dev/nvme0n1
mount -o noatime,nodiratime,largeio,inode64,logbsize=256k \
      /dev/nvme0n1 /var/lib/mysql

# Docker 宿主机
# 选 Btrfs: 快照、子卷、压缩, Docker 原生支持
mkfs.btrfs /dev/sda5
mount -o compress=zstd,noatime,space_cache=v2 /dev/sda5 /var/lib/docker

# NAS 文件存储
# 选 ZFS: 数据完整性 + RAID-Z2 (容忍2盘故障) + 压缩 + 快照
zpool create tank raidz2 /dev/sdb /dev/sdc /dev/sdd /dev/sde
zfs create -o compression=lz4 tank/media
zfs create -o recordsize=1M tank/media/videos

# CI/CD 构建服务器
# 选 ext4: 简单可靠, 大量小文件创建删除
mkfs.ext4 -i 2048 /dev/sda6  # 小 inode 间隔, 应对巨量小文件

# 通用 Linux 根分区
# 选 ext4: RHEL/Fedora 默认, 最成熟
# 或 Btrfs: openSUSE/Fedora 默认 (利用快照做系统回滚)

6.4 性能测试 ​

bash
# 用 fio 测试不同文件系统的随机读写
# 随机读
fio --name=randread --ioengine=libaio --iodepth=16 \
    --rw=randread --bs=4k --direct=1 --size=4G \
    --numjobs=4 --runtime=60 --group_reporting \
    --filename=/mnt/data/fio_test

# 随机写
fio --name=randwrite --ioengine=libaio --iodepth=16 \
    --rw=randwrite --bs=4k --direct=1 --size=4G \
    --numjobs=4 --runtime=60 --group_reporting \
    --filename=/mnt/data/fio_test

# 顺序写 (看带宽)
fio --name=seqwrite --ioengine=libaio --iodepth=16 \
    --rw=write --bs=1M --direct=1 --size=10G \
    --numjobs=4 --runtime=60 --group_reporting \
    --filename=/mnt/data/fio_test

7. 文件系统一致性保证 ​

7.1 fsync 的正确使用 ​

c
#include <fcntl.h>
#include <unistd.h>

void safe_write(const char *path, const char *data, size_t len) {
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);

    // 写数据
    write(fd, data, len);

    // 关键!确保数据和元数据都持久化到磁盘
    // 错误: close(fd) 不保证落盘!
    // 错误: fsync(fd) 只保证数据, 不保证元数据 (有些FS)
    fsync(fd);   // 确保数据 + 元数据 (文件大小、时间戳等)

    // 更精确的:
    // fdatasync(fd): 只确保数据 + 必要元数据 (不刷时间戳!)
    //                 对于只关心数据完整性的场景, 性能更好

    close(fd);

    // 更安全: 原子替换
    // 写临时文件 → fsync → rename
    int tmpfd = open("/path/.tmpfile", O_WRONLY | O_CREAT, 0644);
    write(tmpfd, data, len);
    fsync(tmpfd);
    close(tmpfd);
    rename("/path/.tmpfile", "/path/file");  // rename 是原子的!
}

7.2 崩溃一致性测试 ​

bash
# 验证文件系统的崩溃安全性
# 工具: crashmonkey, xfstests, dm-log-writes

# xfstests: 文件系统回归测试套件
# 包含大量边界条件测试
git clone git://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git
cd xfstests-dev
make
./check -ext4 /dev/sdb1  # 测试 ext4

参考 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记