背景
mempool_alloc 的策略是先以不触发直接回收的 GFP 标志(无 __GFP_DIRECT_RECLAIM)调用底层分配器,拿不到再加重 GFP 重试。SLUB 的 sheaves 引入后,__pcs_replace_empty_main 在 sheaf 空了时会判断能否阻塞(gfpflags_allow_blocking):如果允许阻塞就 refill sheaf 再继续快路径,不允许就直接回退慢路径。
┌────────────────────────────────────┐
│ 内存池分配先以不可阻塞的标志调底层 │
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐
│ refill 空链的判断看是否允许阻塞 │ 不可阻塞就不 refill,快路径拿不到对象
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐
│ 回退慢路径,吞吐回归 │
└────────────────────────────────────┘
问题在于这两个判断口径对不上。mempool_alloc 给 slab 的 GFP 不含 __GFP_DIRECT_RECLAIM,因而不被 gfpflags_allow_blocking 视为"允许阻塞"。于是 sheaf 空了也不 refill,alloc_from_pcs() 快路径拿不到对象,直接回退到 ___slab_alloc() 慢路径。在 ublk null target benchmark 上观测到的回归正源于此。
问题
- 内存池分配给底层分配器的标志不含直接回收
- refill 空 sheaf 的判断看是否允许阻塞,对不上
- 不可阻塞就不 refill,快路径拿不到对象
- 回退慢路径,吞吐回归
方案
核心是把 sheaf refill 的判断条件从"是否允许阻塞"放宽到"是否允许自旋"。
改为 gfpflags_allow_spinning():只要还允许自旋(持锁),就 refill sheaf。
旧:不可阻塞就不 refill
┌──────────────────────────────┐
│ 不可阻塞标志不 refill 空链 │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 快路径拿不到对象,回退慢路径 │
└──────────────────────────────┘
新:不可阻塞也能 refill
┌──────────────────────────────────────┐
│ 改看是否允许自旋 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 不可阻塞时仍 refill 空链,快路径恢复 │ refill 本身不阻塞,原条件过严
└──────────────────────────────────────┘
这个改法之所以安全,是因为 sheaf 的 refill 本身不阻塞:它只是从 pc 列表或 partial slab 搬对象进 sheaf,不需要睡眠。原来用 gfpflags_allow_blocking 是"unnecessarily restrictive"(过严)。放宽后,mempool_alloc 这种"先以轻量 GFP 试一次"的调用者也能走快路径。
作者诚实指出此 patch 不完全解决回归:另一个组件是 memoryless nodes(sheaves 在无内存节点上失效),另行处理。
收益
作者未提供量化 benchmark。从机制与回归动机推断的预期收益:
- mempool_alloc 等以轻量 GFP 调用 slab 的路径恢复 sheaf 快路径
- ublk null target 等经 mempool_alloc 的 I/O 工作负载不再因 sheaf 不 refill 而回退慢路径
- sheaf 快路径减少对慢路径的回退,降低分配开销
作者说明在 ublk null target benchmark 上观察到因 sheaves 导致的回归,profile 显示 alloc_from_pcs() 快路径失败、回退 ___slab_alloc() 慢路径,分配经 mempool_alloc()。