背景
Linux 的 proactive reclaim 允许用户态通过 memory.reclaim 主动释放内存。后续为该接口增加了 swappiness=max 参数,对应内部常量 SWAPPINESS_ANON_ONLY,其语义是只回收匿名页:把冷匿名页换出到 swap,而绝不触碰 page cache。这对需要在分配压力来临前主动收缩匿名占用的负载很有用,因为它让回收动作的边界变得精确。
回收扫描的入口是传统 LRU 的 get_scan_count 与 MGLRU 的 get_swappiness 加 get_nr_to_scan。它们根据 swappiness、可用 swap、各 LRU 长度等输入,决定本次扫描匿名页、文件页还是两者按比例平衡。当用户请求 swappiness=max 时,期望决策恒为只扫匿名页。
proactive reclaim 仅回收匿名页的回收回路
┌─────────────────────┐ ┌────────────────────────┐ ┌────────────────┐ ┌──────────────┐
│ 写入 memory.reclaim │ │ user_proactive_reclaim │ │ get_scan_count │ │ │
│ 并指定 swappiness ├──▶│ 解析请求 ├──▶│ 决定扫描哪类 ├──▶│ 只扫描匿名页 │
│ =max │ │ │ │ 页 │ │ │
└─────────────────────┘ └────────────────────────┘ └────────────────┘ └──────────────┘
问题
SWAPPINESS_ANON_ONLY的判断排在"无可回收匿名页"判断之后,顺序错误。- 当系统无 swap 等导致无可回收匿名页时,请求先落入只扫文件页分支。
swappiness=max反而驱逐 page cache,违反只回收匿名的语义。- MGLRU 中
get_swappiness把该值归零,匿名专属语义丢失,扫描仍落到文件页。 - per-node 接口把错误折叠成
-EAGAIN,用户态无法分辨原因。
复现很直接。在某个持有 64M file cache 且无 swap 的 cgroup 里执行 echo "64M swappiness=max" > memory.reclaim:修复前 page cache 从约 64M 被错误驱逐到 4K,pgsteal_proactive 达到 16400;修复后 page cache 原封不动,写操作返回 Resource temporarily unavailable,pgsteal_proactive 归零。MGLRU 路径同样把 64M file cache 驱逐到 0。
方案
修复的核心是把 SWAPPINESS_ANON_ONLY 的判断前移到所有可能改变扫描目标的检查之前。
一旦识别出只回收匿名页这一最强约束,就不再让后续的无 swap 则扫文件逻辑有机会接管决策。
传统 LRU 的 get_scan_count 在函数开头先判断该标志。若无可回收匿名页,直接把各 LRU 的扫描量清零并返回,主动放弃本次回收以保住 page cache;否则设置扫描模式为只扫匿名页并跳出:
if (swappiness == SWAPPINESS_ANON_ONLY) {
if (!can_reclaim_anon_pages(memcg, pgdat->node_id, sc)) {
memset(nr, 0, sizeof(*nr) * NR_LRU_LISTS);
return;
}
scan_balance = SCAN_ANON;
goto out;
}
这段逻辑的关键不在新增能力,而在调整顺序:原来它位于无 swap 则只扫文件判断之后,那个判断会抢先命中并把决策锁死到文件页,使匿名专属请求被静默改写。前移之后,匿名专属语义成为最先裁决的硬约束。
MGLRU 走的是另一套函数,但语义修复完全一致。get_swappiness 提前计算 swappiness,一旦识别为 SWAPPINESS_ANON_ONLY 就直接返回该值,绕过原本会把它归零的无 swap 检查,使下游能看到完整的匿名专属语义;get_nr_to_scan 据此在无可回收匿名页时返回零扫描量,避免对文件页做无用功。
仅回收匿名页请求的扫描决策修复后顺序
┌──────────────────────┐
│ 进入扫描决策 │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 是否请求仅回收匿名页 │ 否则走原有的匿名与文件页平衡
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 是否存在可回收匿名页 │ 否则清零扫描量直接返回以保住文件页
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 仅扫描匿名页 │
└──────────────────────┘
此外还附带 per-node proactive reclaim 的错误码修复,使其透传真实错误码而不再统一折叠成 -EAGAIN,让调用方能区分已无可回收内存与接口被并发占用 2 种情形,与 memcg 路径保持一致;并删除 __node_reclaim 中已不再使用的 gfp_mask 参数。
收益
作者未提供性能数据,从代码逻辑推断的预期收益:
- 无可回收匿名页时不再误驱逐 page cache,避免随后重新读入引发的额外 IO。
swappiness=max的语义在有无 swap 2 种情形下都成立,proactive reclaim 的回收边界恢复精确。- per-node proactive reclaim 返回真实错误码,便于用户态诊断与重试决策。