背景

SLUB 的 sheaves 是 per-CPU 的对象批量缓存。sheaf 是一个固定容量的对象指针数组,调用方(如 maple tree 的 maple_node)用 prefill API 批量预取对象、用完批量归还,绕开逐对象分配的慢路径。barn 是每个 node 的 sheaf 仓库,分 full 与 empty 两个列表,供 CPU 之间互通有无。kfree_rcu() 释放的对象先聚成 RCU sheaf,之后尝试放进 barn 的 full 列表,放不进去就把对象逐个 flush 回 slab。

现有 sheaf 机制的对象流转
┌─────────────────────────────────────┐
│ prefill API 给未满的 sheaf 补对象, │
│ 只从 partial slab 取                │
└──────────────────┬──────────────────┘
                   ▼
┌─────────────────────────────────────┐
│     调用方从 sheaf 批量取用对象     │
└──────────────────┬──────────────────┘
                   ▼
┌─────────────────────────────────────┐
│ kfree_rcu 的对象聚成 RCU sheaf 交还 │
│ barn                                │  barn 的 full 列表只进不出,逐渐饱和
└─────────────────────────────────────┘

现有的补充逻辑有个偏向,prefill API 给未满的 sheaf 补对象时只从 partial slab 取,从来不从 barn 的 full 列表取。

问题

  • sheaf 未满时只从 partial slab 补充,从不消费 barn 的 full 列表。
  • barn 的 full 列表一旦饱和就一直饱和,只进不出。
  • kfree_rcu 的 RCU sheaf 无处安放,只能 flush 回 slab。
  • 从 partial slab 补充要走 list_lock,flush 让对象回流慢路径。

方案

让补充优先从 barn 取,barn 上新增一个 partial sheaf,专门收留补剩下的对象,不再 flush。

做法是把 barn 的对象拷进调用方手里的 sheaf,而不是拿 barn 的 sheaf 换给它。prefill 路径先调新增的 refill_sheaf_from_barn():在 barn 锁内循环拷贝,直到 sheaf 填满或 barn 掏空。每轮先取 barn 的 partial sheaf 作拷贝源,没有就从 full 列表取一个;full 列表上的 sheaf 未必是满的,所以每轮按缺口与源存量的较小值拷贝。源拷贝后还有剩就留作 partial sheaf,掏空了就放 empty 列表,被补充的 sheaf 始终不换出。barn 掏不出对象时返回 false,回退原有的 partial slab 路径。

从 barn 原地补充未满的 sheaf
┌──────────────────────────────────────┐
│ 循环拷贝直到 sheaf 填满或 barn 掏空  │  调用方的 sheaf 始终不换出
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 拷贝源先取 barn 的 partial sheaf,没 │
│ 有则从 full 列表取一个               │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 源拷贝后还有剩就留作 partial sheaf, │
│ 掏空就放 empty 列表                  │
└──────────────────────────────────────┘

只有 prefill 路径需要这个 partial sheaf。generic 分配路径 __pcs_replace_empty_main() 是拿空 sheaf 换 barn 的 full sheaf,不会有剩余;refill_sheaf() 只从 partial slab 取,不碰 partial sheaf。

也可以直接用 barn 的 sheaf 换掉调用方的 sheaf,省掉这次 memcpy。但换掉 sheaf 会丢失 cache 亲和性,原地填充更直白,实测两者没有可测差异,于是采用原地填充;交换要附加的 pfmemalloc 约束与容量修正也随之不再需要。

partial sheaf 是点睛之笔。把未满的 sheaf 直接挂上 full 列表行不通,它占着位置,full 列表依旧饱和,RCU sheaf 照样无处安放。有了 partial sheaf,剩余对象既不丢回 slab,也不占 full 列表名额。

收益来自两侧。每取走一个 full sheaf,就给未来的 RCU sheaf 腾出一个位置,barn 的循环被打通。从 barn 拿对象也比在 list_lock 下从 partial slab 拿便宜。

barn 的循环被打通
┌────────────────────────┐   ┌───────────────────┐   ┌────────────┐
│                        │   │                   │   │ RCU sheaf  │
│ refill 取走 full sheaf ├──▶│ full 列表腾出位置 ├──▶│ 进 barn 免 │
│                        │   │                   │   │ 于 flush   │
└────────────────────────┘   └───────────────────┘   └────────────┘

收益

will-it-scale mmap1 基准(192 进程,每次一分钟),maple_node cache:

指标 baseline patched 变化
吞吐 27778727 34500556 +24.2%
alloc_slab 7100651 159739 -97.75%
free_slab 7099788 158297 -97.77%
page allocator churn 14200439 318036 -97.76%
free_slowpath 10969156 782851 -92.86%
sheaf_refill 8343794364 4657509724 -44.18%
sheaf_flush 8332823922 4656725764 -44.12%
barn_put_fail 260400484 145522675 -44.12%

代价方面,测试后 maple_node cache 持有的 slab 从 1604 增至 2336,作者手动 shrink 后为 485 对 563,说明多出的 slab 大多只是对象坐在 sheaf 里,shrink 即可归还;剩余的少许差异,是因为从 barn 发出去的对象来自的 slab 比从 partial slab 补充更分散。