什么是 PageCache?它的读写过程是怎么样的?有什么优缺点?

#计算机网络#操作系统#面试八股

什么是 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_ratio20%脏页占内存上限,超过则写进程同步回写并阻塞
vm.dirty_background_ratio10%超过则后台开始刷,但不阻塞应用
vm.dirty_expire_centisecs3000(30s)脏页最长驻留时间
vm.dirty_writeback_centisecs500(5s)回写线程唤醒间隔
vm.vfs_cache_pressure100回收 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 的作用,基本就完整了。

笔记加载中…