背景

可执行文件的页一旦被映射并首次执行,就应尽量留在内存里,否则反复从磁盘重读会引发 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 代码不再被过早换出,减少了重读磁盘的开销,构建耗时随之下降。