2. Page Cache 基础:定义、作用、数据结构与生命周期

Page Cache 是 Linux 内核中用于缓存文件数据页(通常为 4KB)的内存缓存机制。它是文件系统 I/O 性能的核心支柱,也是故障排查中必须掌握的第一环。本节从定义、作用、核心数据结构和生命周期四个维度展开,为后续回写与调度分析奠定基础。

2.1 Page Cache 的定义与作用

Page Cache 是内核在物理内存中维护的文件数据副本。当进程通过 read()/write() 系统调用访问文件时,内核优先操作 Page Cache 中的页面,而非直接读写块设备。这种设计带来了三个关键作用:

  • 加速读写:内存访问延迟(纳秒级)远低于磁盘 I/O(毫秒级),命中 Page Cache 的读操作可避免磁盘寻道与旋转延迟。
  • 减少磁盘 I/O:写操作先写入 Page Cache(标记为脏页),由内核异步回写合并多次小写入,降低磁盘磨损与带宽消耗。
  • 数据共享:多个进程映射同一文件时,共享同一组 Page Cache 页面,节省内存并保证一致性。
关键指标:Page Cache 命中率直接影响文件系统性能。在生产环境中,/proc/meminfo 中的 Cached 字段表示当前 Page Cache 大小,Dirty 字段表示待回写的脏页总量。

2.2 核心数据结构:address_space 与 page

Page Cache 的管理依赖两个核心内核数据结构:struct address_space 和 struct page。它们共同构成了文件数据页的索引、缓存和回写框架。

2.2.1 struct address_space

每个文件索引节点(inode)关联一个 address_space 对象,负责管理该文件的所有缓存页。其关键字段如下:

字段 类型 作用
host struct inode * 指向所属的 inode,建立文件与缓存页的关联
i_mmap struct rb_root 红黑树根,管理该文件的内存映射区域
page_tree struct radix_tree_root 基数树(现为 XArray),按文件偏移快速查找缓存页
nrpages unsigned long 当前缓存的页面总数
a_ops const struct address_space_operations * 操作函数表,包含 readpage、writepage、dirty_folio 等回调

2.2.2 struct page(struct folio)

每个缓存页对应一个 struct page(现代内核使用 struct folio 作为更安全的封装)。页面状态通过一组标志位(flags)和引用计数管理:

  • PG_locked:页面正在被 I/O 操作锁定,防止并发访问。
  • PG_dirty:页面内容已被修改,等待回写至磁盘。
  • PG_uptodate:页面数据与磁盘一致(读完成或写回写后设置)。
  • PG_referenced:页面最近被访问过,影响回收算法。
  • mapping:指向所属的 address_space,若为 NULL 则表示页面未被缓存。
排查提示:使用 /proc/kpageflags 或 mincore() 系统调用可以检查特定页面是否在 Page Cache 中、是否脏页。故障排查时,dirty 页数量异常增长往往预示回写线程阻塞。

2.3 Page Cache 生命周期 SVG 流程图

下图展示了 Page Cache 从创建、使用、脏页标记到回写回收的完整生命周期:

Page Cache 生命周期流程图 Page Cache 生命周期流程图 进程 read() / write() 查找 address_space.page_tree 页面存在? 命中:直接读写页面 未命中:分配新页面 磁盘 I/O:读取数据 设置 PG_uptodate 写操作:标记 PG_dirty 回写线程:写回磁盘 清除 PG_dirty,可回收 回收条件 kswapd 或直接回收 选择干净页驱逐

2.4 生命周期阶段详解

Page Cache 的生命周期可划分为以下五个阶段,每个阶段对应不同的内核操作和排查关注点:

阶段 触发条件 页面标志 排查关注点
分配 读文件未命中、写文件创建新页 PG_locked(分配中) 内存压力下分配延迟、OOM
使用 进程读写、内存映射 PG_uptodate、PG_referenced 命中率低导致频繁磁盘 I/O
脏页 写操作修改页面内容 PG_dirty 脏页比例过高触发回写风暴
回写 定时器到期、脏页比例超阈值、sync 调用 PG_writeback 回写线程阻塞、磁盘 I/O 排队
回收 内存不足、kswapd 扫描、主动释放 无(页面被释放) 回收抖动、大量缺页中断

2.5 内核参数与监控命令

以下内核参数直接控制 Page Cache 的行为,在故障排查中经常需要调整:

# 查看当前 Page Cache 状态
cat /proc/meminfo | grep -E "^(Cached|Dirty|Writeback|WritebackTmp)"

# 脏页回写阈值(单位:页)
sysctl vm.dirty_background_ratio   # 后台回写触发比例(默认 10%)
sysctl vm.dirty_ratio              # 同步回写阻塞比例(默认 20%)
sysctl vm.dirty_expire_centisecs   # 脏页过期时间(默认 3000,即 30 秒)
sysctl vm.dirty_writeback_centisecs # 回写线程唤醒间隔(默认 500,即 5 秒)

# 手动触发回写
sync
echo 3 > /proc/sys/vm/drop_caches   # 谨慎使用,仅用于测试
故障排查警告:drop_caches 仅释放干净页,不会丢失数据。但生产环境中突然释放大量 Page Cache 可能导致磁盘 I/O 暴增,建议在低峰期操作。脏页(Dirty)不会被 drop_caches 释放,必须先通过 sync 回写。

2.6 address_space 与 page 关系 SVG 架构图

下图展示了 inode、address_space、page_tree 和 page 之间的层次关系:

address_space 与 page 关系架构图 address_space 与 page 关系架构图 struct inode i_mapping → address_space struct address_space host | page_tree | nrpages | a_ops page_tree (XArray / radix tree) struct page (folio) flags: PG_dirty | PG_uptodate mapping → address_space struct page (folio) flags: PG_uptodate mapping → address_space struct page (folio) flags: PG_writeback mapping → address_space 关键关系 每个 inode 对应一个 address_space;address_space 通过 page_tree 按文件偏移索引所有缓存页; 每个 page 通过 mapping 指针反向关联到所属的 address_space,形成双向查找路径。

2.7 故障排查关联点

理解 Page Cache 的生命周期和数据结构后,可以快速定位以下常见故障场景:

  • 脏页堆积导致写延迟飙升:检查 /proc/meminfo 中 Dirty 和 Writeback 字段,若 Dirty 持续高于 dirty_background_ratio 且 Writeback 非零,说明回写线程跟不上写入速度。
  • Page Cache 占用过高导致内存压力:使用 sar -r 或 vmstat 1 观察 cache 和 swap 变化,必要时调整 vm.vfs_cache_pressure 参数。
  • 文件读取性能差:通过 fincore 或 mincore() 检查文件是否在 Page Cache 中,若命中率低则需增大文件访问局部性或调整预读算法(blockdev --setra)。
  • 回写死锁:当 address_space 的 a_ops->writepage 回调阻塞在内存分配或文件系统锁上时,可能导致整个回写线程池冻结,此时 Dirty 页只增不减。
排查命令速查:
cat /proc/meminfo | grep -E "^(Cached|Dirty|Writeback)" — 查看 Page Cache 总量与脏页
find /proc/*/fd -lname '*.log' 2>/dev/null | xargs -I {} file {} | grep -v broken — 定位占用 Page Cache 的进程
vmtouch /path/to/file — 检查特定文件在 Page Cache 中的驻留情况