Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

inode、dentry、page cache

一句话

你说 cat /etc/passwd,看似"打开一个文件然后读字节"。实际内核做了五层抽象:

路径 /etc/passwd
   ↓ (dentry 树解析)
inode 号 + fs 元信息
   ↓
磁盘 block 列表 (extent / direct / indirect)
   ↓
page cache (一次 page 4 KB)
   ↓
块层 + IO 调度
   ↓
NVMe 提交 queue → DRAM 落块

每层都是一个 cache, 每层都各有失效模式. 这一章把这套链路拆开, 让你看清 open + read 在 OS 内部到底是怎么发生的——同时也解释为什么 PostgreSQL / InnoDB / Rocks 这些系统都坚持自己再做一遍 IO 层.

1. dentry: 路径字符串看到的虚拟树

当你 cat /etc/passwd 内核要先做 path resolution:

/   →  根 dir inode 1
etc →  在根 dir 找 "etc" 这一项
       这一项指向下一层 dentry
passwd → 在 etc dir 找 "passwd" 这项
       拿到 final inode

每一层访问都是一次目录扫描 (实际是个 hash/遍历查找). preload 内存里维护 dentry cache (dcache), 命中即命中.

dcache 是 OS 层 "路径 → inode" 的缓存. 内核启动后大部分系统的 /usr/bin 等热目录可达 dcache 100% 命中.

2. inode: 一个文件的 metadata

inode 装着:

文件大小
所有者 + 权限
atime / mtime / ctime
文件数据 block 列表 (extent 或 direct + indirect)
文件类型 (regular / dir / link / device / FIFO)

注意 inode 不带文件名——文件名是它在父目录这条 dentry 的属性. 一份 inode 可被多个 dentry 指 (hardlink), 即一个文件可以多个文件名, 但 inode 本身唯一.

读 inode 也走过 VFS 层后到具体 fs, 在 ext4 上是 ext4_inode 结构, 字段 packed, 256 字节起步, 大文件可扩展.

3. extent vs indirect block: 怎么描述"一个文件的所有数据块"

历史方案是 BSD 早期用 direct block + indirect block + double indirect + triple indirect:

inode 前 12 个 block pointer 直接给前 12 × 4 KB = 48 KB;
第 13 个 pointer → 一块装 1024 pointer → 1024 × 4 = 4 MB;
第 14 个 pointer → 一块装 1024 个 pointer → 每 pointer 又指向装 1024 pointer 的块;
第 15 个 → 4 GB;

听起来文艺但不工程友好 —— 大文件一次访问多级寻址 + cache miss.

现代文件系统改用 extent: 一个 extent 直接表示 [起始 block, 长度 L blocks]. ext4 一个 inode 装内嵌 4 个 extent, 一个 extent 默认最大 32 KB (现代改进后 extent tree 可以扩展). XFS 用 B+ 树 extent.

extent 的好处:

  • 顺序文件 metadata 极小 (1 GB 文件 = 一个 extent entry);
  • 顺序 IO 友好 (磁盘一次 sequential read);
  • 大文件碎片化 metadata 节省 cookie.

这是 DSA 中 B+ 树 + 顺序物理化 在 OS 文件系统层的同构.

4. page cache: 把"读过的页" 留在 DRAM

文件 IO 的真实路径不是 read → disk:

read(fd, buf, n)
   ↓
explicit_filemap_read → page cache 命中 →
   ↓                                ↓
命中就 memcpy 到 buf            miss →
                              submit_bio → 块层 / NVMe
                              data 拉进 page cache
                              再 memcpy 到 user buf

read 总是先看 page cache, mmap 是更高级别坐进 page cache 让用户空间零拷贝访问.

page cache 是 OS 层的"通用 read cache", 对所有进程间共享——如果两个进程同时 cat 同一个文件, 同一个 page cache 被 reused.

5. dirty page 与 writeback

写路径:

write(fd, buf, n)
   ↓
page cache 上写 (与 read 反过来, 先看命中)
   ↓
mark dirty + 设置 dirty 标记位
   ↓
  pthread_async + writeback thread 慢慢 submit_bio 来刷盘
   ↓
disk 完成, 清 dirty 标记

write 在 99% 情况下不立刻写盘. 这是 OS 优化的核心: 把 write-burst 缓冲起来 + 顺序刷盘 + 由 IO 调度合并相邻 page.

但要持久化, 用户必须 fsync(fd) —— 强制把 dirty 同步刷盘并等 disk ACK. fsync 之前的数据可能在断电 / panic 中丢, 这是 crash consistency 的核心题.

6. 文件 IO 怎么测性能

# 系统 cache 命中
time cat /etc/passwd > /dev/null          # 几百 μs

# 强制读盘 + Drop cache
echo 3 > /proc/sys/vm/drop_caches
time cat large_file > /dev/null           # 看盘速度

# 用 fio 做 raw benchmark
fio --name=test --filename=/tmp/a --rw=randread --bs=4k --size=1G --iodepth=8 --numjobs=8

# 看 page cache 占用大小
free -h
vmstat 1

7. 多语言 / 多运行时

语言默认 IO 路径优势 / 坑
C read/write系统调用 + page cache取决于 fsync
Go os.File.Read同上 + goroutine 调度 不阻塞 GMPGOMAXPROCS 满时阻塞会 M 扩
Rust File::readlibc wrapper, 内 readasync 用 tokio::fs 走 epoll + thread pool
Java InputStreamReaderJVM 内 heap buffer + page cache双层 buffer 有时浪费
Python open().readC InputStreamReader 流式 buffer写大文件建议 buffering 调大

各语言 IO 接口都 wraps 系统 syscall, 抽象同一, 仅 syntactic 差异. 真正工程上 IO 性能差别主要看 cache 是否被正确使用 + fsync 频率.

8. PG / InnoDB / Rocks 的"自己再写 IO"

主流数据库 (PG / MySQL / MongoDB / RocksDB) 绕过操作系统 page cache + 自己做缓存管理:

  • 内 buffer pool 是 OS page cache 的平行物, 但能精确控制 LRU + 大小 + writeback;
  • 使用 O_DIRECT flag (下一章详谈) 让 IO 不进 page cache;
  • 自己做 pread + WAL + checksum.

为什么? OS page cache 弱在多线程 + 大内存 + 多 workload 共享机器时不可预期. 自家 buffer pool 能控 : 哪个 thread 等谁、 cache hit 高 / 低、flush 时频率.

9. i_size + i_blocks / dense sparse file

inode 的 i_sb (super block) 信息中含 i_blocks (used blocks count, 1 block = 512B). Linux du -hi_blocks × 512 实际盘占用, 而 ls -li_size 逻辑大小. 这两者差距来自 sparse file 不一定真占用 block. mkfile 输出 50 GB sparse 文件 = 0 i_blocks, 但 i_size = 50 GB.

fallocate --dig 通过 fallocate syscall 让 fs 提前置位 extent 但不实际写入 0, 当 metadata-first allocator, 极适合"预占大文件 + 慢慢填充". 不预先 fallocate 大文件, 等 first write 时 fs 才动态分 extent, 在 high perf IO 场景上要 fallocate + O_DIRECT.

10. 这一章带走的东西

  • open + read 实际穿五层 VFS/dentry/inode/page cache/block layer;
  • extent 模型是 indirect block 的工程演进 (少 metadata + 友 cache);
  • page cache 是不分进程共享的 read cache;
  • write 默认仅写 page cache, fsync 才保证持久化;
  • 数据库普遍走 O_DIRECT 绕 page cache, 自己实现 buffer pool.

下一节 → ext4 / XFS / Btrfs 设计差异