1. 初识 Page Cache
1.1 什么是 Page Cache?
-
定义:Linux 内核用于缓存文件数据的内存区域。
-
单位 :以页(Page)为基本单位,通常一页为 4KB。
-
位置:位于内核空间,与虚拟内存系统深度集成。
-
共享性:支持跨进程共享。多个进程访问同一文件时,可共享同一份页缓存,避免重复读盘。
-
核心思想:
-
读文件:先查页缓存,命中则直接返回;未命中才读磁盘。
-
写文件:先写页缓存,标记为脏页;之后由内核异步刷回磁盘。
-
1.2 为什么需要 Page Cache?
磁盘和内存速度差距巨大:
| 对比项 | 内存 | 磁盘 |
|---|---|---|
| 访问延迟 | 纳秒级 | 毫秒级 |
| 吞吐 | GB/s 级别 | MB/s 级别 |
| 差距 | 可达百万倍 / 千倍 | ------ |
如果每次读写都直接访问磁盘,系统性能会极差。Page Cache 通过内存缓存文件数据,大幅减少磁盘 I/O。
1.3 核心作用
-
加速文件读取
-
热点文件如配置文件、日志、数据库数据文件,一旦缓存,后续访问几乎零延迟。
-
例:Nginx 静态资源第一次读盘后进入页缓存,后续大量请求直接命中内存。
-
-
合并写操作
-
多次小写入先缓存在页缓存,后续合并为一次大块写入。
-
减少磁盘 I/O 次数,提升吞吐,延长 SSD 寿命。
-
-
支持"零拷贝"优化
-
如
sendfile()可直接从页缓存传输到网络 socket,避免用户态缓冲区拷贝。 -
Kafka 等高吞吐系统常利用此特性。
-
2. Page Cache 工作机制
2.1 读路径:从磁盘到页缓存
用户程序调用 read() 时:
-
用户态陷入内核态。
-
VFS 根据文件路径找到 inode,再根据 inode + 偏移量在页缓存中查找。
-
命中:
-
直接通过
copy_to_user将数据从页缓存拷贝到用户缓冲区。 -
完全跳过磁盘 I/O。
-
-
未命中:
-
触发缺页异常。
-
VFS 调用具体文件系统的
readpage/readpages。 -
文件系统计算逻辑块地址 LBA,交给块设备层。
-
I/O 调度器优化排序,如 CFQ、Deadline 等。
-
磁盘通过 DMA 将数据传入内存。
-
数据填充到
struct page,加入页缓存。 -
更新
address_space的基数树 radix tree,建立"文件 inode + 页内偏移"到物理页的映射。 -
最后拷贝到用户空间。
-
关键点:文件数据不会直接从磁盘读到用户空间,必须经过 Page Cache。
2.2 预读机制
-
当程序读取某一部分数据时,内核预测后续可能继续读取相邻数据。
-
因此不只加载当前 4KB,还会提前加载后续连续页。
-
默认会根据文件类型和访问模式动态调整,常见预读大小为 16KB 或 32KB。
-
作用:后续访问直接命中缓存,减少磁盘 I/O。
2.3 写路径与脏页
用户调用 write():
-
用户态陷入内核态。
-
内核检查文件描述符有效性。
-
通过
copy_from_user将数据从用户空间拷贝到页缓存。 -
对应页面被标记为脏页,设置
struct page的PG_dirty标志。 -
此时数据与磁盘不一致,但不会立即写盘。
-
后续由内核在合适时机异步回写。
示例:
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main() {
const char* message = "Hello, Linux Page Cache!";
int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
ssize_t bytes_written = write(fd, message, strlen(message));
if (bytes_written < 0) {
perror("write");
} else {
printf("Wrote %zd bytes to file.\n", bytes_written);
}
close(fd);
return 0;
}
写入页缓存后,数据不会立刻落盘。若系统崩溃,未回写的脏页可能丢失。
2.4 脏页刷新机制
内核提供三种可靠刷新机制:
-
定时刷新
-
内核 flusher / writeback 线程周期性扫描脏页。
-
达到时间阈值或脏页比例后,分批写入磁盘。
-
-
主动触发刷新
-
用户调用
fsync、fdatasync、sync。 -
强制立即写入并等待 I/O 完成。
-
适合数据库、日志、关键文件保存。
-
-
内存不足时强制刷新
-
内存紧张时,内核回收页缓存。
-
优先释放干净页。
-
干净页不足时,强制回写脏页,再释放内存。
-
3. 脏页回写机制
3.1 回写触发条件
| 触发方式 | 说明 |
|---|---|
| 周期性回写 | 每个回写设备有定时器 wb_timer,由 vm.dirty_writeback_centisecs 控制,单位 0.01 秒 |
| 比例回写 | 脏页达到 vm.dirty_background_ratio 启动后台异步回写;达到 vm.dirty_ratio 阻塞新写 |
| 显式同步 | sync、fsync、fdatasync |
| 内存压力触发 | kswapd 等回收内存时,遇脏页先回写再释放 |
3.2 回写过程
-
后台 writeback 线程执行回写。
- Linux 2.6.32+ 已废弃
pdflush。
- Linux 2.6.32+ 已废弃
-
从脏页链表获取页面。
-
每个脏页关联
address_space,包含文件索引、磁盘地址等信息。 -
调用文件系统的
writepages。 -
通过块设备层提交 I/O,如 ext4 计算逻辑块地址,使用
submit_bio下发。 -
数据写入磁盘后,清除
PG_dirty标记,页面变为干净页,可回收复用。
伪代码:
struct page *page = get_dirty_page();
if (PageDirty(page)) {
writepages(page->mapping); // 写入磁盘
ClearPageDirty(page); // 清除脏标记
}
3.3 相关内核参数
| 参数 | 作用 |
|---|---|
vm.dirty_ratio |
最大脏页比例,达到后阻塞新写,默认约 20% |
vm.dirty_background_ratio |
后台异步回写触发比例,默认约 10% |
vm.dirty_expire_centisecs |
脏页最大存活时间,超时强制回写 |
vm.dirty_writeback_centisecs |
回写线程周期性唤醒间隔,单位 0.01 秒 |
查看与设置:
sysctl vm.dirty_ratio
sysctl vm.dirty_background_ratio
sysctl -w vm.dirty_writeback_centisecs=500 # 5 秒
4. 实际应用场景
4.1 静态文件服务
-
Nginx、Apache 部署 HTML、CSS、JS、图片等。
-
第一次访问从磁盘读取并加载到页缓存。
-
后续成千上万次访问直接命中页缓存。
-
这是静态文件服务支撑高并发的关键。
4.2 数据库系统
-
MySQL、PostgreSQL 的表数据、索引本质都是磁盘文件。
-
查询优先从页缓存读取,减少磁盘 I/O。
-
数据库自身缓存如 InnoDB Buffer Pool 与页缓存配合。
-
即使数据库缓存未命中,若页缓存命中,也能减少磁盘开销。
4.3 日志写入场景
-
Java logback、Python logging 默认延迟写入,依赖页缓存。
-
写入先进入页缓存,内核后台批量刷新。
-
风险:程序崩溃时,未刷盘脏页可能丢失。
-
核心日志应使用
fsync主动刷新,平衡性能与安全。
5. Page Cache 调优方法
5.1 调整页缓存大小与内存置换
vm.pagecache_limit_mb
-
硬性限制页缓存最大占用,单位 MB。
-
适用于内存紧张、核心应用需要保留内存的场景。
-
例:16GB 内存服务器,可为应用预留 12GB,将页缓存限制为 4096MB。
sysctl vm.pagecache_limit_mb
sysctl -w vm.pagecache_limit_mb=4096
echo "vm.pagecache_limit_mb=4096" >> /etc/sysctl.conf
sysctl -p
vm.swappiness
控制内存不足时,内核优先回收页缓存还是置换 Swap。
| 取值 | 倾向 | 适用 |
|---|---|---|
| 0~20 | 保留页缓存,优先置换 Swap | IO 密集型,如 Nginx、日志读取 |
| 60~100 | 回收页缓存,保留应用内存 | 计算密集型 |
| 生产通用 | IO 密集 10~20,通用 30~50 | ------ |
禁止设置为 0,极端情况下可能禁用 Swap,引发内存溢出风险。
sysctl vm.swappiness
sysctl -w vm.swappiness=10
5.2 优化预读大小
查看:
cat /sys/block/sda/queue/read_ahead_kb
设置:
# 大文件:日志、视频、备份
echo 512 > /sys/block/sda/queue/read_ahead_kb
# 小文件:配置、短文本
echo 64 > /sys/block/sda/queue/read_ahead_kb
-
设备级
read_ahead_kb优先级高于全局vm.readahead_size。 -
默认通常 128KB。
-
大文件顺序读:可调高到 256KB~1024KB。
-
小文件随机读:建议 64KB 及以下,避免缓存污染。
-
修改后实时生效,无需重启。
5.3 合理选择文件 IO 接口
| IO 方式 | 特点 | 适用 |
|---|---|---|
标准缓存 IO read/write |
完全依赖页缓存,性能最优 | 高频读、小文件、非核心数据 |
直接 IO O_DIRECT |
跳过页缓存,应用缓冲区与磁盘直接传输 | 数据库、大文件备份、日志落盘 |
同步落盘 fsync/fdatasync |
配合标准 IO,主动刷脏页 | 交易、订单、数据库事务 |
同步 IO O_SYNC |
写入同步落盘,实时性最强,性能损耗大 | 极高安全性场景 |
示例:
// 标准缓存 IO
int fd = open("app.log", O_RDONLY);
read(fd, buf, 1024);
close(fd);
// 直接 IO:跳过页缓存
int fd = open("backup.db", O_RDWR | O_DIRECT);
// 写入后强制落盘
write(fd, data, len);
fsync(fd);
O_DIRECT需满足内存对齐、块大小对齐,否则可能触发内核降级处理。
5.4 清理页缓存(应急运维)
需 root 权限,通过 /proc/sys/vm/drop_caches:
echo 1 > /proc/sys/vm/drop_caches # 仅清理页缓存
echo 2 > /proc/sys/vm/drop_caches # 清理 dentry 和 inode 缓存
echo 3 > /proc/sys/vm/drop_caches # 全面清理
注意:
-
不会丢失磁盘数据,只清内存缓存。
-
清理后后续读文件会重新从磁盘加载,可能产生大量磁盘 I/O。
-
禁止生产环境频繁、定时执行。
-
禁止写定时任务自动清理缓存。
5.5 脏页回写联动调优
-
高写入业务:降低
vm.dirty_ratio和vm.dirty_background_ratio,避免大量脏页阻塞业务。 -
高性能存储业务:调高
vm.dirty_writeback_centisecs,减少频繁回写带来的 I/O 抖动。 -
组合调优:页缓存大小、预读、IO 接口、脏页机制一起配合,达到缓存命中率、内存利用率、磁盘 I/O 性能的平衡。
6. 速查表
| 主题 | 关键点 |
|---|---|
| 定义 | 内核缓存文件数据的内存区域,以 4KB 页为单位 |
| 读命中 | 直接 copy_to_user,跳过磁盘 |
| 读未命中 | 缺页异常 → 文件系统 readpage → 块设备 → DMA → 页缓存 |
| 预读 | 提前加载连续页,常见 16KB/32KB,可调 read_ahead_kb |
| 写操作 | 先写页缓存,标记 PG_dirty,异步回写 |
| 脏页触发 | 周期、比例、显式同步、内存压力 |
| 回写 | writeback 线程 → writepages → submit_bio → 清脏标记 |
| 关键参数 | dirty_ratio、dirty_background_ratio、dirty_expire_centisecs、dirty_writeback_centisecs |
| 调优 | 页缓存上限、swappiness、预读、IO 接口、drop_caches |
| 应用 | 静态文件服务、数据库、日志写入 |
| 禁忌 | 禁止频繁 drop_caches;禁止 swappiness=0;核心数据要 fsync |
7. 关键结论
-
Page Cache 是 Linux I/O 性能的核心基石。
-
读操作先查页缓存,命中直接返回,未命中才读磁盘。
-
写操作先写页缓存,产生脏页,后续异步回写。
-
脏页回写保证内存与磁盘一致性,触发方式包括定时、比例、显式同步、内存压力。
-
预读、合并写、零拷贝、跨进程共享是页缓存高效的关键。
-
调优要结合业务:IO 密集型保留页缓存,计算密集型适当回收;大文件调大预读,小文件调小预读;核心数据用
fsync,大文件备份可用O_DIRECT。