mglru 到底替换了什么
多代 LRU(mglru,内核源码里也叫 lru_gen)是 Linux 6.1 合入的替换式页面回收算法(commit c174c2cd0441,2022 年底,Yu Zhao 的 12 patch 系列)。它跟经典 active/inactive LRU 并存在 mm/vmscan.c 里,启动时通过 CONFIG_LRU_GEN_ENABLED 切换。
基本判断:经典 LRU 给每个 zone 维护 active + inactive 两套列表,通过在两者之间来回挪页面来探测冷页。这个算法有些广为人知的痛点——inactive 列表被污染时的假阴性、rescale 代价大、单次大读操作会搞乱 hot/cold 区分。mglru 丢掉双列表模型,换成按时分桶的代际数组,配上页表扫描的热点检测。
两种实现共用页缓存和 swap-out 路径,只在回收策略上不同。所以运行时切 mglru 是安全的——你换的不是内存记账模型,只是淘汰启发。
到 2026 年初 mglru 已经在 mainline 上跑了三年多。但是: 周围的氛围变了。2026 年 LSFMM+BPF Summit 上,Matthew Wilcox 当面说算法的维护者「已经转去做别的项目」,号召从 mainline 里把整个东西挖出来。他的论据是补丁通过了维护者名单上人从没审过的那些、mm/ 里自从添加三个维护者之后半年内没人提过任何 commit、下游厂商(Honor、Android)积攒了 out-of-tree patch 强迫 mglru 符合他们的需求。这种紧张关系是本文会花同样篇幅讲「什么 work、什么不 work」的根本原因。
代际模型——按新近度分桶

mglru 把匿名页和文件 backed 页排进四个代(编译时常量 MAX_NR_GENS)。每个代是一个时间窗口。最新的代(最高的 gen)放最近碰过的页;老的代放不再使用的页。
Gen N : 几秒内被访问的页
Gen N-1 : 几十秒前访问的页
Gen N-2 : 闲置约几分钟的页
Gen N-3 : 最老,「首选淘汰候选」—— 需要内存就先从这开始挑内核要内存时,从最老的非空代往新方向走,丢干净页、写出脏页。晋升(Gen N-1 里被访问的页移到 Gen N)由两个访问检测器完成:
- 页表扫描——内核走若干进程的页表,看 PTE.Accessed 位,置位了就晋升。
- 对已隔离页的 rmap 扫描——对已经在 LRU 上扫描到的页,过它的 rmap 看有没有引用它的 PTE 置了 Accessed。
晋升不是物理把页在列表之间挪。它把代际号存在页的 folio->flags 位里(后面单独讲),更新每代计数器(nr_pages[gen][type][zone])反映新分桶。
每节点的热度状态在 struct lru_gen_folio:
/* mm/vmscan.c */
struct lru_gen_folio {
unsigned long max_seq; // 最新代号
unsigned long min_seq[ANON_AND_FILE]; // 每类型最老的非空代号
long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
unsigned long *filters[NR_BLOOM_FILTERS]; // refault 检测用的 Bloom
atomic_long_t nr_evicted[ANON_AND_FILE];
atomic_long_t nr_refaulted[ANON_AND_FILE];
};max_seq 每次内核问「什么是新的?」就被 lru_gen_rotate() 加 1(每个 kswapd 周期、每个 try_to_free_pages)。四个代相对 max_seq 追踪,所以老的代漂到 max_seq − 1、max_seq − 2 这样。当 max_seq − min_seq > MAX_NR_GENS,最老的就被淘汰出账本。
页表扫描那一招——为什么 x86 硅片很重要
mglru 最出名的胜利是不需要扫 rmap 也能找到最近碰过的页。取而代之,每个老化周期它对一部分进程的地址空间做受控的页表扫描。扫描器迭代 PTE,看 Accessed 位(x86 上是 bit 5),置位就晋升。
但有个问题:扫描器读完会把 Accessed 位写回 0。x86 上硬件用 CLFLUSH/CLFLUSHOPT 处理非叶项,或者特定指令序列完成。内核选择批量清正是 kill-switch 标志位 0x0002(leaf)和 0x0004(非 leaf)控制的:
| 值 | 效果 | 取舍 |
|---|---|---|
0x0001 |
主开关 | 整个算法开/关 |
0x0002 |
批量清 leaf PTE Accessed 位 | 优化 TLB 工作,可能增加 mmap_lock 竞争 |
0x0004 |
批量清非 leaf PTE Accessed 位 | 只在 Intel/AMD x86 验证过;其它平台不明确 |
生产一般开 0x0007(三个都开)。关掉批量清让走原子性回到位,但也丢失了 mglru 比 active/inactive 快的那点优化。
内核在 walk_pte_range() → lru_gen_pte_entry() 里对每个 PTE 做这件事。走的目标是每 memcg 的优先级列表(memcg_lru_gen_prio);见 Documentation/admin-guide/mm/multigen_lru.rst 的具体启发。
匿名 vs. 文件——平衡难题
这是mglru 被报告最多的问题,2026 LSFMM 上 Honor/Android 工程师重复说。
经典 LRU 的 swappiness sysctl 直接偏置「交换出去匿名页 vs. 文件缓存」的激进程度。mglru 想在内部处理同样的偏置——但经验发现:
匿名页倾向于聚在最年轻的两代(max_seq 和 max_seq-1)。文件 backed 页漂到老的代。结果:匿名页基本不被回收,文件页被激进出。
swappiness 没法彻底修。Honor 在生产 Android 内核 fork 里的应对是操纵内存 cgroup 的 memory.high 强行在后台应用上做匿名回收。out-of-tree 的 hack。
到 6.10,这个在 mainline 里没解决。如果你的工作负载匿名页为主(JVM 在 RAM 里扎驻、大型 Redis 风格存储、内存数据库),mglru 在 cgroup 层调之前会泄漏内存。
页旗标的预算——稀缺池里挤三个
mglru 用 folio->flags 里的三个位来追踪页代:
LRU_GEN_BIT = PG_reclaim(经典 LRU 已经用的回收旗标位)LRU_USAGE_BIT = PG_young- 第三个跟经典 LRU 的 workingset 追踪共用
folio->flags 里的旗标在内核里是硬通货。每个子系统都想要更多。LSFMM 2026 上提出的反对就是这个设计没法扩展:你想要更多代(比如追踪更长历史),每多一代至少多一个旗标。SUSE 的 Kairui Song 提议把代际追踪挪到 folio 的 rmap 元数据里,腾出三个旗标、解锁 63 代。到 2026 年初那一系列 patch 还没合入。
跟内存 cgroup 怎么交互
Linux memory cgroup v2:内核真的执行这个预算那篇讲过,每个 memcg 有自己独立的 LRU。mglru 保留这个模型。每个 memcg 自己一个 lru_gen_folio,自己一个 max_seq。cgroup v2 的回收路径走 memcg 的 LRU 通过 mglru 的 lru_gen_scan(),遵守每 cgroup 的压力信号。
但这里有生产里咬人的交互:mglru 倾向对 cgroup 过度回收。全局内存压力触发节点级扫描时,算法「从最老的代开始走」的逻辑会把不在预算边缘的 memcg 的干净页也甩出去。表现:memory.current < memory.high 的 memcg 在回收里丢页,工作负载内部出现抖晃。
LSFMM 上讨论的修法涉及 memcg_vmpressure 和 mglru 的每 memcg min_seq 的更好协调。6.10 还没合到 mainline。
实战兜底:在生产工作负载上把 mglru 和显式的 memory.high 搭配,让 cgroup 软上限当护栏。设 memory.high = memory.max * 0.8,让内核在 high 上节流、不要让全局回收扫过去。
跟 PSI 怎么交互
Linux PSI 内存压力:到底在测什么那篇已经讲过会计模型。mglru 会正确触发 PSI memstall 事件——psi_memstall_enter 是从同样的路径调用的,不管是 active/inactive 还是 mglru 在做回收。
有趣的交互点是 min_ttl_ms:
# 默认:抖晃防护关
echo 0 > /sys/kernel/mm/lru_gen/min_ttl_ms
# 保护最后 1 秒的工作集不被淘汰
echo 1000 > /sys/kernel/mm/lru_gen/min_ttl_msmin_ttl_ms 告诉 mglru:「最后 N 毫秒内访问过的页不要淘汰」。当内核需要的内存比工作集边界更紧迫,OOM killer 触发。整个特性瞄的是没 oomd 的笔电/桌面,卡顿比 OOM kill 更难受。
经验法则:min_ttl_ms=1000 应对大多数交互式工作负载可以;3000 是你愿意接受激进杀进程的折衷。服务侧没人眼等着的话别开——你希望内核不等就拿冷匿名页。
运行时旋钮(完整稳定 ABI)

所有稳定 ABI 在 /sys/kernel/mm/lru_gen/ 下:
ls /sys/kernel/mm/lru_gen/
# enabled min_ttl_ms# 看 mglru 是否开着(内核留着 active/inactive 代码备用)
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 (mglru 开、leaf-清开、非 leaf-清开)
# 干净关——回退到经典 active/inactive
echo 0x0000 > /sys/kernel/mm/lru_gen/enabled
# 细粒度开关
echo 0x0001 > /sys/kernel/mm/lru_gen/enabled # mglru 但不开批量清
echo 0x0005 > /sys/kernel/mm/lru_gen/enabled # mglru + 仅非 leaf-清(关 leaf)注意 0x0005 有意思——它只关 leaf-PTE 清,留着非 leaf 清。工作负载里 mmap_lock 抢得凶的(短命 mmap 多,JVM fork、浏览器引擎这种)把 leaf-清关掉能省真时间,因为 leaf-清路径在某些调度路径上要拿 mmap_lock 读锁。
debugfs 下的实验性特性
/sys/kernel/debug/lru_gen 暴露高级能力。格式是一行一条命令,多次写入可用,, 或 ; 分隔。两个实际用法是工作集估计和主动回收——都瞄数据中心调度器。
工作集估计
不带命令去读,返回每 memcg / 每节点上按时间窗分布的访问直方图:
memcg 0 /
node 0
min_gen_nr 7546 982 4096
min_gen_nr 7545 652 3096
...
max_gen_nr 7548 128 256每个 bin 记录窗口(age_in_ms)内访问过的页数,bin 是非累积的。MAX_NR_GENS 决定 bin 数。
实际用:K8s 调度器估计每节点冷页大小。如果 server-A 在下一轮有 4 GiB 工作集,新任务要 8 GiB,这个节点不合适。Bin-packing 可以直接用它。
往直方图里塞新一代:
echo "+ <memcg_id> <node_id> <max_gen_nr+1>" > /sys/kernel/debug/lru_gen主动回收(只要冷页,不需要全局压力)
echo "- <memcg_id> <node_id> <min_gen_nr>" > /sys/kernel/debug/lru_gen这条命令淘汰 <= min_gen_nr 的代里的页,不等全局压力。调度器上新任务、想在新任务落下前把冷页清出来时有用。
约束:min_gen_nr 必须 < max_gen_nr - 1,因为 max_gen_nr 和 max_gen_nr-1 是「active list 等价物」、没老熟。这俩被淘汰是 bug。
可选参数:
echo "- <memcg_id> <node_id> <min_gen_nr> [<swappiness>] [<nr_to_reclaim>]" \
> /sys/kernel/debug/lru_genswappiness:0–200,200 表示纯匿名页回收。nr_to_reclaim: 收够这么多页就停。
什么时候 mglru 帮上忙——什么时候帮倒忙
确实有一批 mglru 实实在在赢的工作负载:
- ChromeOS / Android 设备:Google 多次发证言。5.15 时代的 ChromeOS 配 8 GiB 内存+zswap,SWAP-风暴频次可测量地下降。POWER10 上的 MongoDB,YCSB 类 benchmark +~19% 吞吐。fio 随访问分布 +5–40% 不等。
- 数据库服务器: 工作集高随机局部性的 KV(memcached、redis)。
- 混合工作负载: 多个工作集互不交集的小工作负载同时跑,mglru 按时间记账的新近度能帮隔开。
也确实有一批 mglru 实实在在伤的工作负载:
- 匿名页为主的作业 指望 swap-out 把 RSS 释放掉。mglru 即便
swappiness=200也不彻底回收匿名页。Honor 的应对是用 memcg 硬逼。 - cgroup 重的容器: 从不在预算边缘的 cgroup 过度回收。
- 低内存设备: 可回收的少,页表扫描成本摊不开。经典 LRU 的「扫 inactive list」在没东西可找时更便宜。
- readahead 多的工作负载: readahead 页进了最年轻那一代但不一定用,把会用的页挤走了。修法在飞(让页表扫描跳过 readahead 页),还没合。
2026 年的 reconsider 争论

2026 年 5 月 LSFMM+BPF Summit 现场(Jonathan Corbet 2026 年 3 月 LWN 报道):
- Matthew Wilcox(Intel) 公开喊移除:「撕掉它。」他的论点主要不是算法本身,是维护质量。commit
44958000bada加了三个MAINTAINERS条目之后,半年里这三个人的 commit 在mm/里一个都没有。原作者 Yu Zhao 已经转去做别的。 - Yu Zhao 在 6.14 上提过几个 commit,之后没有了。
- 厂商已经积了补丁——Honor 的匿名页回收 hack、Google Android 给前台任务豁免 reclaim 的 hook 都在 out-of-tree。
- Kairui Song(SUSE) 提了个重新设计,把代际追踪从
folio->flags移出来,腾出三个旗标。这能解锁 63 代、解决旗标稀缺论点,也能改善历史追踪帮匿名/文件平衡问题。 - Kalesh Singh(Google) 点了兼容性问题——Android 的 user-space OOM daemon 假设 mglru 发的指标,复杂的 user-space 故事。
两个并存实现(mglru vs 经典 active/inactive)都在出货、都编译、都可运行时切换。现状是:mglru 进 mainline、kconfig 默认关、能开、问题没全解、正在争论要不要移除。
不要直接看「6.1 合入」那段加粗就以为它健康。看 lkml 讨论线程看看你那个版本的真实状态。
怎么验证内核在用 mglru
# 启动期打开 config
cat /proc/config.gz | gunzip | grep CONFIG_LRU_GEN
# CONFIG_LRU_GEN=y
# CONFIG_LRU_GEN_ENABLED=y (这是启动期 mglru 开/关的默认)
# CONFIG_LRU_GEN_STATS=n (可选:留历史统计)
# 运行时检查
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 表示 mglru 开且在做批量清
# 健全检查:内核暴露运行时 ABI 没?
ls /sys/kernel/mm/lru_gen/enabled
# 没这文件: mglru 整体没编进来、或者内核是 6.1 之前的注意 CONFIG_LRU_GEN=y 只是让代码编译,大多数发行版内核运行时默认是 mglru 关。Ubuntu 24.04 LTS、RHEL 9.x、大多数云厂商内核出 CONFIG_LRU_GEN_ENABLED=n。ChromeOS 内核和 Google 下游 Android fork 把它打开。Debian experimental 内核开了。Google 生产服务在跑。
如果你用的是发行版原厂内核,大概率在跑经典 active/inactive LRU。要开,得自己重编,打开 CONFIG_LRU_GEN_ENABLED=y(或者不开机靠 kpatch 设——但 mainline 里还没这能力)。
代码路径导读
看内核源码:
mm/vmscan.c主体。看lru_gen_*前缀的函数。mm/mmu_gather.c(或mm/rmap.c, 看内核版本)处理 rmap 扫描。include/linux/mm_inline.h有lru_gen_folio_idx()辅助函数和代际旗标位的定义。Documentation/admin-guide/mm/multigen_lru.rst上游文档——精简但权威。make htmldocs本地渲染。
一个内核调试技巧:开 CONFIG_LRU_GEN_STATS=y 拿 /sys/kernel/debug/lru_gen_full,留被淘汰代的全部历史,你能重构某个时间窗里算法看到什么。这个接口目前是实验性的、ABI 不稳定——但调优时它给你真实值。
生产配方
普通的云 Linux 主机跑容器化负载:
- 保留
CONFIG_LRU_GEN_ENABLED=n在你的默认内核上,除非有理由。 - 要打开(比如你在追尾延迟或者 MongoDB 风格的随机读收益),专门监控:
- 匿名页为主的 pod 的内存增长。看
memory.current和你 cgroup 的 anon/file 比例(memory.stat)。 memory.events.high在没撞预算的 cgroup 上跳——过度回收的迹象。- 系统级别的 PSI memstall(不是 cgroup)。mglru 把恢复变成抖晃的话,这一行会飙。
- 匿名页为主的 pod 的内存增长。看
- 主机是交互式面对的话配
min_ttl_ms=1000;后端批处理保持 0。 - 内存紧的主机上别同时跟 zswap 配一起追尾延迟——zswap、mglru、PSI 三者交互还在下游调。
单一租户工作负载(一个大数据库、一台机器):
- 已经 benchmark 验证过(fio、YCSB、sysbench)mglru 帮上忙的话,放心选。
- 不要轻信 5.15 内核时代发的厂商证言;在你用的内核版本上自己验证。
笔电 / 桌面:
- 开 mglru、开
min_ttl_ms=1000,收益(或者工作集漂出物理 RAM 时接 OOM kill)。
做这块的内核开发者 / 维护者:
- 待解的问题是匿名/文件平衡、cgroup 过度回收、页旗标预算。Deflag-from-LRU 那个工作(Kairui Song 的系列)最值得关注;真合了的话,性能和 API 面都改善。
它跟内存管理其他部分的连接
mglru 是现代 Linux 里关于内存行为的三个实时信号之一:
| 信号 | 在哪 | 告诉你什么 |
|---|---|---|
memory.pressure |
cgroup | 内存路径上 PSI stall 时间 |
memory.events.{high,max,oom} |
cgroup | 触发回收的次数 |
min_ttl_ms=... 拒收淘汰 |
系统 | 内核选 OOM 而非淘汰;事实上的计数但不直接导出 |
三者一起让你知道 mglru 配置是不是健康,或者是不是在抖晃。
「要不要用 mglru」的结论
读到这里,这边有个原则答案:
- 单个租户的工作负载跑 vendor-tuned、已经 benchmark 过: 是,mglru。
- 裸金属混跑容器: 等到 mainline 里匿名/文件平衡问题解了。当前行为已经被证明在匿名页为主的工作负载上泄漏内存。
- 发行版原厂内核出
LRU_GEN_ENABLED=n: 上游维护者没有把握把默认翻过来。除非你自己有证据,信这一点。 - 做内核开发的: deflag 重新设计是最激动人心的方向。看那个 patch 系列。
算法本身不是错的主意。它的第一版生产迭代问题定义清楚,社区在 active reconsider mainline 里这一版是不是该留下的那个。这是 in-tree 算法健康的状态;也是你别在没量过的情况下换生产内核的状态。
## 相关文章
这是 tux.fan os-kernel 标签下三篇 Linux 内核内存管理文章之一。
– [Linux PSI 内存压力:到底在测什么](https://tux.fan/zh/2026/07/21/psi-memory-pressure-zh/) —— 配套篇。回收 stall 亮 `memory.pressure`;mglru 调参的时候 PSI 就是要盯的那个信号。
– [Linux memory cgroup v2:内核真的执行这个预算](https://tux.fan/zh/2026/07/21/memory-cgroup-v2-zh/) —— mglru 在回收的那个预算模型。这篇聊三档 `low` / `high` / `max` 限额,以及 mglru 走的 per-memcg LRU 模型。
评论