背景

FOLL_LONGTERM 的 pin 要求目标 folio 允许被长期钉住。当区间里出现不允许的 folio(如可移动区或 CMA 页)时,check_and_migrate_movable_*() 会先用 collect_longterm_unpinnable_folios() 把它们收集起来迁移走,再重新 pin。

收集时要执行 folio_isolate_lru() 把 folio 从 LRU 上摘下来。per-CPU LRU cache(folio batch)可能还持有这些 folio 的引用,不清理会让隔离与后续迁移变得不可靠。于是代码在隔离前做一道引用比对:folio_ref_count() 应当恰好等于 folio_expected_ref_count() 加上本次 pin 自己持有的那一份引用,不符就说明有残留,先 lru_add_drain() 排空本 CPU 的 cache,仍不符再 lru_add_drain_all() 排空所有 CPU。drain 的定位是兜底,只有真出事才该触发。

drain 本是隔离 LRU 前的兜底
┌────────────────────────────────────────┐
│   收集不可长期 pin 的 folio 准备迁移   │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ per-CPU LRU cache 可能残留 folio 引    │
│ 用                                     │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ 引用计数不符才先 drain 本 CPU 再 drain │
│ 所有 CPU                               │
└────────────────────────────────────────┘

问题

  • 引用比对按 +1 预期,small folio 的 pin 实际加 1024
  • 只有 small folio 可能进 per-CPU LRU cache,每次必误判引用不符
  • 每次进入该路径都先 lru_add_drain() 再 lru_add_drain_all()
  • lru_add_drain_all() 在每个在线 CPU 调度执行,全局开销大

方案

关键在于 FOLL_PIN 的计数规则有两条路径。

large folio 有独立的 _pincount 字段,pin 一次记 1;small folio 没有,pin 一次直接在 refcount 上加 GUP_PIN_COUNTING_BIAS(1024)。而旧比对无论哪种情况都按 +1 预期,于是 small folio 永远对不上。更巧的是 per-CPU LRU cache 只缓存 small folio(folio_may_be_lru_cached() 不认 large folio),唯一能进入这道比对的类型恰恰必然误判,同一个 folio 连续触发两级 drain。

修复就是把比对基准改成 pin 的真实增量:

    const int pin_refs = folio_has_pincount(folio) ? 1 : GUP_PIN_COUNTING_BIAS;

    if (folio_ref_count(folio) != folio_expected_ref_count(folio) + pin_refs)
比对基准补上 pin 的真实增量
┌───────────────────────────────────────┐
│ small folio 每次 pin 引用计数加 1024  │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐  每次进入该路径都先 drain 本 CPU 再 drain
│    旧代码按加 1 预期 比对必然不符     │  所有 CPU
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 修正后无残留引用时比对相符 不再 drain │
└───────────────────────────────────────┘

改后语义回到设计初衷:per-CPU LRU cache 里没有该 folio 的残留引用时比对恰好相等,两级 drain 都跳过;真有残留才先排本 CPU,仍不符再排所有 CPU。

收益

作者未提供性能数据,从代码逻辑推断的预期收益:

  • FOLL_LONGTERM pin 收集不可长期 pin 的 folio 时,不再必然触发全 CPU 的 LRU cache drain
  • 消除 lru_add_drain_all() 在每个在线 CPU 上的调度执行与缓存扰动
  • drain 回归按需兜底定位,仅 per-CPU LRU cache 真残留引用时才触发