mm/mglru: frequency guided promotion (MGLRU-FG)

内存 · MGLRU(多代 LRU)页回收 · 访问频率引导的工作集提升

💡 一句话总结

在内存受限(memcg 限额 + 磁盘/ZRAM swap)的混合访问负载下,主线上游 MGLRU 对页面缓存只有"最近是否被引用"这一单比特热度信息:热顺序读(readahead 进入时 refs==0)被误判为冷页优先回收,冷随机扫描又反复污染缓存——作者复现 SQLite 热顺序+小热区场景从经典 LRU 的 14.51ms 恶化到主线上游 MGLRU 的 567.05ms(约 39 倍)。本系列(MGLRU-FG,腾讯 Kairui Song,RFC v2 共 15 补丁)把热度判断从"单比特引用"升级为统一的 0~7 访问计数(refs),按访问频率分档(tier),并在热度达标时把 folio 惰性提升到更年轻代际(gen climbing),让缓存保护按访问频率而非仅按最近访问来分配。作者自报(未独立验证):Chromium & Node.js 测试总请求数 +47.4%、MongoDB YCSB +10.3%、FIO zipf +11.1%、内核构建 refault_file -32%。

📋 补丁基本信息

项目内容
补丁类型优化(页面回收/工作集跟踪,性能)+ 重构(统一 LRU flag 语义、减少 MGLRU flag 占用)
状态In Review(RFC,未合入主线)
当前版本v2 · cover letter(15 补丁)
版本演进 v1(2026-05-01,32 补丁,"MGLRU-FG and refault distance support")→ v2(2026-08-03,15 补丁,砍掉 refault distance,聚焦 MGLRU-FG + flag cleanup)
版本标签目标 Linux 7.3(RFC 未合入主线,非已确定版本;tag 为 7.3 周期候选标注)
作者机构Kairui Song(腾讯 Tencent,kasong@tencent.com)
提交日期2026-08-03(v2 系列,实际 08-03/08-04 北京时间)
改动范围v2:17 文件,+689/-349(15 补丁);核心补丁 09/15 6 文件 +385/-226
核心函数folio_inc_lru_refs() / lru_tier_from_refs() / lru_refs_from_flags() / folio_update_gen() / folio_inc_gen() / lru_gen_refault()
原始链接lore Message-ID

版本说明:作者内部把 08-03 这版按新 change-id 标为 "V1",但从社区视角它是 05-01 32 补丁 RFC 之后的第二次迭代,本文按 v1→v2 记述。

📊 速览卡片

核心机制
按频晋升
优化目标
工作集保护
适用场景
受限内存+swap
特性等级
★★★★
实测提升
+47% 最高

特性等级依据:实测数据亮眼(Chromium/Node.js +47.4%、YCSB +10.3%,作者自报)且纯内核改动、不依赖特殊硬件、落地门槛低;但仍是 RFC 未合入,且 refault distance 未含入(对工作集剧变响应慢),tier 数固定 4 —— 收益需在内存压力场景才体现 → ★★★★

🎯 解决什么问题

系列整体定位(patchset/series)
本分析对象是 15 补丁的 RFC 系列(v2),整体解决一个系统性问题:MGLRU(多代 LRU,Linux 6.1 合入的页回收机制)对"工作集"(workingset)的热度判断过于粗糙,尤其对页面缓存(page cache)——导致在内存受限时缓存命中率低、swap/refault I/O 偏多、PSI 欠计。系列构成: ① 前置重构(补丁 1-8):memcontrol LRU 统计原子化、页 flag 访问器封装、把 refault 激活逻辑收进 lru_gen_refault(); ② 主体(补丁 9/15)"frequency guided workingset promotion (MGLRU-FG)":实现按访问频率的计数/分档/代际提升; ③ 收尾(补丁 10-15):把 refs 计数做成通用 API 并接入 smaps、huge_memory、khugepaged、madvise 等。 系列还顺带统一了经典 LRU 与 MGLRU 的 flag 语义,把 MGLRU 的 flag 占用减少一位。
背景 / 原始动机
作者在 LSF/MM/BPF 2026 提出该方向(v2 cover letter 链接 [1]),并在 cover letter 中点名一个上游近况:主线上游 MGLRU 一个近期改动——lru_gen_folio_seq() 把 refs==1 的新 folio 抬到"第二旧代"——意外让随机读更热、顺序读更冷:顺序读依赖 readahead,而 readahead folio 以 refs==0 进入最旧代,folio_mark_accessed() 对代际几乎无效;随机直读 folio 则从 refs==1 起步,天然被保护。作者认为这不是好的解:它依赖 LRU drain 时机,不可靠,且会伤害"顺序部分实际更热"的负载(如一个热的大文件 + 大量冷小文件)。MGLRU-FG 的目标是在不依赖这个"初始 bump"的前提下覆盖普通工作负载,且不伤害 scan-get 型负载。
系统层面:MGLRU 对页面缓存热度的"单比特"局限
MGLRU 把页按"代际(generation)"组织,新访问的页进入更年轻代,回收从最旧代开始。但它对"同一代内谁更热"的刻画很弱:文件页只靠 PG_referenced 一个比特("最近是否被引用过")+ PG_workingset 标记,无法区分"访问 1 次"和"访问 7 次"。其后果: ① 热顺序读被误判——readahead 批量读入的页 refs==0,即使反复命中也被当作冷页; ② 冷随机扫描污染缓存——一次性读入的冷页与真正热的页挤在同一代; ③ PSI / smaps / readahead 精度差——workingset 判断不准确导致内存压力统计欠计; ④ flag 占用——MGLRU 在 folio flags 上用了 PG_referenced + PG_workingset + LRU_REFS_MASK 三块,压缩空间有限。
场景层面:内存受限容器 / 带 swap 的混合访问负载
典型触发场景是工作集超过可用内存、回收压力持续存在的部署:
  • memcg 限额的编译/批处理容器(如 3G 限额下 make -j48 + 磁盘 swap)——内核构建产生大量临时页缓存,缓存热度判断直接决定换入换出量;
  • 数据库(MongoDB/YCSB,16G memcg)——B-tree 遍历既有顺序扫描又有随机点查,两种访问模式的热度特性完全不同;
  • 桌面/浏览器 + Node.js 混合负载(ZRAM swap,48c96t 128G)——大量短生命周期页 + 少量热页;
  • SQLite 热顺序 + grep 冷扫描——前者验证"热顺序部分能否被保护",后者验证"一次性冷页能否被快速回收不污染缓存"。
这些场景的共同点:缓存命中率 = 换页 I/O 数量。系统缺陷(单比特热度)在这里被放大成"热页被误回收 → 反复 refault/换入"的额外 I/O。
受影响负载:内存受限容器、数据库、浏览器+Node.js、编译 · 为什么此特性解决此场景:把"是否热"从单比特升级为"多频繁"的多级判断,让热页留在缓存、冷页快速让位

🧩 核心机制

MGLRU-FG 的核心思想一句话:把"热度"从单比特(referenced?)换成 0~7 的访问计数(refs),按计数分档保护,并在计数达到上限时把 folio 惰性搬到更年轻的代际。这样页面缓存的热部分会按"访问频率"自动向上爬,冷部分快速沉到最旧代被回收。

从系统层面看
改动横跨 include/linux/mm_inline.h(refs 编解码)、include/linux/mmzone.h(tier 模型与常量)、mm/swap.c(访问路径 folio_inc_lru_refs)、mm/vmscan.c(回收/晋升路径 folio_update_gen / folio_inc_gen / sort_folio / get_tier_idx)、mm/workingset.c(refault 恢复 refs)。四个核心逻辑点:
  1. 统一 refs 计数:lru_refs_from_flags() 现在把 PG_referenced(bit0)、PG_workingset(bit1)、LRU_REFS_MASK(高位移 2)一起拼成一个 0~7 的计数——不再把 workingset 当成一个独立的"最后一档"标记。
  2. 按频分档:lru_tier_from_refs() 从 order_base_2(refs) 改成 fls(refs-1),访问 2 次即进入 workingset 档(tier1),访问 3+ 次进入更高 tier,形成"越热越靠后回收"的层次。
  3. 惰性代际提升(gen climbing):访问路径在 refs 达到 LRU_REFS_MAX 时,通过"清 PG_lru 隔离 + RCU 读锁 + lru_gen_update_size()"把 folio 抬到 gen+1,全程不拿 LRU 锁,只做一次原子 cmpxchg——这是"频率引导提升"的名字由来,也是性能关键。
  4. refault 恢复 refs:eviction 时把 refs 编码进 shadow(pack_shadow),refault 时经 lru_gen_refault() 还原 refs 并参与 tier 统计,让被换出的热页回来时能"接续"其热度而非从 0 开始。
为什么能支撑场景提升:热顺序读在 mainline MGLRU 里因为 refs==0 永远待在最旧代;本系列里 folio_mark_accessed() 会真实递增 refs,几次访问后 folio 就"爬"到更年轻代并被 tier 保护——缓存里留下的是真热页,换入换出和 refault 随之下降。
MGLRU-FG 机制示意图:访问计数→分档→惰性代际提升
图 1:MGLRU-FG 机制——按访问次数分档(tier)+ 惰性代际提升(gen climbing),以及 Before/After 对页面缓存热度判定的差异
来源:基于 lore 真实补丁 diff(v1 06/32 / v2 09/15)绘制
步骤关键代码目的
访问计数folio_inc_lru_refs():refs +1(原子 cmpxchg)把"最近访问过"升级为"被访问了几次"
分档lru_tier_from_refs() = fls(refs-1)2 次即进 workingset 档,越热 tier 越高越难回收
惰性晋升refs 达 LRU_REFS_MAX → 清 PG_lru + RCU → gen+1热页按频率搬到更年轻代,无需 LRU 锁
refault 还原lru_gen_refault():shadow 存/还原 refs换出的热页回来时热度不归零
关键代码片段 1:统一 refs 计数 + 按频分档
static inline int lru_tier_from_refs(unsigned int refs)
 {
-	VM_WARN_ON_ONCE(refs > BIT(LRU_REFS_WIDTH));
-	/* see the comment on MAX_NR_TIERS */
-	return workingset ? MAX_NR_TIERS - 1 : order_base_2(refs);
+	VM_WARN_ON_ONCE(refs > LRU_REFS_MAX);
+	if (refs < LRU_REFS_WORKINGSET)
+		return 0;
+	return fls(refs - 1);
 }

 static inline int lru_refs_from_flags(unsigned long flags)
 {
-	if (!(flags & BIT(PG_referenced)))
-		return 0;
+	int refs;
 	/* Return the total number of accesses. Also see the comment on
 	 * LRU_REFS_FLAGS. */
-	return ((flags & LRU_REFS_MASK) >> LRU_REFS_PGOFF) + 1;
+	refs = (flags & BIT(PG_referenced)) ? BIT(0) : 0;
+	refs += (flags & BIT(PG_workingset)) ? BIT(1) : 0;
+	refs += ((flags & LRU_REFS_MASK) >> LRU_REFS_PGOFF) << 2;
+	return refs;
 }

▲ 这段是机制的地基:把 PG_referenced / PG_workingset / LRU_REFS_MASK 三个分散标记统一编成一个 0~7 的访问计数。旧代码里 workingset 是独立的"最后一档"布尔;新代码里它只是计数的 bit1——于是"访问 2 次"就能进 workingset 档,而"访问 7 次"的页与"访问 2 次"的页在热度上被明确拉开。这是"按频率而非按是否引用"的分水岭。

关键代码片段 2:惰性代际提升(gen climbing)
/* To promote frequently used folios, prevent isolation
 * first, it's a lazy promotion so no LRU lock needed. */
if (!isolated) {
	if (!folio_test_clear_lru(folio))
		goto out;
	isolated = true;
	...
	max_seq = READ_ONCE(lrugen->max_seq);
	max_gen = lru_gen_from_seq(max_seq);
	if (gen == max_gen)
		goto out;   /* 已在最年轻代,无需再提 */
	if (refs <= LRU_REFS_MAX) {
		type = folio_is_file_lru(folio);
		min_gen = lru_gen_from_seq(READ_ONCE(lrugen->min_seq[type]));
		if (gen != min_gen)
			goto out;   /* 只从最旧代提升,防过度保护缓存 */
	} else {
		refs = LRU_REFS_PROTECTED;
	}
	new_gen = (gen + 1UL) % MAX_NR_GENS;
	lru_gen_set_flags(&new_flags, new_gen);
...
out:
	lru_refs_set_flags(&new_flags, min(refs, LRU_REFS_MAX));

▲ 这是"频率引导"的执行点:访问路径在计数达标时把 folio 从最旧代抬到下一代会(gen+1),全程用 folio_test_clear_lru() 短暂隔离 + RCU 读锁,不拿 LRU 锁,所以热路径只有一次原子 cmpxchg。两个 guard 很关键:已在最年轻代(gen == max_gen)不再提;只提升"最旧代"的 folio(gen != min_gen 就跳过),避免把缓存过度保护导致回收滞后。

📈 性能影响

提升角度(方法论分类)
主类是 off-CPU I/O 等待:改动减少的是 swap 换入换出、refault、page-in(作者数据中 pgpgin -15%、pswpin -14%、pswpout -11%、refault_file -32% 全面下降)——即"少等磁盘/ZRAM I/O"。次类是内存层次:缓存命中率提升,热页留驻、冷页让位。这不是减少 CPU 指令数的 on-CPU 优化,而是减少换页等待的 off-CPU 收益。作者也指出统一 flag 语义改善了 PSI / smaps / readahead 的准确性(可观测性收益,非吞吐)。
受益场景
内存受限 + 混合访问模式的场景受益最大:memcg 限额容器、带 swap 的数据库、浏览器+Node.js 桌面负载。机制路径:folio_mark_accessed()(读页缓存路径)→ refs 递增 → 热页 gen climbing → 回收时热页被 tier 保护 → refault/swap 减少。硬件上,磁盘 swap(NVMe/HDD)比 ZRAM 放大收益(I/O 成本越高收益越大)。
场景/用例运行环境主线上游 MGLRU(Before)MGLRU-FG(After)
Chromium & Node.jsZRAM swap,48c96t,128G,64 worker,1h153029 总请求225664 总请求(+47.4%)
MongoDB YCSB workloadb16G memcg,48 线程,3 次取均86421.44 ops/s95378.50 ops/s(+10.3%)
FIO zipf 0.9NVMe,16G cgroup,40G 文件,16 jobs2350.40 MB/s2611.37 MB/s(+11.1%)
内核构建 make -j483G memcg + 磁盘 swap,16 次不同 swappiness 取均real 2m56s,sys 11m06s,refault_file 414kreal 2m49s(-7s),sys 10m39s(-27s),refault_file 280k(-32%)
LevelDB Scan/Getcache_ext 论文 benchmark5026.9 ops/s5029.7 ops/s(主线上游已优于 CLRU,本系列不倒退)
SQLite 热顺序 + 小热区作者复现脚本567.05ms(CLRU 仅 14.51ms)10.58ms(恢复并优于 CLRU)
grep 冷扫描(>RAM)作者复现脚本13694.47ms12930.43ms(无退化)

数据来源:v2 cover letter(2026-08-03)作者 Kairui Song 自报,未独立验证。作者同时说明:一个近期主线上游改动让 Chromium 测试基线"比几个月前快了很多",与本系列无关,但意味着该场景绝对数字可能偏乐观;其他测试(MySQL 等)"looking fine, no regression"。本系列未提供 off-CPU/on-CPU 的细分延迟数据,性能归因(减少 I/O 等待)为解读(AI 分析),基于作者自报的 swap/refault 计数器下降推断。

MGLRU-FG 相对主线上游 MGLRU 的性能变化
图 2:作者自报(未独立验证)的各场景相对主线上游 MGLRU 的变化
来源:v2 cover letter 数据绘制

🔄 方案演进

本系列从最初提出到当前版本(基于 lore 各版本 cover letter 真实信息):

v1 → v2 演进脉络
v1(2026-05-01,32 补丁,"MGLRU-FG and refault distance support"):分三部分——① MGLRU 频率引导 + flag 格式重构(核心补丁 06/32,+248/-155);② 把所有 workingset/referenced flag 用户转换到新通用 LRU refs API;③ refault distance 支持(从 22/32 mm/workingset: simplify and use a more intuitive model 开始)。作者自报 v1 数据:LevelDB GET/SCAN +41%(993.64→1344.72 ops/s)、MongoDB YCSB +16.8%(82127.65→95933.99)。但 refault distance 部分"开销略高于预期(每次 refault 多几十个原子操作 + 一次额外 rstat flush)",作者仍在优化。
v2(2026-08-03,15 补丁,"MGLRU-FG and flag cleanup"):砍掉 refault distance(cover letter 明说 "Refault distance is not included yet, can be added later"),聚焦 MGLRU-FG + flag 清理。核心补丁从 v1 06/32(+248/-155)扩展到 v2 09/15(+385/-226,6 文件),因为部分转换工作被合并进来。benchmark 更全面(新增 Chromium/Node、FIO zipf、SQLite/grep、LevelDB 复测),作者自述 "very usable, stable, and performing well"。
设计权衡 / 讨论推进
  • refault distance 被暂缓:v1 含、v2 不含。作者说明缺失的代价是"MGLRU 对工作集剧变响应较慢",后续可再加(链接到 LWN 945266 与 v1 系列)。这是 v1→v2 最大的范围收敛,让系列更小、更易 review。
  • 与 workingset reporting 的兼容性:作者主动解释 "gen climbing" 设计看似与 workingset reporting(把代际当作访问间隔标识)冲突,实则不冲突——把 generation 数扩到 64/128 后,refs 驱动的提升停在较旧代(如 oldest+16),剩余新代仍可作为纯时间间隔的 bin(LWN 976985)。
  • tier 数固定:MAX_NR_TIERS=4,作者称若未来 tier 超过 4 需另找调节方式,"that shouldn't be hard"。
  • v1 cover letter 还提到 RFC 质量提示:几个 helper(如 folio_mark_referenced)"may lead to inaccurate hotness info, only in theory",可后续改进。
社区参与:v1 线程可见 Joanne Koong(Google)参与(消息 2026-07-22 / 2026-07-24,主题引用 "MGLRU-FG and refault distance support"),说明社区已有关注;v2 发出后有 syzbot CI 自动回复(08-04)。具体 review 观点正文未能从本地 lore 索引完整提取(MCP 工具对该类消息路由异常),如实标注。

💬 讨论焦点

syzbot CI 自动测试(v2,2026-08-04)
syzbot ci(Message-ID: 6a7177f9.d35e88fd.de8b.000b.GAE@google.com) 在系列发出后自动回复,主题 "Re: mm/mglru: frequency guided promotion (MGLRU-FG) and flag cleanup"。这是内核补丁的常规 CI 反馈(编译器/启动测试),未见人工维护者的公开质疑。该回复的具体正文未能从本地索引提取,本文仅确认其存在与时间。
社区关注(v1 线程,2026-07)
v1 系列线程中可检索到 Joanne Koong(joannelkoong@gmail.com,Google)的两条消息(2026-07-22 与 2026-07-24),主题均引用 "MGLRU-FG and refault distance support"。Joanne 是 Google 的页面缓存/回收方向贡献者,她的参与说明该方案在 MM 社区已有关注。这两条消息的正文同样未能从本地索引完整提取(MCP 路由异常),因此本文不转述其具体观点,仅标注"存在参与"。
作者自述的开放问题
  • refault distance 未含入 → MGLRU 对工作集剧变响应慢(v2 cover letter 自述,对应 LWN 945266);
  • 一个近期 mainline 改动疑似破坏 MGLRU 公平性,作者称"与本系列无关,但会让 Chromium 测试绝对读数更好,我稍后深查"(v2 cover letter NOTE);
  • tier 数固定 4,超限需另设计(v2 cover letter)。

⚠️ 风险与局限

收益成立的前提
收益前提是存在内存压力(memcg 限额、swap 或 ZRAM 使工作集放不下);内存充裕时基本 no-op(没有回收压力,热度分层无意义)。依赖 CONFIG_LRU_GEN(MGLRU 开启)。作者数据主要来自磁盘/ZRAM swap 场景,收益大小随换页 I/O 成本缩放。若工作集本身能放进内存,本系列不提供额外收益。
生产落地影响
  • RFC 未合入:当前只到 v2 RFC,无 sysctl/开关约定,任何生产采用都需自行承担实验风险;
  • refault distance 缺失:工作集发生剧烈迁移(如切换热点数据库表)时,MGLRU 响应慢,可能短期命中率下降——作者明确列为待加项;
  • flag 语义统一会影响多个子系统:smaps 的 workingset 报告、khugepaged 的"是否 referenced"判断、madvise 的 MADV_REFERENCED 语义、readahead 都随之变化,升级后这些用户态/内核组件的行为会变(本系列即为此接入);
  • 可观测性变化:PSI 的 workingset 判定更准 → PSI 读数会变化(作者称之为修正欠计,但部署方需知 PSI 基线会移动)。
生态/兼容性
refs 编码从 LRU_REFS_WIDTH 扩到 LRU_REFS_WIDTH + 2(借用 PG_workingset/PG_referenced 两位),在 page flags 极其紧张的配置下 tier>2 不可用(作者注释:无 tier>2 时仍能工作,但文件缓存会被更激进地提升)。与未来 workingset reporting 的兼容依赖 generation 数扩展(作者已给方案)。本系列未触碰用户态 ABI。
review 质疑(若有)
截至本报告日期,公开可检索到的"人工 review 质疑"有限:v1 线程有 Joanne Koong(Google)参与但正文未能提取;v2 只有 syzbot CI 自动回复。作者自述的已知局限(refault distance 缺失、tier 数固定、foliomark_referenced 精度理论问题、MGLRU 公平性疑点)已在上述小节如实列出。无公开 Ack/Reviewed-by 记录。
严重度:MINOR(无已知崩溃/泄漏;风险集中于性能次优、未合入、PSI/flag 语义变化的部署影响)· 落地场景:生产采用前需等正式版(非 RFC)+ 确认 refault distance 是否加入;升级时留意 PSI 读数与 smaps/khugepaged 行为变化

🔗 交叉引用

📌 关联工作 / 系列其他补丁
v2 cover letter(RFC 00/15) — 本系列 15 补丁清单、作者自报性能数据
v1 cover letter(RFC 00/32) — 初版方案(含 refault distance)+ 三部分拆解
v2 核心补丁 09/15(MGLRU-FG) — 主体实现,6 文件 +385/-226
v1 核心补丁 06/32(MGLRU-FG) — 本文 diff 证据来源,+248/-155
v1 补丁 30/32(refault-distance based re-activation) — v2 暂缓的 refault distance 实现
作者另一系列:mm/mglru: improve reclaim loop and dirty folio handling(v7) — v1 cover 依赖的前置系列
LSF/MM/BPF 2026 议题 — 方案起源(作者 cover [1])
MGLRU 原始 RFC(Yu Zhao, 2022) — 被改进机制的出处
LWN: Refault distance update with MGLRU support — 作者引用 [7],后续待加功能背景
LWN: mm: workingset reporting — 作者引用 [9],未来兼容方向
cache_ext(SOSP 2025) — LevelDB Scan/Get benchmark 来源(作者引用 [5])
YCSB workloadb — MongoDB 测试定义(作者引用 [3])
SQLite/grep 复现脚本 — 热顺序/冷随机复现(作者引用 [6])
相关日报:mm/mglru: use folio_mark_accessed to replace folio_set_active — 本系列依赖的"访问标记"路径(2026-08-05)
Huang Ying: mm: workingset reporting(PATCH v1 0/7, 2024) — workingset reporting 原始系列
⚠️ 免责声明

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