同一份文件读两遍,第二遍比第一遍快多少?这个日常经验的背后是 page cache:内核拿空闲内存缓存文件内容,读走内存,写先攒着.这篇把这本"最大的账"的进出规则实测一遍 — 冷热差,缓存涨落,脏页积压与回写,以及预读这台自动喂食机.
这篇回答几件事:
- page cache 命中和不命中差多少,Cached 账怎么变化
- 写文件时 Dirty 账本怎么走,数据什么时候真正落盘
- 预读在哪配置,起什么作用
- address_space 和 folio 在源码里是什么角色
先做冷热对照.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 只清缓存"的底气是丢干净页零成本,这也是为什么它不影响任何进程的正确性,只影响下一轮冷读.
最后留几个容易忽略的细节:
- cp 一个大文件,源和目标都会进缓存,内存观感是双倍文件大小;tmpfs 的页也在 Cached 里但不叫 page cache,drop_caches 清不掉它们(7.6 篇的坑).
- Dirty 偶尔是负数方向的怪值不存在,但 Dirty+Writeback 才是全部未落盘数据,盯单列会低估.
- 直接 IO(O_DIRECT)绕过 page cache,数据库自管缓冲的场景用它;代价是丢掉了预读和写合并,小 IO 性能反而差,不是"高级选项"是"特化选项".
- 脏页回写默认有 each-BDI 节流,进程狂写把系统 Dirty 拉满时,它自己的写会被按住回写等一轮(vm.dirty_ratio 的另一面),表现为"写一会卡一会".
- mmap 改文件走的是这条同一套缓存页,msync 是它的 fsync;只 munmap 不同步,脏账还在.
- page cache 是按文件不是按文件系统组织的,NFS 上同样一套结构,只是"回写"和"丢页"背后是网络,抖动逻辑完全不同.
- free 命令的 buff/cache 合并显示掩盖了细节,/proc/meminfo 里 Cached,Buffers,SwapCached 各是各的,排查要分列看.