| 维度 | Linux 6.1+ MGLRU | Linux 5.10 传统 LRU | iOS (XNU) |
|---|---|---|---|
| 冷热分级 | 最多 4 代 × 4 层级(gen×tier 二维) | 2 档(active/inactive) | 队列粗粒度(active/inactive/speculative…) |
| 访问位收割 | 正向批量扫页表(walk_mm),Bloom filter 过滤 | 回收时 rmap 反向遍历逐页查(page_referenced) | 清位后等门铃(ARM64 access-flag fault 当场记账)+ 回收时 pmap_get_refmod 兜底 |
| 热度记账载体 | folio->flags 的 gen+refs 位(cmpxchg 无锁) | PG_active/PG_referenced + 链表位置(须持锁搬链表) | 物理页软件位 vmp_reference + pmap refmod |
| 老化触发 | 按代人口分布启发式(should_run_aging) | 被动:回收压力驱动 active 降级 | 水位驱动 vm_page_balance_inactive(1/2 目标) |
| 换页后路 | swap/zram(完整 swap 子系统) | swap/zram | 无传统 swap:压缩器(WKdm/LZ4)+ 重读文件 |
| 内存不够时 | OOM killer(badness 打分,min_ttl 可选防抖) | OOM killer(badness 打分) | jetsam 按优先级 band 查杀 + freezer 冻结后台 |
| 进程级隔离 | memcg(还有老/年轻两代 memcg 分代) | memcg | 每 app 限额(phys_footprint)+ band |
| 应用协同 | PSI 压力度量(面向调度) | 无(5.10 有 PSI,面向系统) | 四级压力通知(面向 App 行动)+ purgeable 一等公民 |
| 哲学 | 通用:不知道跑在哪,就做最强通用算法 | 通用(老一代实现) | 专用:知道跑在哪,直接按设备形态定制 |
阅读建议:想看"新东西"直奔 §1-§7(MGLRU 主体);想看"为什么换"先读 §8(传统 LRU 的六大痛点);想看"手机上为什么不是这么回事"读 §9。
物理内存永远比"想占用的总量"少。内核的办法是过度承诺(overcommit):把暂时没人用的内存拿去当磁盘缓存(页缓存),等真的不够了再"回收"——把冷页让给热需求。页回收(page reclaim)子系统的全部工作,就是回答一个问题:哪些页是"冷"的?
判断冷热远比想象中难,因为访问信息的产生分三条路径,各住一处:
| 记账层 | 住哪 | 谁写 | 特点 |
|---|---|---|---|
| 硬件访问位(x86 ACCESSED / ARM64 AF,俗称 young 位) | 页表项 PTE 里 | CPU 的 MMU在 TLB 填充时自动置位,内核不在场 | 成本≈0,但一页被 N 个进程映射就有 N 份;内核必须主动去"收割"(读+清零)才能知道访问发生过 |
| 软件页标志(PG_referenced / PG_active / MGLRU 的 gen+refs) | page->flags | 内核软件 | 每页一份;改它只要原子操作,但"升级/降级"传统上要配合搬链表 |
| 链表/队列位置(LRU 链表、XNU 页队列) | lruvec / vm_page_queue | 内核软件(须持锁) | 全局秩序的载体;搬动成本最高(锁竞争 + 缓存行弹跳) |
纯粹的 LRU(最近最少使用)有个致命缺陷:一次性的批量扫描(比如 grep 整个目录)会把真正的热数据全部挤出去。所以所有实用系统都引入"两次机会":第一次访问不算数,短时间内的第二次访问才确认你是热的。我们会看到:Linux 传统 LRU 用 PG_active×PG_referenced 两位状态机实现它;MGLRU 用"至少两代(MIN_NR_GENS=2)才能完成一次老去"实现它(mmzone.h:321-331 注释原文:aging needs to check the accessed bit at least twice before handing this page over to the eviction);XNU 用"active 队列待一段时间→降级到 inactive→inactive 上被再引用则升回"的队列轮转实现它。
用 grep -c lru_gen 对比真实内核源码(torvalds/linux 标签):v5.10 / v5.15 / v5.19 均为 0 处;v6.1 = 161 处;v6.6 的 mm/vmscan.c 从 5.10 的 4310 行膨胀到 8148 行(+3838 行,主体就是 MGLRU)。配置项 CONFIG_LRU_GEN + CONFIG_LRU_GEN_ENABLED(默认开启,v6.6/mm/Kconfig:1220-1233)SRC。
两个引擎的分工:aging 负责"把热页往上提"(生成新代 + 扫页表升级热页),eviction 负责"把冷页往外卖"(从最老代批量收割)。两者由 should_run_aging()(vmscan.c:5257)按"各代人口分布"协调——年轻代人口超过总量一半说明热页太多要分家,次老代不足总量 1/4 说明冷页太少要老化(见 §5.3)。
// include/linux/mmzone.h:416(v6.6,逐字摘录)
416 struct lru_gen_folio {
417 /* the aging increments the youngest generation number */
418 unsigned long max_seq;
419 /* the eviction increments the oldest generation numbers */
420 unsigned long min_seq[ANON_AND_FILE];
421 /* the birth time of each generation in jiffies */
422 unsigned long timestamps[MAX_NR_GENS];
423 /* the multi-gen LRU lists, lazily sorted on eviction */
424 struct list_head folios[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
425 /* the multi-gen LRU sizes, eventually consistent */
426 long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
427 /* the exponential moving average of refaulted */
428 unsigned long avg_refaulted[ANON_AND_FILE][MAX_NR_TIERS];
429 /* the exponential moving average of evicted+protected */
430 unsigned long avg_total[ANON_AND_FILE][MAX_NR_TIERS];
433 atomic_long_t evicted[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
435 atomic_long_t refaulted[NR_HIST_GENS][ANON_AND_FILE][MAX_NR_TIERS];
437 bool enabled;
}
关键点逐个说SRC:
[MIN_NR_GENS=2, MAX_NR_GENS=4] 之间(mmzone.h:321-347)。aging 推高 max_seq(开新代),eviction 推高 min_seq(清空并退休老代)。anon 和 file 各有独立 min_seq(因为无 swap 时 file 页可先行老去),共用 max_seq(同速老化,mmzone.h:410-418 注释)。// mm/vmscan.c:3773(v6.6,逐字摘录)
3773 static int folio_update_gen(struct folio *folio, int gen)
3774 {
3775 unsigned long new_flags, old_flags = READ_ONCE(folio->flags);
3780 do {
3781 /* lru_gen_del_folio() has isolated this page? */
3782 if (!(old_flags & LRU_GEN_MASK)) {
3783 /* for shrink_folio_list() */
3784 new_flags = old_flags | BIT(PG_referenced);
3785 continue;
3786 }
3788 new_flags = old_flags & ~(LRU_GEN_MASK | LRU_REFS_MASK | LRU_REFS_FLAGS);
3789 new_flags |= (gen + 1UL) << LRU_GEN_PGOFF;
3790 } while (!try_cmpxchg(&folio->flags, &old_flags, new_flags));
3792 return ((old_flags & LRU_GEN_MASK) >> LRU_GEN_PGOFF) - 1;
}
这是 MGLRU 最关键的工程细节:把一个页提升到更年轻的代 = 一次 cmpxchg 原子操作,不碰任何链表、不拿任何锁(调用方持 RCU 读锁即可)。对比传统 LRU 的 activate_page():摘链表 → 改标志 → 挂新链表,全程持有 lru_lock。多核高并发访问下(尤其是几十个线程共享同一把 lruvec 锁的场景),这一个差别就是缓存行弹跳(cache line bouncing)的消除SRC。
编码规则:folio->flags 存的是 gen+1(0 保留表示"不在 MGLRU 链表上",如已被隔离),见 mmzone.h:314-318 注释与 mm_inline.h:157-162 folio_lru_gen() 的 -1 还原。
// include/linux/mm_inline.h:164(逐字)
164 static inline bool lru_gen_is_active(struct lruvec *lruvec, int gen)
166 unsigned long max_seq = lruvec->lrugen.max_seq;
170 /* see the comment on MIN_NR_GENS */
171 return gen == lru_gen_from_seq(max_seq) || gen == lru_gen_from_seq(max_seq - 1);
// gen == 最年轻代 || gen == 次年轻代 ⇒ 视为 active
为了让 /proc/meminfo 的 Active:/Inactive:、/proc/vmstat、以及所有观测工具继续工作,MGLRU 把最年轻的两个代映射为"active",其余映射为"inactive"(mmzone.h:331-335 注释明言 to maintain ABI compatibility)。inc_max_seq() 里能看到同步搬运这些计数的代码(vmscan.c:4462-4483)。这不是可有可无的细节——它是 MGLRU 能作为"可直接替换实现"合入主线的前提。
// include/linux/mm_inline.h:135,143
135 static inline int lru_tier_from_refs(int refs)
140 return order_base_2(refs + 1); // refs=0,1→tier1; 2,3→tier2; 4..7→tier3; 8+→tier4
143 static inline int folio_lru_refs(struct folio *folio)
154 return ((flags & LRU_REFS_MASK) >> LRU_REFS_PGOFF) + workingset;
为什么 gen 之外还要 tier?因为访问有两条通道,热的意义不同(mmzone.h:351-370 注释):
fd 访问的入口是 folio_inc_refs()(v6.6 mm/swap.c:415,mark_page_accessed 的 MGLRU 分流),递进阶梯非常精巧——前两级白嫖既有页标志,第三级才动用专用位:
// v6.6 mm/swap.c:415 folio_inc_refs()(摘录)
415 static void folio_inc_refs(struct folio *folio)
417 unsigned long new_flags, old_flags = READ_ONCE(folio->flags);
421 if (!folio_test_referenced(folio)) {
422 folio_set_referenced(folio); // 第 1 次:置 PG_referenced → tier 1
423 return;
426 if (!folio_test_workingset(folio)) {
427 folio_set_workingset(folio); // 第 2 次:置 PG_workingset → tier 2
428 return;
431 do { // 第 3 次起:cmpxchg 递增 LRU_REFS 位 → tier 3/4
432 new_flags = old_flags & LRU_REFS_MASK;
434 if (new_flags == LRU_REFS_MASK) break; // 顶格封顶
435 new_flags += BIT(LRU_REFS_PGOFF);
436 new_flags |= old_flags & ~LRU_REFS_MASK;
437 } while (!try_cmpxchg(&folio->flags, &old_flags, new_flags));
tier 的移动全程原子位操作(mmzone.h:366-368:moving across tiers only involves atomic operations... negligible cost),而跨代移动(排序归位)才需要 LRU 锁。回收时对比各 tier 的 refault 率决定"多热的页值得保护"(§5.2)。
传统 LRU 想知道"这页最近有没有被访问",唯一的办法是从页反查到所有映射它的 PTE(rmap 反向映射),逐个读 accessed 位再清零——每页每轮都要走一遍(5.10 的 page_referenced(),vmscan.c:992 调用)。页被多个进程共享时(典型:libc),这是 O(映射数) 的树遍历 + 锁 + TLB flush。
MGLRU 反其道而行之:不问"哪些 PTE 映射这页",而是顺着进程的页表从上往下扫——扫到一个 young 的 PTE,就把它指向的页提升到最年轻代。一次遍历的产出覆盖沿途所有页,且天然批量(一个 PTE 表 512 项一次处理)。
// mm/vmscan.c:4495(摘录)
4517 if (!should_walk_mmu()) { // 硬件不自动置 accessed bit 时的回退路径
4518 success = iterate_mm_list_nowalk(lruvec, max_seq);
4520 }
4522 walk = set_mm_walk(NULL, true);
4528 walk->lruvec = lruvec; walk->max_seq = max_seq;
4533 do {
4534 success = iterate_mm_list(lruvec, walk, &mm); // 从 memcg 的 mm FIFO 逐个取进程
4535 if (mm)
4536 walk_mm(lruvec, mm, walk); // 扫这个进程的页表
4537 } while (mm);
4539 if (success)
4540 inc_max_seq(lruvec, can_swap, force_scan); // 全部扫完 → 正式立新代
要扫的进程从哪来?每个 memcg 维护一个 mm_struct 的 FIFO 链表(lru_gen_mm_list),进程 fork 时挂入、退出时摘除,进程迁移 memcg 时跟着走。aging 走完一轮 FIFO 记一次 mm_state.seq,防止陈旧遍历。两个来自官方文档的细节(doc/multigen_lru.rst:160-179):① 多个页表遍历器可并行迭代同一链表、各拿各的 mm;② 基础设施跟踪 mm 在上下文切换间的使用情况——上轮以来一直睡着的进程直接跳过(睡着的人不会访问内存)。inc_max_seq()(vmscan.c:4438-4493)在 LRU 锁内:给新代盖时间戳 timestamps[next] = jiffies、搬运 active/inactive 兼容计数、smp_store_release(&lrugen->max_seq, max_seq+1) 发布新代。
// mm/vmscan.c:4003(摘录)
4017 pte = pte_offset_map_nolock(args->mm, pmd, start & PMD_MASK, &ptl);
4020 if (!spin_trylock(ptl)) { // 拿不到锁就跳过这页表——绝不阻塞业务线程
4021 pte_unmap(pte); return false;
4022 }
4027 for (i = pte_index(start), addr = start; addr != end; i++, addr += PAGE_SIZE) {
4030 pte_t ptent = ptep_get(pte + i);
4039 if (!pte_young(ptent)) { // 没被访问过:只记账,跳过
4040 walk->mm_stats[MM_LEAF_OLD]++; continue;
4042 }
4044 folio = get_pfn_folio(pfn, memcg, pgdat, walk->can_swap);
4048 if (!ptep_test_and_clear_young(args->vma, addr, pte + i)) // 收割并清零 young 位
4049 VM_WARN_ON_ONCE(true);
4059 old_gen = folio_update_gen(folio, new_gen); // ★ cmpxchg 提升到最年轻代,无锁
4060 if (old_gen >= 0 && old_gen != new_gen)
4061 update_batch_size(walk, folio, old_gen, new_gen); // 批量记账,稍后归位
4070 return suitable_to_scan(total, young); // 本 PMD 命中率够高才推荐进 Bloom filter
四个值得咀嚼的细节SRC:
// mm/vmscan.c:4158 walk_pmd_range(摘录)
4182 pmd_t val = pmdp_get_lockless(pmd + i); // 无锁读 PMD 项(乐观读取)
4213 if (should_clear_pmd_young()) {
4214 if (!pmd_young(val)) continue;
4217 walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first); // 第二遍:持锁清 PMD young
4220 if (!walk->force_scan && !test_bloom_filter(walk->lruvec, walk->max_seq, pmd + i))
4221 continue; // ★ Bloom filter 说没戏 → 跳过整张 PTE 表
4225 if (!walk_pte_range(&val, addr, next, args))
4226 continue;
4228 walk->mm_stats[MM_NONLEAF_ADDED]++;
4231 update_bloom_filter(walk->lruvec, walk->max_seq + 1, pmd + i); // 上轮热 → 记入下轮情报
Bloom filter 双缓冲(mmzone.h:447-459 lru_gen_mm_state:NR_BLOOM_FILTERS=2,flip after each iteration):以 PMD 页表指针为 key。空间局部性(热数据往往连片)使得这个简单过滤器命中率高得惊人;冷区则整表跳过。遍历成本从 O(全部页表) 降到 O(热区页表)。注意官方文档强调的反馈方向(doc/multigen_lru.rst:181-193):rmap 遍历贵在"不同 VMA 的页对 rmap 不缓存友好"——所以用 look_around 反哺 Bloom filter:eviction 的 rmap 走到 young PTE 时环视邻居,若邻居也热(缓存行高效),就把这个 PMD 记入 filter,下轮 aging 直接定点扫这张页表——eviction 和 aging 之间形成闭环。误报的代价只是一次多余的 PTE 表扫描(说不定还能捞到热页),可控(rst:206-209)。
两遍式设计(4172-4176 注释):第一遍无锁扫 PTE(避免 PMD 锁);只有 PMD young 位还置着时才第二遍拿锁清 PMD 位——非叶子页表项的 young 位是"这张子表里曾有活动"的免费提示,x86 上需手动清(对应开关 0x0004,§6.2)。
// mm/vmscan.c:4650(摘录)
4667 lockdep_assert_held(pvmw->ptl);
4670 if (spin_is_contended(pvmw->ptl))
4671 return; // 锁有竞争,立刻放弃
4676 start = max(addr & PMD_MASK, pvmw->vma->vm_start); // 以正在回收的页为中心
4698 for (i = 0, addr = start; addr != end; i++, addr += PAGE_SIZE) {
4700 pte_t ptent = ptep_get(pte + i); // 环视同 PMD 的邻居 PTE
... young 的邻居同样 folio_update_gen 提升到最年轻代 ...
eviction 的 rmap 走到某页时(try_to_unmap 路径),反正 PTE 锁已经在手,干脆环视同一张页表里的邻居——young 的顺手提升。这叫"捎带式老化"(piggyback):即使硬件不支持批量收割(should_walk_mmu() 为假,如某些 ARM 配置)、或 aging 引擎没轮到,回收路径自己也能积累热度信息。两台引擎互为备份SRC。
// mm/vmscan.c:5353 try_to_shrink_lruvec(摘录)
5364 while (true) {
5367 nr_to_scan = get_nr_to_scan(lruvec, sc, swappiness); // 内部决定要不要先 aging
5368 if (nr_to_scan <= 0) break; // -1 = 刚做过 aging,换 lruvec;0 = 没活干
5371 delta = evict_folios(lruvec, sc, swappiness); // 批量驱逐一批
5372 if (!delta) break;
5375 scanned += delta;
5379 if (sc->nr_reclaimed >= nr_to_reclaim) break; // 够数就收工(避免过度回收)
}
// mm/vmscan.c:5165 evict_folios(摘录)
5181 spin_lock_irq(&lruvec->lru_lock);
5183 scanned = isolate_folios(lruvec, sc, swappiness, &type, &list); // ① 从最老代批量摘 64~4096 页
5185 scanned += try_to_inc_min_seq(lruvec, swappiness); // ② 老代清空则退休(min_seq+1)
5190 spin_unlock_irq(&lruvec->lru_lock);
5195 reclaimed = shrink_folio_list(&list, pgdat, sc, &stat, false); // ③ 复用经典回收:unmap+写回+释放
5198 list_for_each_entry_safe_reverse(folio, next, &list, lru) {
5213 if (skip_retry || folio_test_active(folio) || folio_test_referenced(folio) ||
5214 folio_mapped(folio) || ...) {
5217 set_mask_bits(&folio->flags, LRU_REFS_MASK | LRU_REFS_FLAGS, BIT(PG_active)); // ④ 拒收页标记 active 下不为例
5229 move_folios_to_lru(lruvec, &list); // ⑤ 归位(sort_folio 决定去哪)
}
三个设计亮点SRC:
mmzone.h:392-393),摊薄了每页锁成本。aging 期间页只改了学籍卡(gen 标签)没换座位;eviction 把页拿在手里时才顺手归位——这就是 folios[][] 注释"lazily sorted on eviction"的含义:
// mm/vmscan.c:4896 sort_folio(摘录,六种判决)
4911 if (!folio_evictable(folio)) { ...挪去 unevictable 链表... }
4921 if (脏的 lazyfree anon) { ...挪尾部... }
4930 /* promoted */ if (gen != lru_gen_from_seq(lrugen->min_seq[type]))
4931 { list_move(&folio->lru, &lrugen->folios[gen][type][zone]); return true; } // ★ aging 升过代的页:搬到它该在的代
4936 /* protected */ if (tier > tier_idx) { folio_inc_gen(...); 记 protected 统计; } // 高 tier 且 refault 率高:升一代保护
4948 /* ineligible */ if (zone > sc->reclaim_idx || CMA) { 升一代; }
4955 /* waiting for writeback */ { 升一代, 带上 PG_reclaim; }
/* 以上都不是 ⇒ 真冷页,进入 shrink_folio_list 回收 */
注意 "promoted" 分支(4930-4933)就是无锁提升的收尾:aging 时 folio_update_gen() 改了标签,此刻把它搬到 gen 对应的链表头部。物理搬运被推迟到"反正已经拿在手里"的时刻——每页每次晋升节省一次专门的链表操作,这是 MGLRU 降低锁竞争的核心账本。
// mm/workingset.c:231,279(摘录)
250 token = (min_seq << LRU_REFS_WIDTH) | max(refs - 1, 0); // 驱逐时的 token:哪个代走的+热度
253 atomic_long_add(delta, &lrugen->evicted[hist][type][tier]);
—— 页回流(refault)时 ——
292 recent = lru_gen_test_recent(shadow, type, &lruvec, &token, &workingset);
298 if (!recent) goto unlock; // 走的时候不是最近一代:正常回流,不奖励
305 refs = (token & (BIT(LRU_REFS_WIDTH) - 1)) + workingset; // 还原当时的 refs
306 tier = lru_tier_from_refs(refs);
308 atomic_long_add(delta, &lrugen->refaulted[hist][type][tier]); // 按 tier 记账!
318 if (lru_gen_in_fault() || refs == BIT(LRU_REFS_WIDTH)) {
319 folio_set_workingset(folio); // 缺页即刻回流 = 工作集在抖动
}
传统 5.10 的 refault distance 是个一维标量(阴影里只存"走时的工作集水位",见 §8.5);MGLRU 的影子 token 是(min_seq, refs) 二元组——"从哪一代走的 + 当时多热"。"最近一代就回流"(token 的 seq == 当前 min_seq)才算 refault,记账精确到 tierSRC。
// mm/vmscan.c:5075 get_tier_idx(摘录)
5081 * To leave a margin for fluctuations, use a larger gain factor (1:2).
5085 read_ctrl_pos(lruvec, type, 0, 1, &sp); // 第 1 层的 refault 均值(基准)
5086 for (tier = 1; tier < MAX_NR_TIERS; tier++) {
5087 read_ctrl_pos(lruvec, type, tier, 2, &pv); // 第 tier 层的 refault 均值(×2 增益)
5088 if (!positive_ctrl_err(&sp, &pv)) // 比值显著高才继续升 tier
5089 break;
5092 return tier - 1; // 得到 tier_idx:回收时 tier > tier_idx 的页受保护(sort_folio L4936)
逻辑:比较"第 1 层(访问 0-1 次)"与"第 N 层(访问 ≥2^(N-1) 次)"的 refault 比率(refaulted / (evicted+protected) 的指数移动平均,read_ctrl_pos)。高 tier 的页回流率若没有显著更高,保护就不划算——它们只是被读过几遍,不是真热。增益 2:1 留出波动余量(5081-5084 注释)。同样思路的 get_type_to_scan()(vmscan.c:5095-5121)决定先扫 anon 还是 file:把 swappiness 编进增益(gain[] = {swappiness, 200-swappiness})后比较两类页的 refault 率——swappiness 从"拍脑袋比例"变成了"带统计检验的反馈控制器"。
// mm/vmscan.c:5257 should_run_aging(摘录)
5269 if (min_seq[!can_swap] + MIN_NR_GENS > max_seq) { // 冷页彻底耗尽
5270 *nr_to_scan = 0; return true; // ⇒ 必须 aging(立新代制造冷页)
...
5296 * The aging tries to be lazy to reduce the overhead, while the eviction
5297 * stalls when the number of generations reaches MIN_NR_GENS. Hence, the
5298 * ideal number of generations is MIN_NR_GENS+1.
5301 if (min_seq[!can_swap] + MIN_NR_GENS < max_seq)
5302 return false; // 代数还富余:不用 aging(懒字诀)
5311 if (young * MIN_NR_GENS > total) // 最年轻代 > 总量 1/2:热页堆太满
5312 return true;
5313 if (old * (MIN_NR_GENS + 2) < total) // 次老代 < 总量 1/4:快没冷页了
5314 return true;
理想分布是每代占 1/(MIN_NR_GENS+1) = 1/3(5304-5309 注释),aging 盯上限(热页 ≤1/2),eviction 盯下限(冷页 ≥1/4)。aging "能懒则懒",因为页表扫描虽是批量也有成本——这是 MGLRU 在"精度"与"开销"之间的显式权衡SRC。
页有代,memcg 本身也被分成老/年轻两代(mmzone.h:496-546):老代 memcg 优先被全局回收光顾;淘汰一轮下来还"有货"或低于 min 水位的 memcg 升入年轻代。每代再随机打散到 MEMCG_NR_BINS=8 个 bin 降低锁竞争(mmzone.h:534-535)。触发规则全部注释在 mmzone.h:518-528:超软限→排头(优先回收);低于 low/可回收阈值→排尾;低于 min 或完成 aging→升年轻代。全局公平由各 memcg max_seq 的轮转推进保证。
| 值 | 组件 | 说明 |
|---|---|---|
| 0x0001 | 主开关 | 整体启用 MGLRU(默认开,取决于 CONFIG_LRU_GEN_ENABLED) |
| 0x0002 | 叶子表批量清位 | 批量清除 PTE 的 accessed 位(x86 等 MMU 自动置位架构);关闭则轻微性能损失 |
| 0x0004 | 非叶子表清位 | 连 PMD 等 non-leaf 项的 young 位也清(仅 Intel/AMD 验证过);关闭则可忽略的损失 |
以上来自内核文档 Documentation/admin-guide/mm/multigen_lru.rst:35-52SRC。运行时可随时开关:写入 y/n 作用于全部位。切换的实现(vmscan.c:5686 lru_gen_change_state())相当讲究——翻 static_branch 静态键后,遍历所有 memcg×node 的 lruvec,把页在两套 LRU 之间整体搬迁(启用时 fill_evictable() 填充、禁用时 drain_evictable() 排空),出问题能一键回退传统 LRU——大改动合入主线的"安全带"。
// mm/vmscan.c:4598,4600 lru_gen_age_node(摘录)
4598 static unsigned long lru_gen_min_ttl __read_mostly; // 默认 0=关闭
4615 if (lruvec_is_reclaimable(lruvec, sc, min_ttl)) { ... return; } // 还有 ≥min_ttl 的老代,正常回收
4624 * The main goal is to OOM kill if every generation from all memcgs is
4625 * younger than min_ttl.
4628 if (mutex_trylock(&oom_lock)) { out_of_memory(&oc); } // ★ 全部内存都"太年轻" ⇒ 直接杀进程
写给笔记本/桌面用户(文档 64-82 行):写入 N 毫秒后,"最近 N 毫秒的工作集"受绝对保护——若这都保不住,说明内存真不够了,与其反复抖动(每次缺页都 IO)不如直接触发 OOM 杀掉应用。文档建议:1000ms 消除人可感知的卡顿(人类感知延迟阈值 ~100ms 的 10 倍余量),3000ms 更保险但更激进。本质是把"抖到死"换成"痛快死"的可调压力阀SRC。
/sys/kernel/debug/lru_gen 提供:工作集直方图(读出每个 memcg×node 下各代的"多少页在多少毫秒内被访问过"——数据中心装箱调度的利器);写入 + memcg_id node_id gen 主动立新代、- ... 主动驱逐指定代(主动回收冷页)。详见 multigen_lru.rst:94-162SRC。MGLRU 把"工作集多大"变成了可直接查询的内核接口——这在传统 LRU 下只能间接推算。
| 收益 | 机制根源(源码) |
|---|---|
| kswapd/回收 CPU 大幅下降 | ① 无锁晋升:folio_update_gen 的 cmpxchg 替代搬链表(§2.2);② 正向批量扫页表 + Bloom filter 跳过冷区,替代每页 rmap 反查(§3);③ 惰性排序把物理归位合并进回收批处理(§4.3) |
| 冷热分辨率提升 8 倍 | 2 档(active/inactive)→ 4 代×4 层级的二维坐标(§2);高共享页(libc 等)不再因 rmap 贵而被"误判保持热" |
| 防抖能力 | min_ttl_ms 绝对保护最近工作集(§6.3);tier 保护基于统计显著性而非经验阈值(§5.2) |
| memcg 可伸缩性 | 老/年轻两代×8 bin 的分片轮转(§6.1),替代单链表全局迭代 |
| 新能力 | 工作集直方图、主动回收成为稳定 debugfs 接口(§6.4) |
官方与产业报告(Google 的 Chrome OS/Android/服务器、Meta 生产环境等)报告了内存占用下降与卡顿/OOM 改善,具体数字随工作负载差异很大,建议以内核文档与原始 patch 系列讨论为准GEN。
Kconfig:1223-1224 的 depends 64BIT || !SPARSEMEM || SPARSEMEM_VMEMMAP)。以下全部基于 v5.10 真实源码核对;结论同样适用于关闭 MGLRU 的更新内核(传统路径一直保留作为回退)。
每个 lruvec(memcg×node)有 5 条链表(mmzone.h:278 lists[NR_LRU_LISTS]):anon/file 各一对 active+inactive,外加 unevictable。页在哪条链上由 PG_active 一个位决定(mm_inline.h:115 page_lru())。头=热端(新),尾=冷端(老),回收从尾部吃。冷热分辨率 = 2 档 × 链内位置(但链内位置只在回收时粗略有意义)。
// mm/swap.c:412-421 注释原文(5.10,状态机四转换)
inactive,unreferenced -> inactive,referenced (第一次访问: mark_page_accessed 置 PG_referenced)
inactive,referenced -> active,unreferenced (第二次访问: folio_activate 搬去 active 头部)
active,unreferenced -> active,referenced (继续访问)
active,referenced -> active,referenced (保持 active)
// 入口 mark_page_accessed()(swap.c:412);激活 activate_page()(swap.c:351,5.10 尚未 folio 化):
// 摘链表 → SetPageActive → 挂 active 头部,全程持有 lru_lock
注意:对映射页,"访问"的原始证据在 PTE 的硬件 accessed 位里,mark_page_accessed 只在缺页、mmap 读等内核在场的时刻被调用。用户态指针解引用不经过内核——那些访问攒在 PTE 里,要等回收时 rmap 收割(8.3)。
// v5.10 get_scan_count 的 fractionary 公式(vmscan.c:2315-2320,逐字)
2315 ap = swappiness * (total_cost + 1); // anon 扫描压力 ∝ swappiness × 反比于 anon 回收成本
2316 ap /= anon_cost + 1;
2318 fp = (200 - swappiness) * (total_cost + 1); // file 同理(200=2×100 的归一化)
2319 fp /= file_cost + 1;
成本来自"refault 比例 × IO 代价"的反馈(注释:inversely proportional to the cost),swappiness 只是加权系数。MGLRU 保留了同样的反馈思想(get_type_to_scan 的 gain[] = {swappiness, 200-swappiness},§5.2),但比较对象换成了按 tier 细分的 refault 统计——反馈粒度从"两类页各一个数"变成"两类页×四层各一个数"SRC。
// v5.10 mm/vmscan.c:986(摘录)
992 referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup, &vm_flags);
994 referenced_page = TestClearPageReferenced(page);
1003 if (referenced_ptes) {
1018 SetPageReferenced(page);
1020 if (referenced_page || referenced_ptes > 1)
1021 return PAGEREF_ACTIVATE; // 软件+硬件都引用过,或多个进程映射都在访问 → 升 active
1027 return PAGEREF_KEEP; // 第一次 → 留在 inactive 再转一圈
1033 if (referenced_page && !PageSwapBacked(page))
1034 return PAGEREF_RECLAIM_CLEAN; // 无引用 → 回收
page_referenced() 是传统方案的原罪:每页每次被考察都要从 page → 反向映射 → 遍历该页全部 PTE → 读+清 accessed 位。共享 N 次的页(libc、共享缓存)成本 O(N),还要拿锁、可能 flush TLB。
// v5.10 mm/vmscan.c:2009 shrink_active_list()(摘录)
2030 nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold, ...); // 从 active 尾部摘一批
2059 if (page_referenced(page, 0, ...)) { // 降级前也查一次 rmap
2070 if ((vm_flags & VM_EXEC) && page_is_file_lru(page)) {
2072 list_add(&page->lru, &l_active); continue; // 可执行页例外:留在 active
2075 }
2077 ClearPageActive(page); /* we are de-activating */
2078 SetPageWorkingset(page);
2079 list_add(&page->lru, &l_inactive); // 其余全部降级,挂 inactive 头部(重新排队)
// v5.10 mm/vmscan.c:2202 inactive_is_low()(逐字)
2212 gb = (inactive + active) >> (30 - PAGE_SHIFT);
2213 if (gb)
2214 inactive_ratio = int_sqrt(10 * gb); // 内存越大,inactive 占比要求越高: 1GB→3.2, 100GB→31.6
2215 else
2216 inactive_ratio = 1;
2218 return inactive * inactive_ratio < active;
active 降级是批量的、无差别的(除可执行页外整批搬走);何时降由双重闸门决定(vmscan.c:2695-2719):① inactive_is_low()——那个著名的 int_sqrt(10*gb) 经验公式(内存越大 inactive 缓冲要求越深:1GB→3.2,100GB→31.6);② refaults 快照有变化——一旦观察到新的工作集正在建立(refault 激活计数动了),就赶紧降级赶走 active 里的陈旧页(注释原文:Deactivate to get rid of any stale active pages quickly)。这两个魔法数/启发式正是 MGLRU 用"代人口分布"(§5.3)替代的东西SRC。
// v5.10 mm/workingset.c:338,360,376(摘录)
338 refault_distance = (refault - eviction) & EVICTION_MASK; // 走的时间戳与回的时间戳之差
360 workingset_size = lruvec_page_state(eviction_lruvec, NR_ACTIVE_FILE);
... (按有无 swap 决定是否计入 anon 与对方 inactive) ...
373 if (refault_distance > workingset_size)
374 goto out; // 离开太久:走了就走了,别回来装热
376 SetPageActive(page); // 距离内回流 = 误伤 → 直接进 active
影子条目记录"驱逐时刻的水位",refault 时比较距离与当前工作集大小。一维标量、与页自身热度无关。file 与 anon 都有影子(anon 走 swap cache 分支:vmscan.c:899-906 的 PageSwapCache 路径调用 workingset_eviction();refault 侧 workingset.c:284 按 page_is_file_lru 分流)SRC。对比 MGLRU 的 (min_seq, refs) 二元 token(§5.1)——差的不是有无,而是"逐出时你多热"这个维度。
| # | 痛点(5.10) | 源码证据 | MGLRU 对策(回链) |
|---|---|---|---|
| 1 | 两档分辨率太粗:active 内部无序,刚激活的和常年热的一样待遇 | page_lru() 只看 PG_active 一位(mm_inline.h:115) | 4 代×4 层级二维坐标(§2) |
| 2 | 每次访问都要搬链表:mark_page_accessed/activate 持 lru_lock 摘挂,多核弹跳缓存行 | folio_activate(swap.c:351) | cmpxchg 改 gen 标签,物理位置不动(§2.2) |
| 3 | rmap 反向查岗贵:每页每轮 O(映射数) 遍历+锁+TLB | page_referenced(vmscan.c:992) | 正向批量扫页表+Bloom filter(§3) |
| 4 | 老化被动且一刀切:只有回收压力来了才降级 active,整批无差别 | shrink_active_list(2009) | aging 主动分代,热页升代冷页自然沉淀(§3、§4.3) |
| 5 | 魔法数硬编码:sqrt(10*gb)、swappiness 线性比例 | inactive_is_low(2214)、get_scan_count(2237) | 人口分布启发式+统计显著的 tier 判定(§5.2、§5.3) |
| 6 | 工作集不可观测:只有间接计数器 | — | debugfs 直方图+主动回收接口(§6.4) |
源码基准 xnu-7195.141.2(iOS 14.8),本站统一引用工作区 deep-read 存档(与上游逐字节一致的 genuine 版本,vm_pageout.c 10996 行);实验数据沿用本工作区 iPhone 12 Pro 实测。
iOS 设备没有传统磁盘换页(swap)——这不是"没配置",而是编译期开关就关死了:vm_pageout.c:4967-4999 vm_config 的 switch 里,COMPRESSOR 模式下 swap 只留给 freezer 用(把冻结进程的内存写到受保护分区,不参与常规换页)。物理内存吃紧时 XNU 的选项只有三个:
| 路 | 机制 | 源码 | Linux 对应物 |
|---|---|---|---|
| 压缩 | 匿名页压缩进 VM 内嵌压缩器(WKdm/LZ4 混合),驻留但省空间 | vm_compressor.c(WKdm 选择 :3917;算法 vm_compressor_algorithms.c:52,276) | zram(用户态块设备,可插拔算法) |
| 丢弃+重读 | 干净的文件页直接丢,要用时从 NAND 重读 | vm_pageout_scan external 路径 | 页缓存回收(相同思想) |
| 查杀/冻结 | jetsam 按优先级 band 杀进程;freezer 把后台整进程冻结 | kern_memorystatus.h:40-62(band 全表);kern_memorystatus_freeze.c | OOM killer(badness 全局打分) |
连"抖动"的处置都不同:Linux 抖动时更努力地换页(或 min_ttl 直接放弃),XNU 抖动时(压缩器饱和)直接升级为 jetsam 杀进程(vm_compressor.c:2241-2268)——它没有"换页扛过去"这个选项SRC。
先立证据:XNU 没有 per-page LRU 链表。每页只有 4 bit 的 vmp_q_state(vm_page.h:271)标记"在哪个全局队列"(vm_page.h:1268-1270 声明的 active/inactive 等队列头),队列进出是 FIFO——先入队的页先被降级、先被考察。冷热信息 = 队列归属 + 引用位,没有更细的粒度。
// vm_pageout.c:2769 vm_page_balance_inactive()(摘录,iOS 14.8)
2786 vm_page_inactive_target = VM_PAGE_INACTIVE_TARGET(vm_page_active_count +
2787 vm_page_inactive_count + vm_page_speculative_count);
// VM_PAGE_INACTIVE_TARGET(avail) = avail * 1/2 (vm_pageout.c:208)
2790 while (max_to_move-- && (vm_page_inactive_count + vm_page_speculative_count) < vm_page_inactive_target) {
2793 m = (vm_page_t) vm_page_queue_first(&vm_page_queue_active); // active 队列头=最老(FIFO)
2813 if (m->vmp_pmapped == TRUE) {
2814 pmap_clear_refmod_options(..., VM_MEM_REFERENCED, PMAP_OPTIONS_NOFLUSH, NULL); // ★清引用位,故意不刷 TLB
2822 vm_page_deactivate_internal(m, FALSE); // 降级到 inactive 队列
}
XNU 的"老化"就这么朴素:每次循环从 active 队列头摘一页(先来先老),清掉引用位,扔进 inactive(vm_pageout_scan 主循环 :2950 每轮调用)。inactive 目标恒为总量 1/2(:208)。没有代、没有 tier、没有链内排序——队列位置本身就是唯一的"温度计",且只有队列头尾两个采样点SRC。不刷 TLB 的理由源码注释写得很诚实(:2802-2812):最坏情况是漏记远端 CPU 缓存里的引用——"TLB 缓存不会存活太久",可接受。
// vm_pageout.c:3357 inactive 队列上的两次机会(摘录)
3357 refmod_state = -1;
3359 if (m->vmp_reference == FALSE && m->vmp_pmapped == TRUE) {
3360 refmod_state = pmap_get_refmod(VM_PAGE_GET_PHYS_PAGE(m)); // 读物理页 ref/mod 位
3362 if (refmod_state & VM_MEM_REFERENCED) {
3363 m->vmp_reference = TRUE; // 降级后被访问过 → 软件位记下
3365 }
3368 }
3381 if (!m->vmp_no_cache && page_from_bg_q == FALSE &&
3383 (m->vmp_reference || ...)) {
// 被引用 → 升回 active(:3405 reactivate),受 reactivate_limit 限流防饿死回收 (:273)
补充:老化触发同样有"五阶段"流水线(balance :2950 → free-target 检查 :2959 → purgeable 对象 :3006 → speculative 队列老化 :3034 → 对象缓存驱逐 :3051),水位驱动睡眠-唤醒(真机实测空闲时计数器完全静止)SRCEXP。
这是 XNU 最有意思的分叉。硬件访问位在 PTE 里,Linux 的做法是回收时 rmap 查岗(§8.3)或 MGLRU 正向扫楼(§3)。XNU 在 ARM64 上玩出了第三种:
代价与保险:第一个撞门的访问者付 O(映射数)(100 映射 ≈10μs);NOFLUSH 让其他核的 TLB 缓存连 fault 都不产生(漏记,源码注释明认)。实测旁证:热探针 55 秒 135 万次访问仅产生 ≈0.4% 单核的 fault 开销——把记账成本转嫁给访问路径的偶发 fault,换回收路径的 O(1) 判定SRCEXP。
再补一个粒度视角的总对照:XNU 的全部三类引用位操作都是单页粒度——降级时清位(:2813-2814)、逐出前读位(:3360)、门铃登记(fault 时)——从不批量扫页表。MGLRU 则为代际精度付出成批遍历页表的代价。一句话:Linux MGLRU 像勤快的仓库管理员定期推小车全店盘点(批量扫页表换精度);iOS 的管理员从不盘点,只在拿起一件货准备扔的瞬间低头看一眼标签(单页 pmap_get_refmod)——便宜,代价是同队列内的页没有个体年龄,存活时间只取决于队列流量SRC。
inactive 队列上的匿名页,老到一定年龄(compute_swapout_target_age,vm_compressor.c:353)就被压缩进 c_segment。算法 WKdm 为主(字典式,快且可随机解压单页,:3917)、LZ4 备选(vm_compressor_algorithms.c:52,276)。段内碎片由 major/minor compaction 处理。真机实测:iPhone 12 Pro 受压时 4 秒内压缩器吸收 19.7% 内存,回收 CPU 的 95%+ 花在 WKdm 上——压缩器是 iOS 事实上的"swap 设备",只是介质是 RAM 本身SRCEXP。Android 同期主流方案 zram 也是压缩换页,殊途同归。
Linux OOM 按 badness 打分全局挑"最肥的"杀;iOS 预先把进程排进 210 个优先级 band(kern_memorystatus.h:40-62 定义 IDLE=0 → IDLE_DEFERRED/AGING_BAND1=1 → BACKGROUND_OPPORTUNISTIC=2 → BACKGROUND=3 → … → FOREGROUND → 各系统关键服务),内存不够时从最低 band 往上杀(从 IDLE 起遍历,:1298/:5701 一带)。App 退后台先进 aging band(10→15→0 三级流水线各缓冲约 10-30 秒,kern_memorystatus.c:307-308,406-442 一带的 band 迁移逻辑)防"刚切走就被杀"。记账用 phys_footprint(task.c:986-993:内含 internal+compressed+iokit_mapped+purgeable_nonvolatile+page_table——压缩后的内存也计入),查杀与记账同币种——这是 iOS 记账体系相对 Linux(memcg 记账与 OOM 决策割裂)的结构优势SRC。
vm_purgeable.c),紧张时零成本丢弃。Linux 只有 MADV_FREE 的懒提示——语义代差。iOS 的页级策略这些年大体稳定,增量都往"查杀与冻结"倾斜:freezer 相关代码从 iOS 12 的约 2400 行扩到 iOS 18 的约 3250 行;swapout 引用 48→50→55→101→102→102(iOS 16 前后翻倍);sustained-pressure kill(持续高压下提前杀进程)是 iOS 16 新增(六版本统计 0→0→0→6→6→6 处引用)——又一个"宁可杀进程、不做更细页级算法"的注脚SRC。
判定限定场景:服务器/通用多租户 vs 单用户交互设备(手机)。"能力独有"= 另两方主线无对应机制。
| # | 对比项 | MGLRU (6.1+) | 5.10 传统 LRU | iOS XNU | 判定 |
|---|---|---|---|---|---|
| 1 | 冷热分辨率 | 4 代 × 4 层级(gen×tier 二维,免锁 tier 递增) | 2 档(active/inactive)×链内位置 | 队列 FIFO 位置(粗粒度,仅头尾两采样点) | MGLRU 胜 8 倍类别数 |
| 2 | 访问位收割方式 | 正向批量扫页表(walk_mm)+ Bloom filter 剪枝 + PMD 两遍式 | 反向 rmap 查岗(page_referenced,每页 O(映射数)) | 门铃登记(ARM64 access-flag fault 当场写软件位,回收 O(1) 判定) | iOS 架构最优雅但绑死 ARM;MGLRU 是通用架构下最优解 |
| 3 | 热度记账成本 | cmpxchg 改 folio->flags(无锁)+ 惰性归位 | 持 lru_lock 摘挂链表(缓存行弹跳) | 清 refmod 位(NOFLUSH 免 TLB)+ 偶发 fault 转嫁 | MGLRU 与 XNU 平手,均碾压传统 |
| 4 | 老化主动性 | 主动 aging(人口分布启发式,能懒则懒) | 被动(回收压力驱动降级) | 水位驱动(inactive_target=1/2,每轮 balance) | MGLRU 胜 反馈最精细 |
| 5 | 工作集保护 | (min_seq, refs)二元 token + 按 tier 统计显著性判定 | 一维 refault distance + sqrt 魔法数 | 无显式模型(靠 band/aging 与压缩器兜底) | MGLRU 胜 模型最完整 |
| 6 | 防抖机制 | min_ttl_ms 绝对保护 + OOM 压力阀 | 无专门机制(靠 watermark boost 缓解) | 四级压力通知 + 滞回(App 主动释放)+ jetsam 提前量 | iOS 胜(面向体验的完整闭环) |
| 7 | 换页后路 | swap/zram 完整体系(真正的逃生通道) | 同左 | 无 swap:压缩器即"swap"(RAM 当介质) | 各有胜场:大内存服务器要 swap,手机 NAND 寿命敏感 |
| 8 | 内存耗尽终局 | OOM killer(badness 打分,可配 min_ttl) | OOM killer(badness 打分) | jetsam 按优先级 band 查杀 + 三级 aging band 防误杀 | 手机场景 iOS 胜(可预期、有缓冲);服务器 memcg 隔离 Linux 胜 |
| 9 | 进程级隔离 | memcg(页级代之外,memcg 也分老/年轻两代×8 bin) | memcg | phys_footprint 双限额(前台宽/后台严)内核强制 | 各有胜场:多租户 Linux 强,单用户体验 iOS 强 |
| 10 | 应用协同 | PSI(连续量,面向调度) | PSI(5.10 已有) | 压力通知(离散四级,面向 App 行动)+ purgeable 一等公民 + freezer | iOS 胜(协作语义丰富) |
| 11 | 可观测性 | debugfs 工作集直方图 + 主动回收接口(工作集变成可查询 API) | /proc/vmstat 间接计数 | vm_stat/task_info 一致链(Instruments) | MGLRU 新能力独有(数据中心装箱利器) |
| 12 | 可回退性 | 运行时开关(enabled 位掩码),传统路径完整保留 | —(它就是默认) | —(无从选择) | MGLRU 工程稳健性加分 |
| 概念 | 比喻 | 要点回链 |
|---|---|---|
| 三套系统的问题 | 小区图书馆管理员想知道哪些书没人看:挨家抄门上的自动记录器(扫页表)、拿起书翻借阅章(rmap)、门口装刷卡机(fault 登记) | §0.1 |
| MGLRU 的代 | 按届分班的学校:最多 4 个届,表扬只改学籍卡届别(cmpxchg),期末大扫除才换教室(惰性归位) | §1.2、§2.2、§4.3 |
| MGLRU 的 tier | 学籍卡上的自习次数星标(对数分桶:2-3 次=2 星,4-7 次=3 星) | §2.4 |
| 正向扫描 vs rmap | 人口普查扫楼 vs 户籍反查打电话;Bloom filter=上轮查到活人的楼优先、空楼跳过 | §3.1、§3.4 |
| min_ttl | 抖动保险丝:最近 N 毫秒的书绝对不动,动不了就拉闸(OOM)痛快死 | §6.3 |
| 传统 LRU 降级 | 一刀切分班考试:inactive 不够深就整批往冷班搬(只保可执行页),考纲是 sqrt(10×GB) 魔法数 | §8.4 |
| iOS 整体策略 | 会员制公寓:无郊区仓库(无 swap),行李真空打包塞床底(压缩器),按会员等级清退(band 查杀),长期不住整体封存(freezer) | §9.1 |
| XNU 引用位 | 门铃登记:清 AF 位=锁门装刷卡机,谁进门谁自己登记(fault 写软件位),管理员躺着收登记表 | §9.3 |
把传统 LRU 里隐式、分散、被动的成本(每页搬链表、每页 rmap、魔法数阈值),换成显式、批量、可控的成本(成批扫页表、无锁改标签、统计判定)——不改 LRU 思想,只换它的实现物理学,性能收益全部来自"批处理化 + 无锁化 + 可观测化"。
两位状态机 + 两条链表在单核时代是成本假设成立的最优解;它的六大痛点(档位粗/锁竞争/被动老化/rmap 贵/一刀切/不可观测)全是多核大内存时代才暴露的——不是设计错误,是时代变了。
苹果知道自己跑在哪:单用户交互设备、ARM64、NAND 寿命敏感、App 生态可控——所以放弃页级精细 LRU,用队列 FIFO + 门铃收割 + 压缩器 + band 查杀 + 应用协同,把"哪个进程该活"置于"哪个页该留"之上。Linux 不知道自己会跑在哪,所以必须把通用算法做到极致——MGLRU 正是这个约束下的杰作。
| 主题 | 文件:行号(v6.6 / v5.10 / xnu-7195.141.2) | 内容 |
|---|---|---|
| MGLRU 数据结构 | v6.6 include/linux/mmzone.h:321-347 | MIN/MAX_NR_GENS、滑动窗口、gen+1 编码、ABI 兼容注释 |
v6.6 mmzone.h:351-370 | tier 语义、MAX_NR_TIERS、LRU_REFS 位布局 | |
v6.6 mmzone.h:416-444 | struct lru_gen_folio(max_seq/min_seq/folios[]/统计) | |
v6.6 mmzone.h:447-459 | lru_gen_mm_state(Bloom filter 双缓冲) | |
v6.6 mmzone.h:496-546 | memcg 老/年轻两代×8 bin | |
v6.6 include/linux/mm_inline.h:135-165 | lru_tier_from_refs/folio_lru_refs/folio_lru_gen/lru_gen_is_active | |
| MGLRU aging | v6.6 mm/vmscan.c:3773 folio_update_gen() | cmpxchg 无锁改代(最关键函数) |
v6.6 mm/swap.c:415 folio_inc_refs() | fd 访问的 tier 递进阶梯(PG_referenced→PG_workingset→refs 位) | |
v6.6 vmscan.c:4003 walk_pte_range() | PTE 批量收割(spin_trylock 不等待) | |
v6.6 vmscan.c:4158 walk_pmd_range() | Bloom filter 过滤 + 两遍式扫描 | |
v6.6 vmscan.c:4438 inc_max_seq() | 立新代(时间戳/兼容计数/发布) | |
v6.6 vmscan.c:4495 try_to_inc_max_seq() | aging 主入口(mm FIFO 迭代) | |
v6.6 vmscan.c:4650 lru_gen_look_around() | 捎带式老化(回收时环视邻居) | |
v6.6 vmscan.c:5257 should_run_aging() | 人口分布启发式(1/2 上限、1/4 下限) | |
| MGLRU eviction | v6.6 vmscan.c:4896 sort_folio() | 六种判决(promoted/protected/ineligible…) |
v6.6 vmscan.c:5123 isolate_folios() | 从最老代批量隔离 + 类型/tier 选择 | |
v6.6 vmscan.c:5165 evict_folios() | 批量驱逐主函数(锁窗口极小) | |
v6.6 vmscan.c:5075/5095 | get_tier_idx / get_type_to_scan(统计判定) | |
v6.6 vmscan.c:5353 try_to_shrink_lruvec() | 主循环(get_nr_to_scan↔evict) | |
| MGLRU workingset | v6.6 mm/workingset.c:231 lru_gen_eviction() | 影子 token=(min_seq<<W)|refs |
v6.6 workingset.c:279 lru_gen_refault() | 按 tier 记 refaulted | |
v6.6 vmscan.c:4598-4637 lru_gen_age_node() | min_ttl 防抖 → OOM 压力阀 | |
| 5.10 传统 LRU | v5.10 mm/swap.c:412 mark_page_accessed() | 两位状态机注释与实现 |
v5.10 swap.c:351 activate_page() | 持锁搬链表激活 | |
v5.10 vmscan.c:986 page_check_references() | page_referenced rmap 查岗判决 | |
v5.10 vmscan.c:2009 shrink_active_list() | 一刀切降级(保 VM_EXEC) | |
v5.10 vmscan.c:2202 inactive_is_low() | int_sqrt(10*gb) 魔法数 | |
v5.10 workingset.c:338,373-376 | 一维 refault distance 激活 | |
| XNU | vm_pageout.c:2769 vm_page_balance_inactive() | active FIFO 头降级+NOFLUSH 清位(:2813-2814) |
vm_pageout.c:3357-3368 | inactive 两次机会(pmap_get_refmod) | |
vm_pageout.c:2833 vm_pageout_scan() | 水位驱动主扫描(五阶段 :2950-3057) | |
kern_memorystatus.h:40-62 / task.c:986-993 | JETSAM_PRIORITY band 表 / phys_footprint 权威定义 |
版本边界实证:v5.10/v5.15/v5.19 的 mm/vmscan.c 中 lru_gen 出现 0 次,v6.1 出现 161 次——MGLRU 于 6.1 合入。v6.6 vmscan.c 共 8148 行(v5.10 为 4310 行)。
mm/vmscan.c、mm/workingset.c、include/linux/mmzone.h、include/linux/mm_inline.hDocumentation/admin-guide/mm/multigen_lru.rst(v6.12 版,全文已核)osfmk/vm/vm_pageout.c、vm_compressor.c、bsd/kern/kern_memorystatus.c