Linux 内存管理的 5 条设计哲学
你给服务器加了 64GB 内存,以为不会再有内存压力。结果一周后 free 显示 MemFree 还是只有几百 MB,系统反而比之前 16GB 时更慢了。
同事抱怨:"内存都去哪了?是不是泄漏了?" 你打开 vmstat,发现 si/so 为 0,但 bi/bo 高得吓人。原来 Linux 内核不是"不会用内存",而是在下一盘更大的棋——用你加的那 64GB 做缓存、做压缩、做预取,只是每一种设计都有代价。
本文总结 Linux 内存管理的 5 条核心设计哲学,帮你理解系统为什么"有时候不卡"、"有时候又突然卡"。
一、以空间换时间:最常见的缓存策略
本章结论:用 RAM 空间避免重复 I/O 或重复计算,是内核缓存最主流的设计哲学。
当你打开一个文件,Linux 不会每次都从磁盘读取。它会先把文件内容读到 RAM 里的 Page Cache,后续再读就直接命中。这就是最经典的"以空间换时间"——用 RAM 空间,换磁盘 I/O 时间。
同样的逻辑在 Swap Cache 中也一样:页被 swap-in 回到 RAM 后,保留 swap entry 不释放,后续再次访问时直接命中,免去了从 zRAM 解压或从磁盘读盘的成本。
Page Cache:磁盘文件 → RAM(换 I/O 时间)
Swap Cache:swap 后端 → RAM(换 I/O 时间)
Dentry Cache:磁盘路径 → RAM(换路径查找时间)
Buffer Cache:块设备数据 → RAM(换块 I/O 时间)
共同点:"我先把一份副本放在 RAM 里,你要的时候直接拿。"
代价:RAM 占用增加。当内存紧张时,这些缓存页会被回收——因为它们的后端都有备份,回收成本极低。
二、以时间换空间:压缩与合并的艺术
本章结论:用 CPU 时间减少内存占用,是内存紧张时的主动策略。
zRAM 是典型的"以时间换空间":
| 阶段 |
行为 |
代价 |
收益 |
| swap out |
压缩 → 写入 zRAM |
CPU 时间 |
内存占用 ↓ |
| swap in |
从 zRAM 解压 → RAM |
CPU 时间 |
获得原始页 |
如果没有 Swap Cache,页被回收后要压缩,又被访问时要解压,往复震荡——CPU 时间白白浪费。Swap Cache 的作用,就是在"以时间换空间"的压缩/解压之间,插入一个"以空间换时间"的缓冲层,避免重复支付压缩/解压的 CPU 成本。
类似的机制还有:
| 机制 |
"时间"花在哪里 |
"空间"省在哪里 |
| zRAM |
压缩/解压 CPU |
减少 RAM 占用 |
| 透明大页(THP) |
合并/拆分 CPU |
减少 TLB miss,提升大页命中率 |
| KSM (内存去重) |
扫描+比较 CPU |
合并相同内容页,减少重复数据 |
关键洞察:"以时间换空间"不是"浪费 CPU",而是在内存紧张时主动用 CPU 资源换取更多可用内存。
三、懒惰策略:不到万不得已不做
本章结论:Linux 内核大量采用"懒"策略——延迟执行直到不得不做,是性能优化的核心手段。
| 机制 |
"懒"在哪里 |
效果 |
| 延迟分配 |
malloc() 不立刻分配物理页 |
进程启动快,但写入时可能 OOM |
| COW (Copy-on-Write) |
fork() 时不复制父进程内存 |
子进程秒启动,写入时才真正复制 |
| 页错误处理 |
缺页时才加载内容到 RAM |
程序启动快,但运行时可能有卡顿 |
| Swap Cache 懒建立 |
swap out 时不建立,swap in 时才建立 |
只有被访问的页才有 Swap Cache |
Swap Cache 的"懒建立"是最典型的例子:页被 swap out 时,内核不会为它建立 Swap Cache。只有当进程再次访问、触发缺页中断、从 zRAM/磁盘读回数据后,才会建立 Swap Cache 映射。
核心理念:"不到万不得已不做,做了就缓存起来。"
四、预取与预读:赌你会用,提前准备
本章结论:预取是"以时间换空间"的反面——用带宽和缓存空间赌未来的命中率。
readahead:你读第 1 页,内核预读第 2-4 页
prefetch:CPU 预测你接下来要用某段数据,提前加载到 L1/L2
| 预取类型 |
触发时机 |
赌什么 |
赌赢省什么 |
赌错浪费什么 |
| 文件预读(readahead) |
顺序读取文件时 |
你会继续读下去 |
磁盘 I/O 时间 |
带宽和 Page Cache 空间 |
| CPU 预取(prefetch) |
循环访问数组时 |
你会访问下一个元素 |
内存访问延迟 |
缓存行和总线带宽 |
Swap Cache 不参与预取——它是"事后补救",不是"事前预测"。
关键洞察:预取和 Swap Cache 是互补的。预取是"我猜你会用",Swap Cache 是"你已经用过了,我帮你留着"。
五、局部性感知:把"附近的"放在一起
本章结论:利用访问局部性,把相关数据放在物理上接近的位置,是缓存设计的底层假设。
| 机制 |
局部性类型 |
作用 |
| SLAB/SLUB |
空间局部性 |
相同大小的对象放在一起,减少碎片,提升分配效率 |
| NUMA 本地分配 |
空间局部性 |
内存分配在本地 CPU 节点,避免跨节点访问延迟 |
| 透明大页(THP) |
空间局部性 |
连续小页合并成大页,减少 TLB miss |
| CPU 缓存对齐 |
空间局部性 |
数据按缓存行对齐,避免 false sharing |
Swap Cache 虽然没有直接的局部性优化,但它利用了时间局部性:刚被访问过的页,很可能马上又被访问。保留它在 RAM 中,就是利用了"最近被访问的页,未来更可能被访问"的假设。
关键洞察:局部性感知是所有缓存策略的底层假设。没有局部性,缓存就失去意义。
源码参考
| 函数 / 符号 |
所在文件 |
作用 |
read_swap_cache_async() |
mm/swap_state.c |
swap-in 路径入口,为目标预留 Swap Cache 空间 |
__swap_cache_add_folio() |
mm/swap_state.c |
将 swap entry 关联到 folio,更新 NR_SWAPCACHE |
swap_cache_get_folio() |
mm/swap_state.c |
按 swap entry 查找已驻留的 folio |
readahead() |
mm/readahead.c |
文件预读,提前加载后续页到 Page Cache |
__do_page_fault() |
arch/x86/mm/fault.c |
页错误处理,延迟分配 + COW 实现 |
ksm_scan_thread() |
mm/ksm.c |
KSM 内存去重,扫描并合并相同内容页 |
alloc_pages() |
mm/page_alloc.c |
物理页分配,支持 NUMA 本地分配 |
kmem_cache_alloc() |
mm/slab.c / mm/slub.c |
SLAB/SLUB 分配器,按大小分桶 |
全文方法总结:5 条设计哲学的取舍矩阵
| 设计哲学 |
核心策略 |
典型场景 |
权衡 |
| 以空间换时间 |
RAM 空间 → 避免重复 I/O |
Page Cache、Swap Cache、Dentry Cache |
空间 ↑,时间 ↓ |
| 以时间换空间 |
CPU 时间 → 减少内存占用 |
zRAM、THP、KSM |
时间 ↑,空间 ↓ |
| 懒惰策略 |
延迟执行 → 减少不必要开销 |
延迟分配、COW、懒建立 |
启动快,突发开销 |
| 预取/预读 |
带宽/空间 → 赌未来命中 |
readahead、prefetch |
赌对省时间,赌错浪费 |
| 局部性感知 |
物理邻近 → 提升命中率 |
SLAB、NUMA、THP、缓存对齐 |
命中率高,碎片可能增加 |
思考题
Q4:如果没有 Swap Cache,只有 zRAM 会怎样?
zRAM 压缩是典型的"以时间换空间",Swap Cache 是典型的"以空间换时间"。如果系统中没有 Swap Cache,只有 zRAM,会发生什么?这俩为什么必须配合?
没有 Swap Cache 的后果:
Swap Cache 的作用:
| 阶段 |
有 Swap Cache |
无 Swap Cache |
| 第一次 swap out |
压缩到 zRAM |
压缩到 zRAM |
| 再次访问 |
直接命中 Swap Cache |
从 zRAM 解压 |
| 再次回收 |
直接丢弃 |
再次压缩到 zRAM |
| CPU 成本 |
低(只压缩一次) |
高(反复压缩/解压) |
核心结论:Swap Cache 是"以空间换时间"对"以时间换空间"的补偿机制。zRAM 压缩用 CPU 换内存,Swap Cache 用 RAM 换 CPU,二者配合才构成完整的内存回收策略。
Q5:内存紧张时,优先回收预取的 Page Cache 还是 Swap Cache?
预取(readahead)和 Swap Cache 都是缓存机制,但一个是"事前预测",一个是"事后补救"。如果系统内存极度紧张,应该优先回收预取的 Page Cache 还是 Swap Cache?为什么?
优先回收预取的 Page Cache。
判断依据:
| 缓存类型 |
本质 |
回收成本 |
回收策略 |
| 预取的 Page Cache |
"我猜你会用" |
低(干净页直接丢弃) |
优先回收,因为"猜错"了 |
| Swap Cache |
"你已经用过了" |
极低(直接丢弃) |
也直接丢弃,但保留价值更高 |
为什么预取的 Page Cache 优先:
- 预取是"猜测":预取的内容可能是"猜错"的,用户根本不需要
- Swap Cache 是"事实":页已经被访问过,再次被访问的概率更高
实际回收顺序:
kswapd 扫描 inactive 链表:
→ inactive file(Page Cache)
→ 预取的 Page Cache:优先丢弃(成本低,且可能猜错)
→ 非预取的 Page Cache:其次
→ inactive anon(匿名页)
→ Swap Cache:直接丢弃(保留价值高,但后端有备份)
→ 普通匿名页:swap out(成本高)
核心结论:预取是"赌博",Swap Cache 是"保险"。内存紧张时,先清掉"赌错"的,保留"有 backup"的。