内存管理-77-kernel-internals系列-Memory专题-Caching & Reclaim-3-MGLRU


一、MGLRU: Multi-Generation LRU 翻译

注: 翻译自 https://kernel-internals.org/mm/mglru/

一种用于更佳工作集检测的多代页面回收算法


1. 传统 LRU 的问题

传统的 LRU(最近最少使用)页面回收机制在每个内存区域(Zone)中只使用两个链表:活跃(active)和非活跃(inactive)。页面根据访问情况在它们之间迁移。其根本缺陷在于:

• 扫描成本(The scan cost):为了寻找可回收的页面,回收器必须遍历非活跃链表。如果非活跃链表中充满了热页(即假阴性/false negatives),就会白白浪费大量的 CPU 时间。
• 一次性访问污染(One-time access pollution):大型的 dd 或 tar 操作会把每个文件仅读取一次,从而导致冷页面大量涌入并冲刷、破坏活跃链表。
• 缺乏时间局部性(No temporal locality):传统 LRU 对 1 秒前被访问过的页面与 1 年前被访问过的页面一视同仁。


2. MGLRU 的多代模型

MGLRU(Multi-Generation LRU,多代 LRU,于 Linux 6.1 合并)将页面划分为不同的**“代”(generations)**。每一代代表在大致相同的时间段内变热的页面。最老的一代会被优先回收:

• 第 0 代(最老):自最早的时间戳以来就未被访问过的页面。
• 第 1 代:在倒数第二老的时间段内被访问过的页面。
• ……
• 第 N 代(最新):最近被访问过的页面。

• 回收机制:先从第 0 代进行回收 → 如果第 0 代空了,再回收第 1 代 → ……
• 老化机制:页面一旦被访问,就会被提升(Promote)到最新的那一代中。

典型代数:通常为 4 代。每一代大约代表几秒钟到几分钟的活跃时间。


3. 核心数据结构

/* mm/vmscan.c (6.1+) */
/* Per-node, per-type (anon/file) LRU state */
struct lru_gen_folio {
    /* Min/max generation counters */
    unsigned long max_seq;                /* newest generation */
    unsigned long min_seq[ANON_AND_FILE]; /* oldest non-empty generation */

    /* Per-generation page counts */
    long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];

    /* Bloom filter for faster refault detection */
    unsigned long *filters[NR_BLOOM_FILTERS];
    atomic_long_t  nr_evicted[ANON_AND_FILE];
    atomic_long_t  nr_refaulted[ANON_AND_FILE];
    atomic_long_t  protected[NR_HIST_GENS][ANON_AND_FILE][NR_BIRTH_MARKS];
};

/* 追踪每个 folio(内存页)所属代数的信息,直接内嵌在 folio 的 flags 标志位中。gen = folio_lru_gen(folio): 表示该 folio 当前正处于哪一代 */


4. 老化:扫描热页 (Aging: scanning for hot pages)

老化机制通过遍历页表(Page tables)来寻找最近被访问过的页面,并将它们提升到最新的那一代:

/* mm/vmscan.c */
static bool walk_mm(struct lruvec *lruvec, struct mm_struct *mm, struct lru_gen_walk_control *lwc)
{
    struct lru_gen_mm_walk walk = {
        .lruvec = lruvec,
        .seq    = min_seq(lruvec, ANON_AND_FILE),
        /* ... */
    };

    /* 遍历所有的 VMA,检查 PTE 的访问位(accessed bits) */
    walk_page_range(mm, 0, ULONG_MAX, &lru_gen_mm_walk_ops, &walk);
    return walk.force_scan;
}

页表项(PTE)中的**访问位(accessed bit)**会明确告诉内核,自上一次老化轮次以来有哪些页面被触碰过。被设置了访问位的页面会被提升到最新的那一代####;而没有被设置访问位的页面则留在老一代中(这意味着它们会更早被回收)。


5. 回收:清理老一代页面

回收机制直接从最老的一代中释放页面:

/* mm/vmscan.c */
static long evict_folios(struct lruvec *lruvec, struct scan_control *sc, swp_entry_t *swpent)
{
    int type = get_type_to_scan(lruvec, sc, &tier);
    long nr_to_scan = get_nr_to_scan(lruvec, sc, can_age, type);
    long nr_reclaimed = 0;

    /* 获取最老的一代 */
    unsigned long min_seq = READ_ONCE(lrugen->min_seq[type]);

    /* 从最老的一代中收集(隔离)folios */
    isolate_folios(lruvec, sc, type, min_seq, &list);

    /* 尝试回收它们(通过回写磁盘、换出到 swap,或者直接释放物理内存) */
    nr_reclaimed = shrink_folio_list(&list, pgdat, sc, &stat, false);

    /* 如果当前最老的一代已经被回收空了,则推进 min_seq */
    if (!nr_pages_in_gen(lruvec, type, min_seq))
        WRITE_ONCE(lrugen->min_seq[type], min_seq + 1);

    return nr_reclaimed;
}


6. 工作集保护:二次装入检测

MGLRU 可以检测出被回收的页面是否被立刻重新换入(即二次装入/refault),这通常意味着系统的工作集(Working set)被破坏了。它利用了 **Bloom filter** 来实现这一点:
(1) 当一个页面被回收时,它的指纹(fingerprint)会被加入到Bloom过滤器中。
(2) 当一个页面发生缺页中断并被重新加载时,MGLRU 会检查它最近是否被回收过。
(3) 如果是:该页面会被放入一个**受保护的代(protected generation)**中——虽然比最新的代要老,但会免受被立即回收的命运。

/* 二次装入检测 */
static bool lru_gen_test_recent(struct lruvec *lruvec, bool file, struct folio *folio, ...)
{
    struct lru_gen_folio *lrugen = &lruvec->lrugen;
    /* 检查布隆过滤器:该 folio 最近是否被回收过? */
    return test_bloom_filter(lrugen, lrugen->min_seq[file], folio);
}


7. 对比:传统 LRU vs MGLRU

------------------------------------------------------------------------------------------------------------------
特性维度(Aspect)	传统 LRU (Classic LRU)						MGLRU
------------------------------------------------------------------------------------------------------------------
数据结构			活跃(active)+ 非活跃(inactive)链表		    4 个世代的链表
回收目标			扫描非活跃链表								清理最老的一代
访问追踪			在活跃与非活跃链表间提升/降级				    扫描页表项(PTE)的访问位
工作集检测		二次装入距离近似估算法						    每一代配备布隆过滤器
流式顺序读取		会污染活跃链表								一次性访问的页面会留存老世代中被迅速释放
内存开销			每个 folio 占用 2 个 list_head 指针			世代计数器 + 布隆过滤器
------------------------------------------------------------------------------------------------------------------


8. 相关配置

# MGLRU 在内核 6.1+ 中默认处于启用状态
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007  ← 位掩码:1=开启主控,2=启用mglru,4=启用老化

# 启用/禁用相关组件
echo 0 > /sys/kernel/mm/lru_gen/enabled       # 彻底禁用 MGLRU(回退使用传统 LRU)
echo 0x0007 > /sys/kernel/mm/lru_gen/enabled  # 启用全部 MGLRU 特性

# 每一代在回收前的最小生存时间(默认值为 0)
cat /sys/kernel/mm/lru_gen/min_ttl_ms
# 0  ← 不设定最小生存时间限制

# 设置某一代在被回收前必须在内存中留存的最短时间
echo 1000 > /sys/kernel/mm/lru_gen/min_ttl_ms  # 保护页面至少在 1 秒内不被释放

# 代数的数量由 MAX_NR_GENS 宏决定(编译时确定,通常默认为 4)


9. 观测 MGLRU

# 查看每个节点/内存控制组(memcg)/内存区域(zone)的世代分布情况
cat /sys/kernel/debug/lru_gen

# 输出格式示例:
# memcg    N    lruvec    aged anon  0  1  2  3
#                         evicted anon 0 1 2 3
#                         aged file  0  1  2  3
#                         evicted file 0 1 2 3

# 查看内存压力统计信息
cat /proc/vmstat | grep -E "pgpromote|pgdemote|lru_gen" # 实测只有 pgdemote_kswapd  pgdemote_direct 两个字段
# pgpromote_success 12345    ← 页面被成功提升到更新一代的次数
# pgdemote_kswapd 67890      ← 页面发生降级(由于老化移入更老世代)的次数

# 查看二次装入率(即工作集被破坏侵占的频次)
cat /proc/vmstat | grep pgrefault
# pgrefault 1234

# 使用 perf 工具追踪追踪 LRU 的隔离事件
perf stat -e vmscan:mm_vmscan_lru_isolate -a sleep 5


10. 延伸阅读 (Further reading)

• Page Reclaim —— 页面回收的整体框架
• Reclaim Throttling —— 内存压力状态下的节流信号机制
• Swap —— 匿名页面被回收后换出的去向
• Page Cache —— 文件后备页面的生命周期管理
• 内核源码树中的 mm/vmscan.c —— MGLRU 的具体底层代码实现
• 内核源码树中的 Documentation/admin-guide/mm/multigen_lru.rst —— 官方多代 LRU 使用指南与参考文档。

 

posted on 2026-10-08 15:13  Hello-World3  阅读(2)  评论(0)    收藏  举报

导航