背景
可执行文件的页一旦被映射并首次执行,就应尽量留在内存里,否则反复从磁盘重读会引发 IO thrashing。classical LRU 早已据此把映射的可执行文件页当作一等公民,并在首次使用后激活它们,让 exec 代码获得更高的留内存优先级。
MGLRU 的回收逻辑与 classical LRU 不同,对这类 folio 的保护并不完整。可执行代码能否留住,直接影响内存压力下的构建与运行流畅度。
MGLRU 下映射的 executable folio 容易被错误回收
┌──────────────────────────────────────┐
│ lru_gen_look_around 与 walk_mm 可能 │
│ 提前检查并清除 exec folio 的 access │
│ flag │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ folio_update_gen 与 lru_gen_set_refs │
│ 对 exec folio 只设 PG_referenced │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ shrink_folio_list 因此忽略首次使用, │
│ exec 代码被换出引发 IO thrashing │
└──────────────────────────────────────┘
问题
- 回收路径检查引用时,exec folio 的 access flag 可能已被提前清除。
- 引用计数路径对 exec folio 只设
PG_referenced。 shrink_folio_list()因此忽略首次使用,容易把这些 folio 回收。- exec 代码被换出后,下次执行又要重新读盘,引发 IO thrashing。
方案
根因在于 MGLRU 的引用计数路径没有像 classical LRU 那样,在 exec folio 首次使用时主动提升。
修复跟随 classical LRU 的逻辑,在 folio_update_gen() 与 lru_gen_set_refs() 里识别首次使用的映射 executable folio 并直接提升。判定由 is_exec_file_folio() 统一完成,它要求同时满足 VMA 可执行(VMA_EXEC_BIT)与 file folio(folio_is_file_lru())。
2 个函数都改在「首次使用」分支里动手,即 folio 既无 PG_referenced 也非 workingset 时。lru_gen_set_refs() 先判断是否 exec file folio:若是就设 PG_workingset 并返回真,让上层走 FOLIOREF_ACTIVATE 激活路径;否则才设 PG_referenced 返回假:
if (!folio_test_referenced(folio) && !folio_test_workingset(folio)) {
if (is_exec_file_folio(folio, vma_flags)) {
set_mask_bits(&folio->flags.f, LRU_REFS_FLAGS, BIT(PG_workingset));
return true;
}
set_mask_bits(&folio->flags.f, LRU_REFS_MASK, BIT(PG_referenced));
return false;
}
folio_update_gen() 走的是页表扫描(aging)路径,对同样的首次使用 exec file folio 改为跳过「仅设 PG_referenced」的旧分支,直接落到 promote 逻辑把它提升到目标 generation。2 条路径因此都能在首次使用时把 exec 代码留住,而不依赖后续扫描是否还看得到 access flag。为了让 VMA 执行标记能传进引用计数路径,walk_update_folio() 接收 vma 并把 vma->flags 向下传递;前置改动还把 folio_referenced() 及相关逻辑从旧的 vm_flags_t 切换到 vma_flags_t,使整套引用计数都能拿到一致的 VMA 标记。
首次使用检测到 exec file folio 即直接提升
┌────────────────────────────────────────────┐
│ 新增 is_exec_file_folio 判断 VMA_EXEC_BIT │
│ 与 file folio │
└─────────────────────┬──────────────────────┘
▼
┌────────────────────────────────────────────┐
│ folio_update_gen 与 lru_gen_set_refs │
│ 在首次使用时识别 exec file folio │
└─────────────────────┬──────────────────────┘
▼
┌────────────────────────────────────────────┐
│ 命中则跳过仅设 PG_referenced,直接 promote │
│ 到更老 generation 留住 exec 代码 │
└────────────────────────────────────────────┘
收益
作者在 32-core Arm 机器上,将 memcg 限制设为 2G 制造内存压力,用 make -j32 构建内核,观察构建耗时的变化:
| 指标 | Before | After | 改善 |
|---|---|---|---|
kernel build sys time (make -j32) |
9248.543s | 7861.579s | -15.0% |
作者将这一结果表述为 sys time 维度的改善。内存压力下 exec 代码不再被过早换出,减少了重读磁盘的开销,构建耗时随之下降。