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 从创建、使用、脏页标记到回写回收的完整生命周期:
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 之间的层次关系:
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 中的驻留情况