内存管理-75-lru_gen-1-理论
基于 Linux-6.1.115
一、LRU_GEN 简介
1. 一句话概括
CONFIG_LRU_GEN 启用 Multi-Gen LRU(多代 LRU),它用一套基于"代"(generation)的页面老化和淘汰机制, 完全替代传统的 active/inactive 双链表 LRU,目标是更精确地识别冷热页面,在内存过量使用(overcommit)场景下显著提升回收效率和性能。
2. 传统 LRU 的问题
传统 LRU 只有两个链表:active 和 inactive。这带来三个固有缺陷:
------------------------------------------------------------------------------------------------------------------------ 问题 表现 ------------------------------------------------------------------------------------------------------------------------ 精度粗糙 仅区分"活跃/非活跃"两个状态,无法区分"刚刚访问"和"很久以前访问过一次" 时钟精度差 依赖 PG_referenced + PG_active 两位标志做近似 LRU,但这只是"最近是否被访问"的布尔值,不是真正的时间序列 大扫描开销 在内存压力下需要扫描整个 inactive 链表来找冷页,扫描量与链表长度线性相关 工作集保护弱 面对突发大文件读取(streaming IO),整个 active 链表可能被冲刷,工作集保护不足 ------------------------------------------------------------------------------------------------------------------------
3. MGLRU 的核心设计
3.1 代(Generation)替代双链表
传统 LRU: Active <--> Inactive(2 个状态) MGLRU: Gen 0 (最老) --> Gen 1 --> Gen 2 --> Gen 3 (最年轻) ↑ 淘汰方向 ↑ 新页进入方向 min_seq max_seq
页面被分配/访问时加入最年轻的代(max_seq),老化过程将页面从年轻代"滑"向年老代,淘汰从最老的代(min_seq)开始选取。
// mmzone.h #define MIN_NR_GENS 2U //最少保持 2 代(对应 active/inactive 兼容性) #define MAX_NR_GENS 4U //最多 4 代(支持更精细的冷热区分)
3.2 核心数据结构
struct lru_gen_folio { unsigned long max_seq; //最年轻代的序号(全局递增) unsigned long min_seq[ANON_AND_FILE]; //最老代的序号(匿名/文件分别维护) unsigned long timestamps[MAX_NR_GENS]; //每代的"出生时间" //核心链表:folios[代][匿名/文件][zone] struct list_head folios[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES]; //每代的页数统计 long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES]; //refault 统计(用于工作集保护决策) unsigned long avg_refaulted[ANON_AND_FILE][MAX_NR_TIERS]; unsigned long avg_total[ANON_AND_FILE][MAX_NR_TIERS]; };
3.3 层(Tier)— 更精细的热度区分
在"代"内部,页面还根据被文件描述符访问的次数分为多个层:
#define MAX_NR_TIERS 4U // Tier = order_base_2(访问次数 N) // Tier 0:N=0,1(冷,用 PG_referenced 标记) // Tier 1:N=2,3(温,PG_referenced + PG_workingset) // Tier 2:N=4..7 // Tier 3:N=8+
代(generation):基于时间的老化,通过页表扫描(walk_pte_range)驱动;
层(tier):基于访问频率,通过 folio->flags 中的 reference 位免锁递增;
3. 两大核心操作
3.1 老化(Aging)— inc_max_seq / try_to_inc_max_seq
目的:推进 max_seq,创建新的年轻代
时机:当最老代中的页不够淘汰时,需要老化来产生更多候选页
流程:
(1) 遍历进程的 mm_struct 列表;
(2) 通过 walk_pte_range() 扫描页表;
(3) 检查每个 PTE 的 accessed bit (硬件自动置位);
(4) accessed=1 的页:清除 accessed bit,提升到年轻代 <-- "续命";
(5) accessed=0 的页:不动,自然留在当前代 <-- "老化";
(6) max_seq++ 创建新一代;
这比传统 LRU 的优势:
(1) 不扫描页本身,而是扫描页表 —— 页表是紧凑的,cache 友好;
(2) 批量操作 —— 一次页表遍历可以处理整个 PMD 范围(512 个页);
(3) 精确时间 —— 利用硬件 accessed bit,而非软件模拟的 referenced;
3.2 淘汰(Eviction)— 从 min_seq 的最老代选取
流程:
(1) 从 min_seq 对应的代中选取页面(最老的页面);
(2) 按 tier 判断是否保护(高 tier 的热页可能被提升回年轻代);
(3) 不受保护的冷页被淘汰(回写/swap/释放);
(4) min_seq++ 当该代为空时推进;
lru_gen_look_around — 空间局部性优化, 这是 MGLRU 的另一个精妙设计:
传统 LRU: 逐页扫描 rmap,一次只看一个页
MGLRU: 当回收过程中遇到一个被访问的热页时,"顺便"扫描同一 PMD 内相邻的 PTE(空间局部性),把附近的热页一起提升,冷页留在原地。
效果: 利用 cache line 已经加载了相邻 PTE 的事实,几乎零额外开销地完成了批量老化判断。
4. 与传统 LRU 的完整对比
---------------------------------------------------------------------------------------------------- 维度 传统 Active/Inactive LRU Multi-Gen LRU (MGLRU) ---------------------------------------------------------------------------------------------------- 状态数 2(active/inactive) 2~4 代 × 4 层 = 最多 16 级精度 老化驱动 软件 kswapd 扫描 LRU 链表 扫描页表 accessed bit(硬件辅助) 扫描粒度 单页遍历(逐 page struct) 页表级批量(一次扫描整个 PMD 范围) cache 效率 差(链表指针跳跃) 好(页表是连续内存,cache line 友好) 工作集保护 依赖 refault 统计反馈 多层 tier + refault 统计联合决策 匿名/文件平衡 粗粒度 swappiness min_seq 独立演进,文件可以先淘汰 锁竞争 LRU lock 是全局热点 代间移动只需原子操作(folio->flags) 适合场景 内存充裕、负载稳定 内存过量使用、工作集动态变化 ----------------------------------------------------------------------------------------------------
5. 性能收益
根据 MGLRU 作者(Yu Zhao,Google)的基准测试和社区反馈:
---------------------------------------------------------------------------------------------------- 场景 传统 LRU MGLRU 提升 ---------------------------------------------------------------------------------------------------- kswapd CPU 占用(内存压力下) 高 显著降低 减少 40-80% CPU 开销 数据库(MySQL/PostgreSQL)在内存压力下的吞吐 下降明显 基本稳定 吞吐量提升 ~40% Android 应用启动延迟 高 低 减少 ~18% 冷启动时间 Chrome 多标签页(overcommit) 频繁 OOM 稳定 tab discard 减少 ~55% ----------------------------------------------------------------------------------------------------
核心原因:MGLRU 能更准确地识别真正的冷页,减少了"误杀热页再 refault"的恶性循环。
6. 启用方式
编译时默认启用: CONFIG_LRU_GEN=y + CONFIG_LRU_GEN_ENABLED=y
编译了但运行时启用: echo 1 > /sys/kernel/mm/lru_gen/enabled
运行时查看状态: cat /sys/kernel/mm/lru_gen/enabled(位掩码, cat出来是 0x0003)
enabled 是一个 bitmask:
bit 0:core功能(MGLRU 本身)
bit 1:mm_walk(页表遍历老化)
bit 2:nonleaf_young(非叶页表项 young 优化)
典型值 0x7(全部启用)或 0x0(回退到传统 LRU)。
7. 整体架构图
页面生命周期(MGLRU 视角)
[新分配/fault] --> Gen max_seq (最年轻) │ ┌───────────────────┐ │ 老化循环 │ │ (walk_pte_range) │ │ │ │ accessed=1? │ │ 是 → 留在年轻代 │ │ 否 → 自然老化 │ └───────────────────┘ │ max_seq++ ▼ Gen max_seq-1 │ ▼ Gen max_seq-2 │ ▼ Gen min_seq (最老) --> [淘汰] │ │ │ tier 高(热)? │ │ 是 → 保护/提升 │ │ 否 → 回收 │ └────────────────────┘
辅助机制:
lru_gen_look_around(): 淘汰时发现热页 --> 批量扫描相邻 PTE --> 热页提升;
Bloom filter: 记录哪些 PMD 有热页 --> 下次老化优先扫描这些区域;
refault 统计: evicted/refaulted 比率 --> 动态调整 tier 保护阈值;
8. 在 vmscan.c 中的位置
shrink_node() │ ├─ lru_gen_enabled()? │ 是 → lru_gen_age_node() //MGLRU 老化路径 │ try_to_inc_max_seq() // 遍历页表,推进 max_seq │ │ evict_folios() //MGLRU 淘汰路径 │ isolate_folios() // 从 min_seq 代选取冷页 │ shrink_folio_list() // 实际回收(writeback/swap/free) │ │ 否 → shrink_active_list() //传统 active → inactive 降级 │ shrink_inactive_list() //传统 inactive 扫描回收
MGLRU 完全替代了传统 shrink_active_list + shrink_inactive_list 的双链表扫描逻辑。
posted on 2026-08-05 15:44 Hello-World3 阅读(12) 评论(0) 收藏 举报
浙公网安备 33010602011771号