背景

page fault 的匿名页重用路径上有大量冗余的 lru_add_drain() 调用。这些 drain 的目的是把 per-CPU pagevec 里的页刷到 LRU 上,从而让 refcount 降下来、判断 folio 是否独占。但很多时候 drain 根本不会改变 refcount(folio 不在 lru_cache 里、或不在 swapcache 里),纯属浪费。

┌─────────────────────────────────────────┐
│ page fault 重用路径多次调 lru_add_drain │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│   drain 在 refcount 不会变时纯属浪费    │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐  内存压力下 page fault 高频 lru_add_drain
│ do_swap_page 无条件 drain 在 swapcache  │  叠加开销
│ 统一后已冗余                            │
└─────────────────────────────────────────┘

更关键的是:Kairui 的 swap table phase II 系列让 SYNC I/O swapin 也走 swapcache,和 ASYNC I/O 统一路径。这让 do_swap_page 里的 refcount==1 检查变得不可达(swap-in 的 folio 至少有 base + swapcache 2 个引用),而无条件 lru_add_drain 也完全失去了意义。

问题

  • wp_can_reuse_anon_folio 无条件 drain 试图降 refcount
  • drain 在 folio 不在 lru_cache 或不在 swapcache 时不会改变 refcount
  • do_swap_page 的 stale refcount==1 检查在 swapcache 统一后不可达
  • do_swap_page 的无条件 lru_add_drain 在 swapcache 统一后已冗余
  • 内存压力下 page fault 高频,冗余 drain 叠加开销

方案

这些冗余 drain 分 2 类,各有不同的成因和修法。

第 1 类在 wp_can_reuse_anon_folio。这里 drain 的目的是把 per-CPU pagevec 里的引用刷下去、让 refcount 降到能判独占的程度。但 drain 能不能降 refcount,取决于 folio 当前的状态:如果 folio 不在 lru_cache 里、也不在 swapcache 里,drain 不会改变 refcount,纯属浪费。修法是在函数顶部记录 in_lru_cache 与 in_swapcache 2 个状态,把 refcount 检查改为 folio_ref_count > 1 + in_lru_cache + in_swapcache,只在 drain 真能降 refcount 时才 drain。

第 2 类在 do_swap_page,是 swap table phase II 系列(把 SYNC I/O swapin 也统一进 swapcache)带来的直接后果。统一之后,写 swap-in folio 的 refcount 至少是 base + swapcache = 2,不会出现 refcount==1 的情况。这意味着原先判断"刚分配、尚未进 swapcache"的 refcount==1 检查变成了死代码,而每次写 swap-in page fault 无条件做的 lru_add_drain 也完全失去了意义(drain 降不了 refcount,因为 folio 的引用来自 page table 和 swapcache,不在 pagevec 里)。整段删除,包括 stale 检查和无条件 drain。

swapcache 的释放判断也随之简化。原先用 refcount 推断是否该释放 swapcache(extra_refs 参数),但 commit 4b34f1d8 之后 do_wp_page 已经处理了 non-exclusive 情况(reuse 或 CoW),do_swap_page 不再需要用 refcount 做这个判断。改为 should_try_to_free_swap 用 FAULT_FLAG_WRITE 加 exclusive hint 直接决定,干净利落。

旧:多次无条件 drain
┌─────────────────────────────────────────────┐
│    wp_can_reuse 无条件 drain 降 refcount    │
└──────────────────────┬──────────────────────┘
                       ▼
┌─────────────────────────────────────────────┐
│ do_swap_page 无条件 drain 加 stale refcount │
│ 检查                                        │
└──────────────────────┬──────────────────────┘
                       ▼
┌─────────────────────────────────────────────┐
│      refcount 驱动 swapcache 释放判断       │
└─────────────────────────────────────────────┘

新:按需 drain 加 exclusive hint
┌────────────────────────────────────────┐
│ wp 检查 lru 与 swapcache 再决定 drain  │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ do_swap_page 删除无条件 drain 与 stale │
│ 检查                                   │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│   swapcache 释放改用 exclusive hint    │
└────────────────────────────────────────┘

收益

平台:zRAM swap + 20 线程 kernel build in memcg。作者提供 lru_add_drain 调用次数(behavioral counter,非 wall/sys time):

memcg 限制 Before After 减少
1 GB 276,278 226,318 -18%
800 MB 778,950 541,149 -30.5%

作者未提供 wall/sys time 数据,仅以 drain count 为准。内存压力越大(800M vs 1G),page fault 越频繁,冗余 drain 被消除的比例越高(-30.5% vs -18%)。Ubuntu boot 观察到 5000+ 次冗余 local LRU drain;1G memcg minimal kernel build 跳过 43000+ 次。