Linux LRU:active → inactive 的时机与原理(Linux 5.10 vs 6.6 源码分析)
分析对象:
torvalds/linux标签v5.10与v6.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 决定:
- 5.10
include/linux/mm_inline.h:115 page_lru() - 6.6
include/linux/mm_inline.h:86 folio_lru_list()
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/meminfo 的 Active:/Inactive: 就是对应的 NR_ACTIVE_*/NR_INACTIVE_* 计数之和。
1.2 原理:PG_active × PG_referenced 两位状态机
经典 LRU 用两个页标志位构成"第二次机会"(second chance)状态机。mm/swap.c 中 mark_page_accessed() 的注释把状态机写得很明白(5.10:412-421 / 6.6:448-457,逐字相同):
inactive,unreferenced -> inactive,referenced (第一次访问)
inactive,referenced -> active,unreferenced (第二次访问 → 激活!)
active,unreferenced -> active,referenced (继续访问)
- inactive → active(升):
mark_page_accessed()检测到"短时间内第二次访问"(PG_referenced 已置位)→activate_page()/folio_activate()(mm/swap.c5.10:351 / 6.6:352),__activate_fn做"从 inactive 摘下 → SetPageActive → 挂到 active 头部",计数PGACTIVATE。另有两个激活入口:缺页重算 refault(workingset_refault(),mm/workingset.c5.10:376SetPageActive/ 6.6:545folio_set_active)和回收中不可释放页的重激活(shrink_page_list的activate_locked标签)。 - active → inactive(降,本文主题):没有"自动老化定时器"。一个 active 页只有两种下场:要么在回收扫描 active 链表时被统一降级,要么被显式去激活(madvise 等)。详见下节。
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
}
要点:
- 被引用 ≠ 不降级。除了"可执行文件映射"这一特例(让代码段在中等压力下尽量驻留),其他所有被扫描到的 active 页——包括刚被访问过的——都会被降级。因为
page_referenced()已经把 PTE 的 young 位和 PG_referenced 清掉了,页进入 inactive 后从"干净"状态重新开始二次机会。 - 降级时打
PG_workingset标记(2078),配合 shadow 记录,供将来 refault 时计算 refault distance。 - 统计上体现为
/proc/vmstat的pgdeactivate(以及 tracepointmm_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;
即满足任一条件才降级:
- 观察到 refault 在增长(正在建立新的工作集 → 尽快把陈旧的 active 页挤下去);
inactive_is_low()(inactive 占比低于目标值);- 直接回收压力极大(
force_deactivate,如 5.10:6895 一带的may_enter_fs/优先级判断,优先级打满时强开)。
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:
shrink_lruvec()尾部(2530-2537):有 swap 且 inactive anon 偏低时,"即使本轮不回收匿名页,也要平衡 anon 的 active/inactive 比例"——shrink_active_list(SWAP_CLUSTER_MAX, ..., LRU_ACTIVE_ANON)。age_active_anon()(3357-3377):kswapd 主循环(balance_pgdat,3651 调用)在准备睡眠前,遍历所有 memcg 周期性地老化 active anon——"kswapd 定期给 active anon 降温"。
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/473 → reclaim_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)一览(用于对照理解)
mark_page_accessed()二次访问(上述状态机);workingset_refault():被逐出的页重新缺页进来,若 refault distance ≤ 工作集大小,说明逐出是误伤 →SetPageActive直接进 active(workingset.c:376);shrink_page_list()中:映射无法解除、脏页不允许写、swap 满了等"回收失败"的页 →goto activate_locked→ 重新激活。
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 中已存在。是否启用由三层决定:
- 编译:
CONFIG_LRU_GEN(6.6 的mm/Kconfig中默认 n;CONFIG_LRU_GEN_ENABLED(默认 n)决定编译后是否默认开启); - 启动参数:
lru_gen=、lru_gen_min_ttl_ms=; - 运行时:
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];
...
};
- 代号窗口
[min_seq, max_seq],大小在[MIN_NR_GENS=2, MAX_NR_GENS=4](mmzone.h:346-347)之间滑动; - 页所属的"代"直接编码进
folio->flags的LRU_GEN_MASK位段(mmzone.h:376),PG_referenced/PG_workingset 及其上若干位也被复用为访问计数LRU_REFS_MASK; - PG_active 在 MGLRU 模式下、页挂在链表上时恒为 0;只有隔离瞬间(如迁移)才按需恢复(
lru_gen_del_folio(),mm_inline.h:278)。
关键映射——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 判断)。
通过页表的访问则由两条路径收割:
- rmap 反查时发现 young PTE →
lru_gen_look_around()(vmscan.c:4650,由mm/rmap.c:826调用):顺手扫描相邻 PTE,对 young 页folio_activate()→ 重新入队时进最年轻代; - 回收线程的主动老化 walk(见下)。
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):
shrink_lruvec()(vmscan.c:6284):if (lru_gen_enabled() && !root_reclaim(sc)) { lru_gen_shrink_lruvec(...); return; }shrink_node()(6525):if (lru_gen_enabled() && root_reclaim(sc)) lru_gen_shrink_node(...);
即 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 | ✓ | ✓ |