lru_add 批量
per-CPU 暂存 folio
攒够或需要时 drain
drain 时进入 LRU
从添加到排空
__folio_batch_add_and_move()
folio_get() 持有批量引用
lru_add_drain_cpu()
folio_batch_move_lru()
批量里的死 folio
refcount 只剩批量这 1 份
已经没人使用
迟早要释放
却仍先被排进 LRU
关键
死 folio 仍会被排进 LRU
随后由 folios_put_refs 释放
旧路径 · 先入 LRU
遍历 lru_add 批量
加锁 folio_lruvec_relock
lru_add()
锁内加入
lruvec_add_folio()
trace_mm_lru_insertion
同一 lruvec 复用锁
folio_set_lru()
循环后释放
folios_put()
folios_put_refs()
refcount 减到 0
__page_cache_release()
代价
加入 LRU 后马上又移除
两次 lruvec 锁
死 folio · 只剩批量引用
batch 自己 folio_get
别的引用都已消失
refcount == 1
判定 dead
folio_ref_freeze(folio, 1)
从 1 冻结到 0
判定为 dead
引用计数已归零
旧路径仍入 LRU
不检查 refcount
照样加入 LRU
folios_put_refs 减到 0
再从 LRU 摘下
问题
加入只为移除
两次锁都白花
代价 · 两次锁
加入 LRU:lruvec 锁
移除 LRU:又一次
死 folio 白走两趟
锁在哪里
lru_add 批量持有引用
加入时拿锁
释放时再拿锁
竞争集中在 lruvec
生产观测
Meta 生产环境
约 24% lruvec 锁竞争
发生在 drain 时
死 folio 就在批量里
关键
死 folio 越多,白锁越多
先筛掉就能省
方案 · 冻结筛出
drain 前先判断
只留批量引用的 folio
移出 add 批量
冻结判定
folio_ref_freeze(folio, 1)
成功:只有批量引用
失败:还有别人用
失败就照常加 LRU
临时批量
free_fbatch 暂存
add 批量槽位置 NULL
循环结束再释放
不进 LRU
关键
冻结把 1 变 0
成功即确认没人再用
绕过清理 · 清标志
不走 lru_add 常规清理
PG_active 可能还在
PG_unevictable 可能也在
手动清标志
__folio_clear_active()
__folio_clear_unevictable()
释放前清掉
否则脏标志回分配器
deferred split
大 folio 可能挂在
deferred split 链表
folio_unqueue_deferred_split()
避免悬空表项
细节
过滤路径绕过 lru_add
三个清理都要做
释放 · 先 uncharge 再 free
mem_cgroup_uncharge_folios()
free_unref_folios()
放在循环之后
先解除计费
死 folio 可能仍计费
先 uncharge
再归还页面
顺序不能反
原批量收尾
原 fbatch 槽位是 NULL
folios_put_refs() 跳过
不会重复减引用
批量照常复用
收尾
free_fbatch 单独释放
add 批量只清空
结果 · 生产 A/B
Instagram 生产 A/B
60 hosts/侧 / 3 × 60s
95% CPU / classic LRU
死 folio 与锁
dead/min 1,297,785 → 14
total/min 19,341,986 → 19,039,996
dead 占比 6.71% → 0.0001%
约 2.6M 次锁 / 分钟 / 主机
系统指标
direct reclaim -7%
alloc stall -5.2%
compaction stall -12.3%
page frees -4.9%
边界
requests/sec 与 p99 无回归
85%→95% 方向性改善
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x