mm/mglru: use folio_mark_accessed to replace folio_set_active

内存管理 · MGLRU 页回收 · 缺页/预读页的活跃代定位修正

💡 一句话总结

在内存受限(memcg 1GB)且大量读取文件页的构建类负载下,MGLRU 原本在每次缺页时就把读入的页标记为活跃(folio_set_active()),导致预读带进来的冷页挤占"受保护"的年轻代、把真正热的工作集挤出内存,引发大量换入换出与 refault;本补丁改用 folio_mark_accessed(),让缺页读入的文件页从"第二旧代"冷起步、只有被真实复用才提升到活跃代——作者在 x86 内核构建(1GB memcg、20 线程)实测:换入页数 -57%、换出页数 -47%、内核态 CPU 时间 -9.4%(作者自报,未独立验证)。

📋 补丁基本信息

项目内容
补丁类型优化(页回收行为修正:消除 MGLRU 对缺页/预读冷页的过度保护)
状态Merged(已合入主线 Linux 7.2,首个含此补丁的发布 tag 为 v7.2-rc1;commit 2026-06-04,Andrew Morton 合入)
当前版本v3(合入主线版)· lore 链接 · git.kernel.org commit
版本演进 RFC(02-25,lazily activate)→ v1(04-18,+1/-1)→ v2(05-25,+15/-11)→ v3(05-26,+25/-9,合入)
作者机构Barry Song (Xiaomi) <baohua@kernel.org>(小米),Tested-by Lance Yang / Xueyuan Chen
提交日期2026-06-04(合入);作者署名 2026-05-26
改动范围4 文件(include/linux/mm_inline.h、mm/swap.c、mm/vmscan.c、mm/workingset.c),+25/-9
核心函数folio_add_lru() / lru_gen_folio_seq() / lru_gen_set_refs() / lru_gen_refault()
原始链接[PATCH v3] lore Message-ID 20260526130938.66253-1-baohua@kernel.org

📊 速览卡片

核心机制
冷启复用晋升
优化目标
回收准确性
适用场景
内存压力负载
特性等级
★★★★
实测提升
换入-57%

特性等级依据:内存受限场景收益显著(换入页 -57%、内核态 CPU 时间 -9.4%,作者自报)且纯内核改动、无硬件/配置门槛、易于落地;但收益集中在文件页 + 内存压力负载,并改变了回收语义有一定行为风险 → ★★★★

🎯 解决什么问题

背景 / 原始动机
作者在 RFC 中描述了起点(来源:RFC message-id 20260225212642.15219-1-21cnbao@gmail.com):MGLRU 在"新 folio 加入且 lru_gen_in_fault() 为真"时就把页激活。问题是当地址 N 发生缺页时,readahead(预读)会把 N 周围许多 folio 一起读进来,这些预读 folio 也一起被激活,尽管它们中很多永远不会被真正访问。作者引用合作者 Tangquan 的观察:这在 Android 设备上用 MGLRU 时会造成显著的 file refault。作者还提到 Lei Liu 曾提出"为 readahead 单独建一条 LRU"的方案(message-id 20250916072226.220426-1-liulei.rjpt@vivo.com),但作者认为那"可能过度设计了"(over-engineered)。
系统层面:folio_set_active 与 MGLRU 代际定位的错配
MGLRU(Multi-Gen LRU,多代 LRU)把每类页(anon/file)分成最多 4 个"代"(generation),max_seq 是最年轻代、min_seq 是最旧代,回收(eviction)从最旧代开始。一个 folio 落进哪一代由 lru_gen_folio_seq() 决定。补丁前,缺页路径的 folio_add_lru() 会对每一个在缺页时读入的 folio 调用 folio_set_active() 置上 PG_active 位——而 MGLRU 对带 PG_active 的 folio 是放进"第二年轻代"(活跃代)。于是"读进来但从未被访问"的预读页与真正热的工作集页享受同样的高优先级保护,把真正的热点挤出内存。
场景层面:内存受限 + 大量读文件页的负载
触发条件是"内存有压力 + 文件页读入频繁":缺页/预读读入大量文件页,同时内存容量不够、回收必须持续运行。典型负载包括:内存受限容器里的内核/软件构建(作者基准,1GB memcg 下 make -j20)、数据库/服务进程的工作集超限、移动设备(Android)(RFC 中 Tangquan 的观察来源)。这类负载下,readahead 预取的文件页数量远大于实际被访问的页,而旧代码把它们全部放进活跃代 → kswapd 选不中该回收的冷页 → 换入换出与 refault 激增,回收方向失真。
受影响负载:内存受限容器构建 / 文件缓存密集的服务与移动端 · 为什么此特性解决此场景:让冷文件页从旧代起步、把活跃代让给真正复用的页,回收选 victim 更准(作者自报基准 + 解读(AI 分析))

🧩 核心机制

一句话:把"缺页即激活"改成"缺页先标 referenced、真正复用才提升"——用更弱的 PG_referenced 信号起步,用更强的 PG_active/PG_workingset 信号只奖励真实热点。

从系统层面看:四个改动点如何配合
文件/函数改动为什么是机制关键
mm/swap.c folio_add_lru()缺页读入时不再无条件 folio_set_active();改为:workingset folio → 仍 folio_set_active();否则若尚未 referenced → folio_mark_accessed()分流点:只有"refault 回来的 working set"才享受活跃代保护;普通缺页/预读页只打 PG_referenced
include/linux/mm_inline.h lru_gen_folio_seq()文件页代际公式从 MAX_NR_GENS - workingset 改为 MAX_NR_GENS - (workingset || referenced);恢复 anon folio 原有分支落点:带 PG_referenced 的文件页进入"第二旧代",未 referenced 的进最旧代——从冷处起步;anon 仍受保护(第二年轻代)
mm/vmscan.c lru_gen_set_refs()rmap 扫描路径从"一律置 workingset"改为"第 2 次访问才置 workingset,否则 folio_mark_accessed()"晋升门:恢复"二次访问才晋升"机制(Kairui review 驱动的修复)
mm/workingset.c lru_gen_refault()WORKINGSET_ACTIVATE 计数只在 workingset 分支内(与 folio_set_active() 对齐)记账对齐:防止 refault 非 working set 页被误计入激活统计
MGLRU 代际定位 before/after 示意
图 1:缺页读入的文件页在 MGLRU 代际中的落点变化(Before/After)
来源:基于 git show 真实 diff(commit 6cbdd9726fb5)绘制
机制表:代际落点映射
folio 状态旧行为落点新行为落点回收优先级
缺页读入的文件页(未复用)PG_active → 第二年轻代(活跃代)PG_referenced → 第二旧代从"难回收"变"易回收"
缺页读入、从未被访问的预读页同上(过度保护)无 referenced → 最旧代最先被回收
真正被复用的文件页(第 2 次访问)—lru_gen_set_refs() 置 PG_workingset → 提升到活跃代与旧行为一致(获得保护)
refault 回来的 working set 页folio_set_active()folio_set_active()(保留)不变
anon 页(非 swapcache)缺页时 PG_activelru_gen_folio_seq() 的 anon 分支仍给第二年轻代不变(Kairui review 要求恢复)
关键代码片段
diff --git a/mm/swap.c b/mm/swap.c
index 2dd84813f4dd..588f50d8f1a8 100644
--- a/mm/swap.c
+++ b/mm/swap.c
@@ -544,10 +544,20 @@ void folio_add_lru(struct folio *folio)
 			folio_test_unevictable(folio), folio);
 	VM_BUG_ON_FOLIO(folio_test_lru(folio), folio);
 
-	/* see the comment in lru_gen_folio_seq() */
+	/*
+	 * For refaulted workingset folios, set PG_active so they
+	 * can be added to active generations.
+	 * For prefaulted file folios, folio_mark_accessed() sets
+	 * PG_referenced so lru_gen_folio_seq() places them into
+	 * the second oldest generation.
+	 */
 	if (lru_gen_enabled() && !folio_test_unevictable(folio) &&
-	    lru_gen_in_fault() && !(current->flags & PF_MEMALLOC))
-		folio_set_active(folio);
+	    lru_gen_in_fault() && !(current->flags & PF_MEMALLOC)) {
+		if (folio_test_workingset(folio))
+			folio_set_active(folio);
+		else if (!folio_test_referenced(folio))
+			folio_mark_accessed(folio);
+	}
 
 	folio_batch_add_and_move(folio, lru_add);
 }

▲ 这是整个机制的分流核心:旧代码对每个缺页读入的 folio 都 folio_set_active()(置 PG_active → 活跃代);新代码只对 PG_workingset(refault 回来的真实工作集)保留 folio_set_active(),其余(含所有普通预读/缺页页)只 folio_mark_accessed()。关键在 folio_mark_accessed() 在 MGLRU 下走 lru_gen_inc_refs()——只置 PG_referenced 位并累加一个引用计数,不触碰 LRU 链表结构,比"激活"轻得多,而 lru_gen_folio_seq() 会把带 PG_referenced 的文件页放进第二旧代。

diff --git a/mm/vmscan.c b/mm/vmscan.c
index 3c856a78c0a5..76193a84a2af 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -850,7 +850,11 @@ static bool lru_gen_set_refs(struct folio *folio)
 		return false;
 	}
 
-	set_mask_bits(&folio->flags.f, LRU_REFS_FLAGS, BIT(PG_workingset));
+	/* Promote on second access */
+	if (folio_lru_refs(folio) > 1)
+		set_mask_bits(&folio->flags.f, LRU_REFS_FLAGS, BIT(PG_workingset));
+	else
+		folio_mark_accessed(folio);
 	return true;
 }
 #else

▲ 这是"复用才晋升"的第二个入口(rmap 扫描路径):旧代码对 folio_check_references() 判定为 referenced 的 folio 一律置 PG_workingset(晋升);新代码只在 folio_lru_refs() > 1(第 2 次访问)时才晋升,首次访问只 folio_mark_accessed()。为什么必须改:因为补丁让 PG_referenced 在缺页时就置上(提前了),若这里还按"首扫见 referenced 就晋升",任何刚被缺页读入的冷页都会被立刻提升——必须改判据为"引用计数 > 1"才能区分"第一次读到"与"真的在用"。

📈 性能影响

提升角度(方法论分类)
主角度:off-CPU 回收效率(减少 I/O 与 swap 等待)——本补丁不减少某条指令,而是让回收(reclaim)选 victim 更准:冷文件页从最旧/第二旧代被优先回收,热工作集留在内存,直接结果是换入/换出/页读入大幅下降(见下方基准),对应的磁盘与 swap I/O 等待随之减少。
次角度:on-CPU 开销降低——换入换出减少 → kswapd/缺页/swap 处理的 CPU 时间下降(作者基准中 sys 时间 -9.4%);同时 folio_mark_accessed() 只置位/计数,比 MGLRU 下需要"从 LRU 摘下来再加回活跃代"的 folio_activate() 路径更轻(解读(AI 分析))。
按 Brendan Gregg 视角:延迟 = on-CPU 计算 + off-CPU 等待,这里主要砍的是 off-CPU 的 swap/I/O 等待,附带 on-CPU 的回收处理开销。
受益场景
复用"改动路径 → 高频触发场景":改动在缺页读入(folio_add_lru())与回收扫描(lru_gen_set_refs())两条路径,二者都只在"内存有压力、回收持续运行"时才会高频生效。因此收益前提是内存容量小于工作集(memcg 限额、容器、移动端等),且负载以文件页为主(构建、数据库缓存、文件密集服务)。无内存压力时补丁基本 no-op(解读(AI 分析))。
指标(x86 内核构建,memcg 1GB、20 线程)改进前 w/o patch改进后 w/ patch变化
real 墙钟时间1m50.764s1m48.879s-1.7%
sys 内核态 CPU 合计4m0.012s3m37.421s-9.4%
pswpin 换入页数1,333,245568,480-57.4%
pswpout 换出页数4,366,4432,322,657-46.8%
pgpgin 页读入6,962,5924,073,416-41.5%
pgpgout 页写出17,780,7129,613,408-45.9%
refault_file 文件 refault287,794262,505-8.8%
refault_anon 匿名 refault1,347,963577,550-57.2%

说明:数据来自 commit message(PATCH v3)作者自报,未独立验证。作者另附经典 active/inactive LRU 对照:real 1m49.928s、pswpin 463,452——说明补丁后 MGLRU 在该负载上与经典 LRU 表现相当(甚至略快),且 refault_file 更低(262,505 vs 562,555)。

内核构建回收/交换相关计数 before/after 对比
图 2:x86 内核构建(1GB memcg、20 线程)关键回收/交换计数对比(单位百万 M)
来源:作者自报数据(commit message)
v1 的 fio 模型演示(动机佐证)
作者在 v1 里用一个 fio 模型展示问题机理:memcg 400M 限额内以 mmap 方式按"每 64KB 读 4KB"的 strided 模式读 600M 文件(模拟"预读进来但没被访问"的页)。旧代码下观测到 12,883,855 次 file refault、带宽仅 58.5 MiB/s;新代码下 0 次 refault、带宽 5078 MiB/s。作者自报,未独立验证;该模型是"过度保护导致热页被挤出"的直观演示,正式基准仍以上方内核构建为准。

🔄 方案演进

本补丁从 2 月的 RFC 到 5 月底合入主线,经历了 4 个版本,核心变化由 Kairui Song 的 review 推动。演进脉络(各版链接均经 checking-references 验证):

RFC → v1 → v2 → v3 演进脉络
RFC(02-25):"mm/mglru: lazily activate folios while folios are really mapped"。思路是新增 folio_activate_on_mapped(),在 filemap_fault()/filemap_map_pages() 真正把页映射进页表时才激活(3 文件 +19/-1)。对 anon 保留 folio_set_active(),只对文件页做"映射时才激活"。
v1(04-18):收窄为 1 行改动(+1/-1)——直接把 folio_add_lru() 里的 folio_set_active(folio) 换成 folio_mark_accessed(folio)。附带 fio 模型演示与 arm64 "old PTE" 背景说明。
v2(05-25):扩到 4 文件(+15/-11)——补上 lru_gen_folio_seq() 代际公式、lru_gen_set_refs()、lru_gen_refault() 的配套调整。
v3(05-26):合入主线版(+25/-9)——按 Kairui review 修正两处(见下)。
review 如何推动版本演进(Kairui Song,v2 回复)
  • 质疑 1:v2 改了所有 anon folio 的行为——"you are changing the pattern for all anon folios here? But I think we are mostly working with file folios here…"(来源 message-id CAMgjq7CZt+EFoVqSti93QpKUNJ4VySxm5TAOgkMmpkS+73QyZA@mail.gmail.com)→ v3 在 lru_gen_folio_seq() 恢复 anon folio 原有分支(anon 仍放第二年轻代)。
  • 质疑 2:运算符优先级——Kairui 指出 v2 的 MAX_NR_GENS - folio_test_workingset(folio) || folio_test_referenced(folio) 优先级有歧义,问作者本意是 (MAX_NR_GENS - workingset) || referenced 还是 MAX_NR_GENS - (workingset || referenced) → v3 用括号写成后者。
  • 质疑 3:v2 的 lru_gen_set_refs() 用 folio_test_active() 是坏的——"PG_active is always unset for on LRU folios for MGLRU… So PG_workingset seems broken by this point, rmap walk no longer get PG_workingset set, which removes the 'promote on 2nd access' mechanism entirely?" → v3 改用 folio_lru_refs(folio) > 1(第二次访问才晋升)。
  • 方向认可 + 展望——Kairui 对整体方向明确赞同("Overall I agree with this directly that file folios shouldn't get over protected"),并建议未来逐步走向 refault-distance 激活(他此前实验在某数据库上获 500% 提升,LWN: Refault distance update with MGLRU support)与他的 MGLRU-FG 系列:"let's fix the low-hanging fruit first"。
  • 注:RFC/v1/v2/v3 各版链接均已通过 lore 索引验证(message-id 精确命中);Kairui 的 review 内容取自 lore 原文。

    ⚠️ 风险与局限

    收益成立的前提
    ① 必须启用 MGLRU(CONFIG_LRU_GEN,多数发行版默认开启);否则 folio_mark_accessed() 走经典 active/inactive 路径,本补丁的 4 处改动均被 lru_gen_enabled()/#ifdef CONFIG_LRU_GEN 门控,行为不变。
    ② 必须有内存压力:改动只影响"回收选 victim"这一步,无压力时是 no-op。
    ③ 负载以文件页为主:作者基准与 fio 模型都是文件页场景;纯 anon 工作集负载收益有限(解读(AI 分析))。
    生产落地影响(落地场景角度)
  • 行为语义变化:文件页从"缺页即激活"变成"冷起步 + 二次访问晋升",依赖旧"过度保护"的负载(例如一次读入、隔很久才复用一次的稀疏访问)可能 refault 上升。作者基准显示整体净收益,但个别负载需回归验证(解读(AI 分析))。
  • 定期 aging 下的再激活:Kairui 在 review 中提醒 "this would break reactivation if we age periodically. That is less of a problem compared to overprotection"——即周期性 aging 会清掉 PG_referenced/refs,被清后要重新累积计数才能晋升,热页可能在下次扫描前被误回收。作者接受此权衡(来源:Kairui review message-id CAMgjq7CZt+EFoVqSti93QpKUNJ4VySxm5TAOgkMmpkS+73QyZA@mail.gmail.com)。
  • 观测性:WORKINGSET_ACTIVATE 计数口径收紧(只在真实 workingset refault 时计数),依赖该计数做容量规划的监控可能看到数值下降——不是回归,是口径更准确。
  • 生态/兼容性
    纯内核 mm 改动,无用户态 ABI 变化;folio_mark_accessed()/folio_set_active() 均为既有 API,不改签名。对依赖 MGLRU 行为的第三方模块/调优(如显式 posix_fadvise(DONTNEED)、madvise 冷热提示)的影响很小——它们仍会走各自路径(解读(AI 分析))。
    review 质疑(均已回应/合入)
    Kairui Song 在 v2 的 3 条质疑(anon 行为、运算符优先级、lru_gen_set_refs() 的 PG_active 误判)全部在 v3 得到修复并合入;整体方向获其明确认可(CAMgjq7CZt+EFoVqSti93QpKUNJ4VySxm5TAOgkMmpkS+73QyZA@mail.gmail.com)。此外作者也 Cc 了多位 mm 相关维护者(Matthew Wilcox、Suren Baghdasaryan、Shakeel Butt 等)与 MGLRU 生态相关人(Yuanchu Xie、Wei Xu、Kalesh Singh 等);commit message 未出现 Yu Zhao 的 Ack/Reviewed tag。
    严重度:MINOR~MAJOR(落地关注)· 收益前提明确(MGLRU + 内存压力 + 文件页负载);主要风险是"定期 aging 下热页再激活延迟"与个别稀疏访问负载的 refault 上升,生产落地前建议在典型内存压力负载上做回归(解读(AI 分析))

    🔗 交叉引用

    📌 关联工作 / 相关补丁
    commit 4d5d14a01e2c ("mm/mglru: rework workingset protection") — 本补丁的直接前身:把 PG_active folio 从"最年轻代"改为"第二年轻代",但仍过度保护,本补丁在其基础上继续收紧。
    Lei Liu: mm/vmscan: Add readahead LRU to improve readahead file page reclamation efficiency — 被作者否定的替代方案(单独 readahead LRU),本补丁用更轻的手段达到同类目的。
    Kairui Song: mm/mglru: frequency guided workingset promotion (MGLRU-FG) — review 中提到的并行演进方向(频率引导的工作集提升,RFC 06/32),与本补丁的"二次访问晋升"理念一致。
    LWN: Refault distance update with MGLRU support — Kairui 引用的 refault-distance 再激活实验(某数据库 +500%),指向未来更精确的激活策略。
    RFC: mm/mglru: lazily activate folios while folios are really mapped — 本补丁的起点(02-25),思路从"映射时才激活"收敛到"referenced 冷起步"。
    ⚠️ 免责声明

    本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。