背景
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 真残留引用时才触发