Linux Kernel · Memory Management

Linux LRU:active → inactive 的时机与原理
Linux 5.10 vs 6.6 源码对比分析

源码基线:torvalds/linux tags v5.10 / v6.6 · 全部引用为真实文件与行号

Linux LRU:active → inactive 的时机与原理(Linux 5.10 vs 6.6 源码分析)

分析对象:torvalds/linux 标签 v5.10v6.6(源码已下载至本仓库 v5.10/v6.6/ 目录,下文引用均为真实行号)。 结论先行:5.10 和 6.6 在"传统 LRU"(关闭 MGLRU)下,active→inactive 的时机和原理完全一致,只是 API 全面 folio 化;6.6 的本质区别是引入了 MGLRU(多代 LRU),开启后 "active/inactive" 变成"最年轻两代/其余代"的兼容视图,转换时机也随之变成以"代"为单位的批量转换。


1. 基础:active/inactive 到底是什么

1.1 数据结构

每个 memcg × node 有一个 lruvec,内含 5 条双向链表(include/linux/mmzone.h,两版本均为 struct list_head lists[NR_LRU_LISTS],5.10:278 / 6.6:615):

LRU_INACTIVE_ANON   LRU_ACTIVE_ANON
LRU_INACTIVE_FILE   LRU_ACTIVE_FILE
LRU_UNEVICTABLE

一个页在哪条链表上,完全由页标志位 PG_active 决定:

lru = folio_is_file_lru(folio) ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;
if (folio_test_active(folio))
        lru += LRU_ACTIVE;

链表方向(两个版本一致):add_page_to_lru_list()list_add() 挂到头部=最新端;回收扫描 isolate_lru_pages()lru_to_page()(= list_entry(head->prev),include/linux/mm.h:227)从尾部=最老端取页。即:头部是热端(新),尾部是冷端(老),回收从尾部吃

/proc/meminfoActive:/Inactive: 就是对应的 NR_ACTIVE_*/NR_INACTIVE_* 计数之和。

1.2 原理:PG_active × PG_referenced 两位状态机

经典 LRU 用两个页标志位构成"第二次机会"(second chance)状态机。mm/swap.cmark_page_accessed() 的注释把状态机写得很明白(5.10:412-421 / 6.6:448-457,逐字相同):

inactive,unreferenced  ->  inactive,referenced     (第一次访问)
inactive,referenced    ->  active,unreferenced     (第二次访问 → 激活!)
active,unreferenced    ->  active,referenced       (继续访问)

2. 5.10:active → inactive 的全部时机

2.1 主路径:回收时扫描 active 链表 —— shrink_active_list()

mm/vmscan.c:2009-2102,这是绝大多数 active→inactive 的发生地。流程:

static void shrink_active_list(unsigned long nr_to_scan, struct lruvec *lruvec,
                               struct scan_control *sc, enum lru_list lru)
{
        ...
        nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold, ...);  // 从 active 链表尾部隔离

        while (!list_empty(&l_hold)) {
                page = lru_to_page(&l_hold);
                ...
                /* ① 反向映射遍历所有映射该页的 PTE,统计并清除 accessed 位 */
                if (page_referenced(page, 0, sc->target_mem_cgroup, &vm_flags)) {
                        /* ② 只有"被引用过 + 可执行文件的页"才留在 active 链表 */
                        if ((vm_flags & VM_EXEC) && page_is_file_lru(page)) {
                                nr_rotated += thp_nr_pages(page);
                                list_add(&page->lru, &l_active);
                                continue;
                        }
                }
                /* ③ 其余一律降级:清 active、打上 workingset 标记 */
                ClearPageActive(page);      /* we are de-activating */   // v5.10/mm/vmscan.c:2077
                SetPageWorkingset(page);
                list_add(&page->lru, &l_inactive);
        }
        ...
        nr_deactivate = move_pages_to_lru(lruvec, &l_inactive);   // 重新挂回(挂到 inactive 头部=新端)
        __count_vm_events(PGDEACTIVATE, nr_deactivate);           // /proc/vmstat 的 pgdeactivate
}

要点:

  1. 被引用 ≠ 不降级。除了"可执行文件映射"这一特例(让代码段在中等压力下尽量驻留),其他所有被扫描到的 active 页——包括刚被访问过的——都会被降级。因为 page_referenced() 已经把 PTE 的 young 位和 PG_referenced 清掉了,页进入 inactive 后从"干净"状态重新开始二次机会。
  2. 降级时打 PG_workingset 标记(2078),配合 shadow 记录,供将来 refault 时计算 refault distance。
  3. 统计上体现为 /proc/vmstatpgdeactivate(以及 tracepoint mm_vmscan_lru_shrink_active)。

什么时候会去扫描 active 链表?(这才是"时机"的核心)

shrink_node() 在每轮回收前决定本轮是否允许"去激活"(mm/vmscan.c:2695-2719):

        if (!sc->force_deactivate) {
                unsigned long refaults;

                refaults = lruvec_page_state(target_lruvec, WORKINGSET_ACTIVATE_ANON);
                if (refaults != target_lruvec->refaults[0] ||
                    inactive_is_low(target_lruvec, LRU_INACTIVE_ANON))
                        sc->may_deactivate |= DEACTIVATE_ANON;      // 匿名页允许降级
                else
                        sc->may_deactivate &= ~DEACTIVATE_ANON;

                /* When refaults are being observed, it means a new
                 * workingset is being established. Deactivate to get
                 * rid of any stale active pages quickly. */
                refaults = lruvec_page_state(target_lruvec, WORKINGSET_ACTIVATE_FILE);
                if (refaults != target_lruvec->refaults[1] ||
                    inactive_is_low(target_lruvec, LRU_INACTIVE_FILE))
                        sc->may_deactivate |= DEACTIVATE_FILE;      // 文件页允许降级
                ...
        } else
                sc->may_deactivate = DEACTIVATE_ANON | DEACTIVATE_FILE;

满足任一条件才降级:

inactive_is_low()(mm/vmscan.c:2202-2219)实现的是一张经验表(注释在 2174-2201):

        gb = (inactive + active) >> (30 - PAGE_SHIFT);
        if (gb)
                inactive_ratio = int_sqrt(10 * gb);   /* 目标 active:inactive 比例 */
        ...
        return inactive * inactive_ratio < active;
总内存 目标比例(active:inactive) inactive 上限
100MB 1 50MB
1GB 3 250MB
10GB 10 0.9GB
100GB 31 3GB
1TB 101 10GB

配额经 get_scan_count() 分给 4 条链表后,shrink_list()(2160-2172)对 active 链表只做"降级"不做回收:

static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan, ...)
{
        if (is_active_lru(lru)) {
                if (sc->may_deactivate & (1 << is_file_lru(lru)))
                        shrink_active_list(nr_to_scan, lruvec, sc, lru);   // ← 唯一动作:降级
                else
                        sc->skipped_deactivate = 1;
                return 0;
        }
        return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);          // 真正的回收在 inactive
}

另有两个固定的补偿调用点,不依赖 may_deactivate:

2.2 显式路径:用户空间主动"制冷"

系统调用 路径 效果
madvise(MADV_COLD) mm/madvise.c:381(THP)/467(PTE) → 先清 PTE young 位 + ClearPageReferenced,再 deactivate_page(page)(5.10 mm/swap.c:695)→ lru_deactivate_fn(576) active 页直接搬到 inactive(挂头部),清 PG_active/PG_referenced
madvise(MADV_PAGEOUT) mm/madvise.c:385/473reclaim_pages()(mm/vmscan.c:2104,2127 行 ClearPageActive) 清 active 后直接走 shrink_page_list 试着当场回收
posix_fadvise(POSIX_FADV_DONTNEED) 等 invalidate mm/truncate.c:599:invalidate_inode_page() 失败(页被映射,丢不掉)→ deactivate_file_page()(mm/swap.c:666)→ lru_deactivate_file_fn(528) "这页没用了但删不掉"→ 降级加速回收;若是脏页,挂 inactive 头部并置 PG_reclaim(等 flusher 写回后,rotate_reclaimable_page/pagevec_move_tail_fn(swap.c:235-246)再把它推到 inactive 尾部=最快被回收的位置)
madvise(MADV_FREE) lru_lazyfree_fn(mm/swap.c:594) 干净匿名页清除 PG_active、清 PG_swapbacked,转入 inactive file(lazyfree)

注意这些函数都不是立刻操作链表,而是先进 per-CPU 批量缓存(5.10 是 struct pagevec,容量 15,include/linux/pagevec.h:15),攒满或 lru_add_drain() 时才在持有 LRU 锁的情况下统一搬移(pagevec_lru_move_fn,swap.c:206)。

2.3 反方向(inactive → active)一览(用于对照理解)


3. 6.6 与 5.10 的区别

3.1 第一层:API 基础设施全面 folio 化(语义不变)

若 6.6 没有启用 MGLRU(见 3.2 的判定方法),active→inactive 的逻辑与 5.10 逐行等价,只是命名和锁变了:

5.10 6.6 说明
struct page + pagevec(15) struct folio + folio_batch(15) 6.6 mm/swap.c 全部 folio 化;批量结构换名但容量不变
deactivate_page() folio_deactivate()(swap.c:713) MADV_COLD 入口
deactivate_file_page() deactivate_file_folio()(swap.c:690) invalidate 失败入口
activate_page()/__activate_page folio_activate()/folio_activate_fn(swap.c:327) 激活入口
mark_page_accessed() folio_mark_accessed()(swap.c:458) 状态机入口
shrink_page_list() shrink_folio_list() inactive 回收
move_pages_to_lru() move_folios_to_lru()(vmscan.c:2490) 放回链表
page_referenced() folio_referenced() 反查 young 位
shrink_active_list() shrink_active_list()(vmscan.c:2688) 名字没变,逻辑一致:folio_clear_active(folio); /* we are de-activating */(2758)
pgdat->lru_lock lruvec->lru_lock(vmscan.c:2706) 5.11 起 LRU 锁从"每 NODE 一把"细化为"每 lruvec(memcg×node)一把",多 memcg 场景锁竞争大幅下降
lru_note_cost()(vmscan.c:2780) 6.6 在 shrink_active_list 末尾把 rotate 的页计入 anon/file 成本模型(5.15+ 引入),用于 get_scan_count 的成本均衡

shrink_node()may_deactivate 决策、inactive_is_low()get_scan_count() 在 6.6(2940-2964、2886、3053+)与 5.10 基本相同。

3.2 第二层(本质区别):MGLRU——"代"取代"两级链表"

MGLRU(CONFIG_LRU_GEN,6.1 合入)在 6.6 中已存在。是否启用由三层决定:

  1. 编译:CONFIG_LRU_GEN(6.6 的 mm/Kconfig 中默认 n;CONFIG_LRU_GEN_ENABLED(默认 n)决定编译后是否默认开启);
  2. 启动参数:lru_gen=lru_gen_min_ttl_ms=;
  3. 运行时:cat /sys/kernel/mm/lru_gen/enabled(十六进制位掩码,bit0=核心)。

6.6 内核中的判定函数是静态钥匙 lru_gen_enabled()(mm_inline.h:105),Android 6.6 通用内核、Arch/Fedora 等发行版内核普遍 =y 且默认启用;若 /sys/kernel/mm/lru_gen/enabled 为 0,则完全走 3.1 的传统路径。

3.2.1 数据结构:代(generation)替代 active/inactive 双链表

lruvec 中新增(6.6 mmzone.h:416-441):

struct lru_gen_folio {
        unsigned long max_seq;                 /* 最年轻代号(aging 递增) */
        unsigned long min_seq[ANON_AND_FILE];  /* 最老代号(eviction 递增) */
        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];
        ...
};

关键映射——MGLRU 模式下 active/inactive 的定义变了(mmzone.h:331-334 原注释):

"And to maintain ABI compatibility with the active/inactive LRU, e.g., /proc/vmstat, these two generations are considered active; the rest of generations, if they exist, are considered inactive. See lru_gen_is_active()."

static inline bool lru_gen_is_active(struct lruvec *lruvec, int gen)   // mm_inline.h:164
{
        unsigned long max_seq = lruvec->lrugen.max_seq;
        return gen == lru_gen_from_seq(max_seq) || gen == lru_gen_from_seq(max_seq - 1);
}

即:最年轻的 2 代 = active,更老的 1~2 代 = inactive/proc/meminfo 的 Active/Inactive 在 MGLRU 下是这个兼容视图的产物。

3.2.2 页面的生命周期(与 5.10 对照)

入队(lru_gen_add_folio,mm_inline.h:220-264):

        if (folio_test_active(folio))
                seq = lrugen->max_seq;               /* ① 热:最年轻代(active) */
        else if ((type == LRU_GEN_ANON && !folio_test_swapcache(folio)) ||
                 (folio_test_reclaim(folio) && (dirty||writeback)))
                seq = lrugen->min_seq[type] + 1;     /* ② 冷但暂时逐不出:次老代 */
        else
                seq = lrugen->min_seq[type];         /* ③ 其余:最老代(inactive) */

谁会被打上 PG_active 进最年轻代?folio_add_lru()(swap.c:510-512):处于缺页路径(lru_gen_in_fault(),由 mm/memory.c:5197 在 fault 中设置)且非 PF_MEMALLOC 的新页。这与 5.10"新页先进 inactive"不同——MGLRU 下缺页进来的页直接进最年轻代(active),非缺页路径(如预读)的干净页则从最老代(inactive)起步

访问计数(folio_mark_accessed(),swap.c:458-463):

void folio_mark_accessed(struct folio *folio)
{
        if (lru_gen_enabled()) {
                folio_inc_refs(folio);   /* 不再搬链表!只累加引用层级 */
                return;
        }
        ... /* 传统状态机 */
}

MGLRU 模式下,经典两位状态机被 folio_inc_refs()(swap.c:415-441)替代:PG_referenced → PG_workingset → LRU_REFS 计数位,最多 4 个层级(tier)。read()/write() 这类文件描述符访问不再触发 inactive→active 搬移,只在 folio->flags 上原子累加;层级决定该页被逐出时的保护程度(sort_folio() 的 tier 判断)。

通过页表的访问则由两条路径收割:

3.2.3 MGLRU 下 active → inactive 的时机

(a) 核心:aging —— inc_max_seq() 整代批量转换

回收压力下(lru_gen_shrink_lruvec/lru_gen_shrink_node 判定需要 aging)执行 try_to_inc_max_seq()(vmscan.c:4495):先用 walk_mm() 扫描进程页表,清 young 位、把被访问的页提升到最年轻代(folio_update_gen(),3773;物理搬移延后到 sort_folio() 的 "promoted" 分支,4930-4933),然后 inc_max_seq()(4438)让 max_seq++ 创建新的最年轻代。

inc_max_seq() 中(4462-4483)就是 MGLRU 版的"active 变 inactive":

        /*
         * Update the active/inactive LRU sizes for compatibility. ...
         */
        prev = lru_gen_from_seq(lrugen->max_seq - 1);   /* 即将掉出活跃窗口的代 */
        next = lru_gen_from_seq(lrugen->max_seq + 1);   /* 新生的最年轻代 */

        for (type = 0; type < ANON_AND_FILE; type++) {
                for (zone = 0; zone < MAX_NR_ZONES; zone++) {
                        long delta = lrugen->nr_pages[prev][type][zone] -
                                     lrugen->nr_pages[next][type][zone];
                        if (!delta)
                                continue;
                        __update_lru_size(lruvec, lru, zone, delta);            /* Inactive += delta */
                        __update_lru_size(lruvec, lru + LRU_ACTIVE, zone, -delta); /* Active   -= delta */
                }
        }

max_seq 前进后,活跃窗口 {max_seq, max_seq-1} 整体右移,刚掉出窗口的那一代(prev,原 max_seq-1)连同其全部页,一次性从 Active 统计转入 Inactive 统计——页本身不动、不摘链,纯记账。这就是 MGLRU 下"active 变 inactive"的批量时机:它不再按页发生,而是按代发生。这些页随后随 min_seq 推进成为逐出候选。

(b) 单页显式降级:MADV_COLD

folio_deactivate()(swap.c:713)在 MGLRU 模式下条件变为 folio_test_active(folio) || lru_gen_enabled()(6.6,MGLRU 页 PG_active 恒 0,故专门放行);lru_deactivate_fn(602-616)做"摘下 → 清 active/referenced → 重新入队",入队时因 PG_active=0 落到最老代(min_seq),若统计上从活跃代来,则经 lru_gen_update_size()(mm_inline.h:174-218,注释:"demotion requires isolation, e.g., lru_deactivate_fn()")完成 Active→Inactive 的单页转换。

(c) 逐出侧的推进:try_to_inc_min_seq()(4395)只在老代链表已空时推进 min_seq;inc_min_seq()(4359,在窗口达到 4 代时被 inc_max_seq 强制调用)把最老代剩余页搬到次老代——注意这发生在 inactive 侧,不产生 active→inactive 转换。

(d) 逐出中被"拒绝"的页反而会升回 active:evict_folios()(5165)对 shrink_folio_list 未能释放且满足 active/referenced/mapped/locked/dirty/writeback 的页设置 BIT(PG_active)(5213-5220 注释:"don't add rejected folios to the oldest generation"),重新入队时回到最年轻代。而高访问层级(tier)的页在 sort_folio()(4935-4945)被 folio_inc_gen() 上移一代作为保护。

3.2.4 传统路径在 MGLRU 开启后成为死代码

分流点(6.6):

即 MGLRU 开启时,shrink_active_list()shrink_inactive_list()get_scan_count() 的双链表配额、may_deactivate 决策等 5.10 时代的整条 active 链路不再执行(其中 workingset_refault 也分流到 lru_gen_refault(),workingset.c:517-520)。


4. 两个版本对比总结

维度 5.10 6.6(未开 MGLRU) 6.6(开启 MGLRU)
数据结构 lruvec 5 条链表 + PG_active 同左(folio 化) lrugen->folios[gen][type][zone],代号编码进 folio->flags
active 的含义 PG_active=1 的页在 ACTIVE_* 链表 同左 最年轻两代 {max_seq, max_seq-1}
新页去向 一律 inactive 头部(lru_cache_add) 同左 缺页→最年轻代;预读/冷文件→最老代
降级时机(回收) shrink_active_list():逐页 ClearPageActive,受 may_deactivate/refault/inactive_is_low 门控 同左(锁换成 lruvec 级) inc_max_seq():整代 Active→Inactive 记账转换
降级时机(显式) MADV_COLD→deactivate_page();fadvise/invalidate 失败→deactivate_file_page();MADV_PAGEOUT→reclaim_pages() 同左(folio 化) MADV_COLD→folio_deactivate() 落最老代;其余入口同样兼容
访问状态机 PG_active×PG_referenced 二次访问激活 同左 folio_inc_refs() tier 计数,不搬链表;页表访问由 aging walk/look_around 收割
kswapd 周期性老化 age_active_anon()(有 swap 时) 同左 aging walk(try_to_inc_max_seq)取代
批量缓存 per-CPU pagevec(15) per-CPU folio_batch(15) 同左
LRU 锁 pgdat->lru_lock(每 NODE) lruvec->lru_lock(每 memcg×node) 同左
观测 /proc/vmstat 的 pgdeactivate/pgactivate 同左 同左(兼容计数);另有 /sys/kernel/mm/lru_gen/*

5. 观测与验证手段

# 链表存量
grep -E '^(Active|Inactive)' /proc/meminfo

# 转换速率(5.10/6.6 通用;MGLRU 下同样维护)
grep -E 'pgactivate|pgdeactivate|pgrefill' /proc/vmstat

# 每 memcg
grep -E '^(active|inactive)_(anon|file)' /sys/fs/cgroup/<path>/memory.stat

# tracepoint(传统路径降级)
perf record -e mm:vmscan_lru_shrink_active -a

# MGLRU 状态(6.6)
cat /sys/kernel/mm/lru_gen/enabled     # 0 = 传统路径,非 0 = MGLRU

演示(任一版本):

# 让一批 active 页降温
madvise(addr, len, MADV_COLD);   # pgdeactivate 立即/批量 drain 后 +N

附:本次分析引用的源码文件

文件 v5.10 v6.6
mm/vmscan.c 4310 行 8148 行(MGLRU 约 3700-6300 行)
mm/swap.c 1215 行 1104 行
mm/workingset.c 629 行 814 行
mm/madvise.c 1241 行 1534 行
mm/truncate.c 955 行 (invalidate 路径)
mm/rmap.c 2570 行(look_around 调用点)
include/linux/mmzone.h / mm_inline.h / page-flags-layout.h / pagevec.h / mm.h