Linux:缓存
在实际的 CPU 与磁盘数据读写中,三级缓存帮助我们更快地取到数据。磁盘 I/O 的速度非常慢,计算机发展了几十年,硬盘的读取速度却没有多大提升,这严重影响了 CPU 的性能发挥。因此,计算机用缓存来帮助 CPU 更快地获取数据。
在 Arm 官方整理的一份延迟对照表中,L1 缓存访问约 0.5 ns,L2 约 7 ns,主内存约 100 ns。这还只是 CPU 与内存之间的差距。继续往下走,从 SSD 随机读取 4 KB 数据约需 150,000 ns(150 μs),而机械硬盘的一次寻道是 10,000,000 ns(10 ms)。换句话说,一次 HDD 寻道的时间里,CPU 可以在 L1 缓存里完成两千万次访问。如果程序每次需要的数据都直接从磁盘取,CPU 绝大部分时间都在空转。
这就是缓存存在的根本理由:存储层级中上下两层的速度差距已经大到无法靠指令级优化来掩盖。缓存的本质,是在快但小的存储和慢但大的存储之间插入一个容量与速度折中的中间层,让绝大多数访问落在快的一侧。命中率决定了这个折中是否有效——L1 配合 L2、L3 之后,CPU 整体缓存命中率可以达到 95% 左右。剩下的 5% 才穿透到内存乃至磁盘,系统才得以在可接受的延迟内运行。
但“插入一个中间层”只是故事的开始。真正困难的是:当中间层的空间满了,该把谁请出去。这个问题在硬件和软件两个层面上以不同的方式被回答,而答案的演化,构成了缓存算法的主线。
1. 硬件的三级缓存
CPU 的三级缓存是在不断更新与迭代中确定的。
L1 缓存离核心最近,速度最快,但容量极其有限。现代 CPU 的 L1 通常在 32 KB 到 256 KB 量级,并且已经拆分为指令缓存和数据缓存,各自独立,以减少取指和取数之间的争用。L1 的容量上不去,不只是成本问题:越靠近核心的存储,访问延迟对容量越敏感,做大一点就可能把关键路径拖慢。因此 L1 只适合承接最热的那一小撮数据和指令。
如果只有 L1,一旦发生 L1 缺失,CPU 就要直奔主内存,等待上百纳秒。L2 填补的正是这一段。L2 通常为每个核心独享,容量在数百 KB 到数 MB 之间,访问延迟约 10 个时钟周期,明显慢于 L1 的 3 到 4 个周期,但仍然远快于内存的数百个周期。L2 的使命不是替代 L1,而是把“L1 装不下的热数据”接住,避免每次 L1 缺失都穿透到内存。
那为什么还需要 L3?答案在于多核。L1 和 L2 都是核心私有的,如果只靠这两级,多个核心访问共享数据时,每个核心都要在自己的私有缓存里保留一份副本,缓存一致性协议的压力会非常大。L3 被设计为核心间共享的最后一级缓存,容量通常可达 16 MB 甚至更大,访问延迟约 50 个时钟周期。它不只是“更大的缓存”,更是多核之间共享数据的汇聚点:核心私有的 L1/L2 过滤掉大部分本地访问,L3 承接跨核共享,再往下才是内存。
三级的分工可以粗略概括为:L1 管“最热且最急”,L2 管“私有热数据”,L3 管“共享热数据”。再往下做第四级私有缓存,收益会被一致性协议和访问延迟的增加吃掉;只做两级,多核共享场景又会退化。三级是在延迟、容量、一致性和成本之间目前最稳定的平衡点。
硬件缓存的淘汰策略比软件世界朴素得多。CPU 缓存通常采用组相联结构,每一组内的替换多由硬件近似完成(如伪 LRU 或随机策略)。硬件没有余裕去维护全局精确的访问顺序,accessed 位是它仅有的廉价信号。这种“只记录是否被碰过,而不记录何时被碰过”的局限,在软件缓存和操作系统页面回收中会以另一种形式再次出现。
2. 磁盘缓存
CPU 缓存挡住的是“访问内存太慢”,但真正慢得离谱的是磁盘。一次内存访问 100 ns,一次 SSD 随机读 150 μs,一次 HDD 寻道 10 ms——层级越往下,速度差距越夸张。于是操作系统在内存里又架了一层缓存,专门用来缓存磁盘上的数据。这就是 Page Cache,也叫磁盘缓存。
Page Cache 的存储单位是页,典型 4 KB。索引结构挂在 inode 上:每个 inode 有一个 address_space 对象,其中的 i_pages 是一棵按文件内偏移组织起来的树(早期是 radix tree,4.20 之后换成 XArray)。内核判断“文件偏移 X 处的数据在不在缓存里”,就是拿 X 除以 4 KB 得到索引,去这棵树里查一次,命中拿到页指针,没命中返回空。
这个结构解释了一个常见现象:两个进程读同一个文件,内存里只有一份数据。进程 A 读 /etc/passwd,页被加载进 Page Cache;进程 B 再读同一个文件,走的是同一个 inode 的 address_space,查到的是同一批页。Page Cache 是全局的、按文件共享的,不是按进程私有的。
3. 缓存算法
3.1 LRU
LRU(Least Recently Used)实现比较简单:淘汰最久未被访问的元素。它背后的假设是时间局部性——如果一个数据刚刚被访问过,那么它在近期再次被访问的概率,高于一个很久没有被碰过的数据。对于网页浏览、文件系统元数据访问、OLTP 数据库的索引查找这类负载,这个假设大体成立,因此 LRU 长期是通用缓存和操作系统页面回收的默认基线。
实现层面,LRU 用哈希表加双向链表做到 O(1) 的 get 和 put:哈希表负责定位节点,双向链表按访问时间排序。命中时将节点摘除并重新插入头部,插入新节点时也放入头部,容量超限则淘汰尾部节点。双向链表之所以不可替代,是因为只有持有节点指针时才能 O(1) 地完成“从任意位置删除”,单向链表做不到这一点。
但 LRU 的简洁恰恰来自它对访问信息的极大压缩:它只记录“最后一次访问发生在什么时候”,不记录访问了几次,也不区分这一次访问是顺序扫描的一部分还是真正的热点命中。这个压缩在理想负载下几乎无损,在真实负载下却会系统性地出错。
LRU 的第一个缺陷是顺序扫描污染。当一次全表扫描、批量备份或 SELECT * 无索引查询发生时,数据库或操作系统会把大量只读一次的数据页依次推入缓存。在标准 LRU 下,这些页会占据链表头部,把原本的高频热点页推向尾部并最终淘汰。扫描结束后,缓存里留下的是不会再被读的“一次性页”,而真正的工作集需要重新从磁盘加载。一篇 2026 年的 InnoDB 调优分析给出了量级:一次全表扫描就能让缓存命中率从 99% 跌到 80% 以下,后续查询被迫走磁盘 I/O,响应时间可能上升一个数量级。学术界对此的描述同样直接:低复用率的页面驱逐了高复用率的页面,LRU 在存在扫描和污染时命中率会下降。
第二个缺陷是预读的副作用被 LRU 放大。顺序读场景下,系统会预取后续相邻数据并放入缓存。如果这些预读页没有被真正使用,它们仍然会占据 LRU 的头部位置,挤走真正有用的页。问题不在于预读本身是错的——预读对顺序访问的吞吐有明确收益——而在于 LRU 无法区分“被请求的页”和“只是被预取进来的页”,两者在它眼里都只是一次“访问”。
3.2 LRU-K
LRU-K 的做法是把判断标准从“最近被访问过 1 次”提高到“最近被访问过 K 次”。新数据不直接进入缓存主队列,而是先进入一个历史访问队列;只有当它在该队列中累计达到 K 次访问,才被晋升到真正的 LRU 缓存队列。淘汰时依据的是“第 K 次最近访问距今多久”,而不是“最后一次访问距今多久”。LRU 可以视为 K=1 的退化情形。LRU-2 在实践中被证明可以拿到 LRU-K 的大部分收益:一个页面必须在两次不同的时间点被访问,才有资格进入缓存,一次性扫描页在第一次访问后自然停留在历史队列中并被淘汰。
但 LRU-K 的代价是 O(log N) 的队列维护开销。每次访问都需要操作一个按“第 K 次访问时间”排序的优先队列,对于高吞吐系统来说这个开销不可忽视。
3.3 MGLRU
MGLRU(Multi-Generational LRU)于 2022 年底合并进 Linux 6.1,试图用多个“代”替代二值的 Active/Inactive 判断。页在缺页时进入最年轻的代,随着老化推进代号递增,回收从最老的代中选取牺牲页。理论上,这提供了一个更清晰的时间轴:内核可以在扫描时看到页在多个代之间的分布,而不是只知道它“活跃或不活跃”。一项 2025 年的评测也确认,MGLRU 在多种内存约束下确实能做出更好的回收决策,尽管决策开销更高,且在内存极度紧张时收益会减弱。
但 MGLRU 的工程现实远不如“更新一代算法所以更好”那样线性。LWN 在 2026 年的回顾中指出,MGLRU 的推进已经陷入停滞,在许多系统中仍未启用,甚至有位开发者呼吁将其彻底移除。一个被反复提及的问题是它在匿名页和文件页之间的回收平衡上表现不佳:传统双链表为两类页维护独立列表,并通过 swappiness 调优回收倾向,而 MGLRU 在这方面被认为不够理想,多个 Android 系统供应商因此选择不开启它。这意味着,一个在“冷页识别准确率”上更优的算法,可能因为打破了既有的页类型平衡和调优接口,而在落地时遭遇阻力。
4. 总结
从 CPU 的三级缓存到操作系统的 Page Cache,再到 LRU、LRU-K 和 MGLRU 的演进,缓存要解决的问题始终只有一个:在容量有限的前提下,尽量让访问落在快的一侧。硬件用组相联结构和近似 LRU 换取低延迟,软件则用更精细的访问历史换取更高的命中率。
LRU 简单高效,却容易被顺序扫描和预读污染;LRU-K 通过记录第 K 次访问时间弥补了这一缺陷,代价是更高的维护开销;MGLRU 用多代结构给出更细的时间轴,但工程落地仍受制于页类型平衡和调优接口的兼容性。没有一种算法在所有场景下都最优,选择哪一种,取决于负载特征、延迟预算和实现成本之间的权衡。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)