page cache是linux内核以页为单位缓存文件数据的动态映射系统,通过address_space与inode关联,用xarray/radix tree索引struct page,支持读命中快速返回、预读优化顺序读、写时标脏并异步回写,实现内存与磁盘间高效协同。

理解Page Cache的内核机制,不是为了背诵数据结构,而是为了在真实场景中让文件访问更快、更稳、更可预期。它和文件缓存(如应用层缓存)不是替代关系,而是分层协作:Page Cache是系统级、透明、共享的底层加速器;应用缓存则是业务感知、有策略、带逻辑的上层优化。二者配合得当,性能提升远超简单叠加。
看清Page Cache如何真正“工作”
Page Cache不是一块静态内存池,而是一套动态映射系统:
- 每次打开文件,内核生成
inode,通过i_mapping关联到address_space; -
address_space用基数树(radix tree)按文件偏移量索引struct page,实现O(1)级定位; - 每个
page标记PG_dirty表示已修改未落盘,PG_writeback表示正在刷盘; - 读操作先查cache,命中即返回;未命中则触发预读(read-ahead),加载连续页;
- 写操作默认走writeback:数据进cache、标脏、后台线程异步刷盘。
这意味着:顺序读天然友好,随机小文件读易碎片化,频繁覆盖写可能引发脏页风暴。
让应用缓存与Page Cache形成合力
应用层缓存若不考虑Page Cache行为,容易重复劳动或互相干扰:
- 避免重复缓存热文件:Nginx日志分析脚本若自己读取+解析+缓存一份JSON,不如信任Page Cache,专注做计算;用
vmtouch -v /var/log/access.log确认关键段已驻留,再启动处理; - 对齐页边界提升预读效率:视频转码服务将素材切片为4KB整数倍(如1MB/片),使每次read()恰好触发完整页加载,减少跨页中断;
- 用
mmap()替代read():绕过用户态拷贝,让应用直接操作Page Cache中的页,既省CPU又保一致性;特别适合大文件只读场景(如配置中心推送的配置包); - 写密集型任务慎用双缓存:若应用已用Redis缓存写入结果,再经Page Cache落盘,可能因
dirty_ratio触发激进回收,反而拖慢吞吐;此时可调高vm.dirty_ratio=30并搭配fsync()按需落盘。
用关键指标验证配合效果
不看数字,优化就是盲调。重点关注三类可观测项:
-
缓存有效性:用
cat /proc/meminfo | grep -i "cached\|sreclaimable"看Page Cache实际占用;用cachestat 1(bcc工具)实时看每秒命中/未命中次数,理想读密集场景命中率应>95%; -
脏页健康度:
cat /proc/vmstat | grep -E "pgpgin|pgpgout|pgmajfault"观察换入换出压力;若pgpgout持续飙升,说明回收太猛,可调低vm.swappiness(如设为10); -
IO路径延迟:用
iostat -x 1看%util和await;若await长期>5ms但%util<30%,说明I/O在等缓存——大概率是预读不足或随机读太多,而非磁盘瓶颈。
避开常见配合陷阱
有些做法看似合理,实则削弱Page Cache价值:
-
滥用
O_DIRECT:除非你确信自己比内核更懂预读和合并(如MySQL InnoDB自己管理buffer pool),否则禁用。曾有服务因误加该标志,导致日志读取变慢3倍; -
频繁
drop_caches:生产环境清缓存等于主动丢性能。它不解决根本问题,只掩盖配置失当; - 忽略跨进程共享:PHP-FPM和Logrotate同时读同一日志?它们共用Page Cache中的一份副本。应用缓存若各自保存,纯属浪费内存;
-
忽视NUMA节点:在多路服务器上,若进程绑定在Node1却从Node0的Page Cache读数据,延迟翻倍。用
numactl --membind=1 --cpunodebind=1 ./app绑定本地内存更高效。










