什么是 PageCache?它的读写过程是怎么样的?有什么优缺点?
PageCache(页缓存)是内核用空闲内存给磁盘文件内容做的缓存,单位是页(通常 4KB)。 读文件时先查它,命中就直接拷给用户;写文件时先写进它并标脏,由后台线程批量刷盘。 它把"磁盘 IO 很慢"这件事藏了起来,代价是内存被占用、掉电可能丢最近几秒的写。
1. 读路径
read(fd, buf, n)
│
▼
查页缓存(以 inode + 页偏移为键)
├── 命中 ──► copy_to_user → 返回(无磁盘 IO,微秒级)
│
└── 未命中 ──► 分配页 → 发起块层 IO → 磁盘读入页 ──► 唤醒 → copy_to_user
│
└── 同时按预读窗口(readahead)多读几页,供下次命中
2. 写路径
write(fd, buf, n)
│
▼
写入页缓存对应页,标记为 dirty(脏页),立即返回
│
├── dirty 占比 > dirty_background_ratio → 唤醒 flush 线程后台回写
├── dirty 占比 > dirty_ratio → 写进程被阻塞,强制回写
└── 脏页超时(dirty_expire_centisecs) → 定时回写
所以 write 返回只代表数据进了内存,不代表落盘。要真正落盘得 fsync/fdatasync,
或者用 O_SYNC/O_DIRECT 绕过缓存。
3. 关键参数
| 参数 | 默认(典型) | 作用 |
|---|---|---|
vm.dirty_ratio | 20% | 脏页占内存上限,超过则写进程同步回写并阻塞 |
vm.dirty_background_ratio | 10% | 超过则后台开始刷,但不阻塞应用 |
vm.dirty_expire_centisecs | 3000(30s) | 脏页最长驻留时间 |
vm.dirty_writeback_centisecs | 500(5s) | 回写线程唤醒间隔 |
vm.vfs_cache_pressure | 100 | 回收 inode/dentry 的倾向 |
# 观察缓存与脏页
grep -E 'MemFree|MemAvailable|^Cached|^Dirty|^Writeback' /proc/meminfo
# 看某个文件被缓存了多少
fincore /data/app.log
# 清缓存的三级(生产慎用,2 会清脏页,3 连 dentry/inode 一起清)
sync; echo 3 > /proc/sys/vm/drop_caches
# 顺序读不污染缓存
dd if=/data/big.db of=/dev/null bs=1M iflag=nocache
4. 优缺点
| 优点 | 缺点 |
|---|---|
| 读命中免磁盘,延迟从毫秒降到微秒 | 吃掉空闲内存,易被误判为内存不足 |
| 写合并 + 延迟回写,减少随机小 IO | 掉电/宕机丢失未刷盘的写 |
| 预读提升顺序读吞吐 | 大文件全表扫描会挤掉热数据 |
| 多个进程共享同一份缓存,省内存 | 与数据库 buffer pool 形成"双缓存",浪费内存 |
5. 常见追问
Q:free 显示内存快满了,正常吗?
正常。页缓存是"可回收内存",真正该看的是 MemAvailable。当内存压力上来,内核会回收干净页(直接丢弃)
和脏页(先回写再丢弃),应用并不会 OOM。
Q:数据库为什么常常绕过页缓存?
MySQL/PostgreSQL 自己维护 buffer pool,若再让页缓存存一份,同一份数据在内存里有两份,命中率反而下降。
所以常用 O_DIRECT 让 IO 直接进出磁盘,缓存完全由数据库自己管。
Q:mmap 和 read 读写文件有什么区别?
mmap 把文件页映射进用户地址空间,访问时靠缺页中断按需加载,省掉一次 copy_to_user;
read 每次都要拷贝。但 mmap 遇到缺页、信号(SIGBUS)、文件截断时处理更麻烦,且不利于错误恢复。
Q:怎么避免缓存被大文件"洗掉"?
用 posix_fadvise(POSIX_FADV_DONTNEED) 提示用完即弃,或用 O_DIRECT/iflag=nocache 直接绕过。
备份、日志归档这类一次性顺序读最容易造成这个问题。
小结
PageCache = 内核的磁盘缓存层:读先查缓存,写先进缓存再异步回写。它用内存换延迟,
带来"free 小但正常""掉电丢数据""双缓存"三个经典现象。回答时把读写两条路径画出来,
再补 dirty_ratio 与 fsync 的作用,基本就完整了。