本页目录
页面回收与交换
覆盖: LRU (匿名/文件) → kswapd + direct reclaim → swap 机制 → 反向映射 (rmap) → page cache reclaim → compaction 内核版本: 2.6 ~ 6.x,重点标注 multi-gen LRU (6.1+)
概述
当物理内存不足时,内核必须收回一些页面。回收的对象分两类:
- 文件页 (file-backed): 干净的直接丢弃(有磁盘副本),脏的写回
- 匿名页 (anonymous): 必须写到 swap 才能回收
回收策略的核心是 LRU (Least Recently Used)——把冷页面换出,保留热页面。但"冷热"判断在几百 GB 的工作集中是极其困难的。Linux 从传统的双链表 LRU 演进到 6.1 引入的 Multi-Gen LRU (MGLRU),显著改善了对大工作集和扫描风暴的处理。
LRU 链表
传统双 LRU (2.6 ~ 5.x)
// mm/vmscan.c
// 每个 memcg 和每个 node 有一组 LRU:
;
// 页面在 active 和 inactive 之间移动:
// 首次映射 → inactive
// 被再次访问 → 提升到 active
// 长时间未被访问 → 降级到 inactive
// inactive 中的页面 → 回收候选
回收优先级
// 回收顺序 (文件页优先于匿名页):
// 1. 干净文件页 (drop, 最快)
// 2. 脏文件页 (写回)
// 3. 匿名页 (swap out)
//
// 为什么文件页优先?
// 干净文件页: 直接丢弃, 0 IO
// 匿名页: 必须写入 swap → IO 开销
// 脏文件页: 必须写回 → IO 开销
//
// /proc/sys/vm/swappiness 控制这个优先级:
// 0: 尽量回收文件页,除非实在不够才 swap
// 100: 平等对待文件和匿名
// 默认 60
Multi-Gen LRU (6.1+)
传统 LRU 的问题:
- active/inactive 只有两代 → 粒度太粗
- 冷热判断依赖 referenced bit + 周期性扫描
- 大工作集下扫描成本极高 ("scanning storm")
MGLRU 的方案:
页面按代 (generation) 分组,每代有独立的 LRU
新一代 = 活跃 (最近被访问)
旧一代 = 冷 (候选回收)
回收时从最老一代开始 → 自然排除热页面
不需要扫描所有页面来判断冷热 → 扫描量大幅减少
实现: mm/vmscan.c (MGLRU build) + /sys/kernel/mm/lru_gen
Google 和 Meta 都报告了显著的 CPU 节省
(扫描开销降低 50%~90%)
kswapd 与 Direct Reclaim
kswapd: 后台回收
// mm/vmscan.c
// 每个 NUMA node 都有自己的 kswapd 内核线程
// 唤醒条件:
// 空闲页 < high watermark → kswapd 醒来
// kswapd 持续回收直到空闲页 > high watermark
static int
Direct Reclaim: 同步回收
// 当 kswapd 来不及回收 (内存分配太快), 分配者自己回收:
→
→ →
→ →
→ →
// 与 kswapd 相同的回收逻辑,但是同步执行
// 分配者阻塞在这里直到有可用的页面
direct reclaim 会导致分配延迟突增——这也是为什么大家都关注"内存压力"指标。
Swap
Swap 分区/文件
# 配置
# 优先级 (多个 swap 设备)
Swap 出 (匿名页 → swap)
// mm/vmscan.c
// shrink_page_list() 回收匿名页时:
if
// swap cache:
// 页面在 swap 和内存之间过渡时的缓存
// 如果 swap 出和 swap 入之间有 race → swap cache 提供一致性
Swap 入 (page fault → swap)
// mm/memory.c
// do_swap_page(): 进程访问了被 swap 出去的地址
├─ // 检查 swap cache (可能在内存中)
│ └─ 如果在:
│
└─ // 从 swap 设备预读
└─
→ // 提交 IO
→ 页面入 swap cache
→ 映射 PTE
→ 用户进程继续执行
反向映射 (rmap)
// mm/rmap.c
// 反向映射回答: "给定一个物理页, 哪些进程的哪些虚拟地址映射了它?"
// 匿名页:
// 通过 anon_vma + VMA 的 interval tree 遍历所有映射
// page->mapping → anon_vma → anon_vma_chain → VMA
// 文件页:
// 通过 address_space + page->index 查找
// page->mapping → address_space → i_mmap (interval tree)
// rmap_walk(page, func):
// 遍历 page 的所有 PTE 映射
// 对每个映射调用 func → 如 try_to_unmap
// → 将 PTE 标记为 swap entry 或清空
// → flush TLB
// try_to_unmap() 是 swap 的前提:
// 必须找到所有 PTE 引用,全部解除后,页面才能回收
Compaction (内存压缩)
// mm/compaction.c
// 问题: buddy allocator 需要连续页面 (如 order 4 = 16 页 = 64KB)
// 但内存已经碎片化 → 无法分配连续块
// compaction: 将已使用的页面"搬走",腾出连续的空闲区域
// 1. 扫描: 找 free pages (目标) 和 used pages (源)
// 2. 迁移: 将 used page 内容复制到 free page
// 3. 更新页表: 修改所有 PTE 指向新位置
// 4. 重复: 直到腾出足够连续空间
// 触发方式:
// 1. kcompactd: 后台线程 (类似 kswapd)
// 2. direct compaction: 分配者在 slowpath 中主动压缩
// 查看 compaction 状态:
cat /proc/vmstat | grep compact
调试与观测
# 内存压力概览
# SwapTotal/SwapFree: swap 使用
# Active/Inactive(anon/file): LRU 中的页面
# Dirty/Writeback: 待写回页面
# 回收统计
|
# pgsteal_*: 回收了多少页
# pgscan_*: 扫描了多少页
# scan >> steal → LRU 扫描效率低 (MGLRU 改善)
# 是否启用了 MGLRU
&&
# kswapd 行为
|
# compaction
|
# 触发手动回收 (测试用, 生产慎用)
参考与延伸
- 内核文档:
Documentation/admin-guide/sysctl/vm.rst,Documentation/mm/multigen_lru.rst - LWN:
- "Multi-generational LRU" (lwn.net/Articles/856932/)
- "The case for MGLRU" (lwn.net/Articles/916777/)
- "Swap and the page cache" (lwn.net/Articles/358953/)
- 源码文件:
mm/vmscan.c— kswapd + LRU 扫描 + direct reclaimmm/swap_state.c— swap cachemm/swapfile.c— swap 分区/文件管理mm/rmap.c— 反向映射mm/compaction.c— 内存压缩mm/page_io.c— swap IO
关键词: LRU, MGLRU, kswapd, direct reclaim, swap, swappiness, rmap, try_to_unmap, compaction, page cache reclaim