[内核学习]Page Cache

/ 0评 / 0赞

同一份文件读两遍,第二遍比第一遍快多少?这个日常经验的背后是 page cache:内核拿空闲内存缓存文件内容,读走内存,写先攒着.这篇把这本"最大的账"的进出规则实测一遍 — 冷热差,缓存涨落,脏页积压与回写,以及预读这台自动喂食机.

这篇回答几件事:

先做冷热对照.64MB 文件,drop_caches 清干净,dd 读到 /dev/null:

冷读: real 0.221s
热读: real 0.065s     (3.4 倍)
Cached: 31668 kB -> 97648 kB

冷读真去了 eMMC,热读是纯内存拷贝,3.4 倍 — 板上 eMMC 不算慢所以差距不大,机械盘的机器上这个比值轻松到几十倍.更有说服力的是 Cached 账本:一次读完,31668 变 97648,涨的 65980 kB 恰好是文件大小加零头 — 读过即缓存,一个字节不差.这就是 Linux 内存管理的第一信条的由来:空闲内存就是浪费的内存,page cache 是最体面的去处.反过来 MemAvailable 的含义也清楚了:它估计的是"含可回收缓存在内还能挤出来的量",不是真的空着.

page cache 的组织:每个文件一个 address_space,里面一棵 XArray(基数树变体)按页索引挂着缓存页.文件页缺页(上一篇的 do_read_fault)先来这棵树上找,找到直接映射给进程 — 这就是热读的全程,连设备都不碰;找不到才发起 IO,读进来的页顺手挂上树,下一次就是别人(或自己)的热读.多个进程映射同一文件,共享的就是这些缓存页 — 5.5 篇 smaps 里那 9.8MB Shared_Clean 的实体.近年的大动作是 folio:page cache 的操作单位从"恰好一页"泛化成"至少一页",大页缓存不必再拆成一个个 struct page 假装,这是 7.1 篇留的尾巴在这里兑现.

写路径的主角是 Dirty 账.实测写 32MB 到文件,不 fsync:

Dirty: 0 -> 32612 kB
3 秒后: 32768 kB
sync 后: 32 kB

write 返回时数据只进了缓存,Dirty 精确记着 32MB;三秒后纹丝不动 — 回写没这么着急;sync 一声令下,32MB 全部落盘,Dirty 归零.这不是 bug 是设计:写缓存的全部意义就是把 N 次小写攒成少数大块顺序写,回写时机由脏页比例(脏到一定百分比)和过期时间(最多忍多久)双阈值控制,到了由每个块设备的 flusher 线程批量刷.代价是断电窗口:没有 fsync 的"写成功"只是进了内存,数据库们对 fsync 的执念,反对的就是这个窗口.程序退出不触发回写,缓存里的脏页会活到阈值到期 — 拔电源前先 sync,老运维的口头禅在这里有出处.

预读是喂食机.块设备队列里 read_ahead_kb 一项,板上 eMMC 是 128 — 顺序读第一页缺页时,内核不读一页,读 128KB 进来赌你马上要后面的.赌对了,后续"缺页"全在缓存里命中,吞吐逼近设备上限;赌错了(随机访问),预读的页就是白读.page cache 的预读引擎还会根据命中情况动态伸缩窗口,顺序流越读越大,随机流缩回最小.应用程序也能自己点菜:posix_fadvise(POSIX_FADV_SEQUENTIAL/WILLNEED) 和 readahead 系统调用,以及 mmap 后 madvise(MADV_WILLNEED).

回收闭环连到下一篇:这些缓存页都挂在 LRU(或 MGLRU)的文件页链上,内存紧张时干净页直接丢(反正盘上有),脏页先回写再丢 — "drop_caches 只清缓存"的底气是丢干净页零成本,这也是为什么它不影响任何进程的正确性,只影响下一轮冷读.

最后留几个容易忽略的细节:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注