背景

k[v]free_rcu() 把对象的释放推迟到 RCU grace period 之后,调用方不用等。但它有个硬约束:释放路径内部要拿锁,在持锁状态不明的上下文(unknown context)里调用它,可能撞上自己持有的锁,与正在进行的 k[v]free_rcu() 自我死锁。

kfree_rcu 的上下文约束
┌──────────────────────────────────────┐
│    kfree_rcu() 释放路径内部要拿锁    │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 调用方持锁状态不明,可能撞上自己持有 │
│ 的锁                                 │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 自我死锁风险下,unknown context 只能 │
│ 绕开不用                             │
└──────────────────────────────────────┘

SLUB 的 sheaf(每 CPU 成捆的对象缓存)给了一条快路径 kfree_rcu_sheaf():对象塞进每 CPU 专用于 RCU 释放的 rcu sheaf,攒满整个 sheaf 才 call_rcu() 提交。但它原本挑地方:不能 spin 的上下文不行,PREEMPT_RT 不行,unknown context 更不行,sheaf 满了要 call_rcu() 提交,而 call_rcu() 在 unknown context 调不得。

问题

  • k[v]free_rcu() 在 unknown context 调用可能自我死锁,调用方只能绕开
  • kfree_rcu_sheaf() 快路径不支持 !allow_spin、PREEMPT_RT 与 unknown context
  • sheaf 攒满要 call_rcu() 提交,unknown context 调不了 call_rcu()
  • kvfree_rcu 批处理的 rcu_head 带函数指针,回调其实固定,元数据有浪费

方案

引入 kfree_rcu_nolock(),任意上下文可调:能走 sheaf 就走 sheaf,走不了挂 irq work 延后走 kvfree_rcu 批处理。

kfree_rcu_nolock 的两条释放路径
                    ┌───────────────────────────────────┐
                    │ kfree_rcu_nolock() 任意上下文调用 │
                    └─────────────────┬─────────────────┘
                 ┌────────────────────┴────────────────────┐
                 ▼                                         ▼
┌──────────────────────────────────┐     ┌────────────────────────────────────┐
│ sheaf 快路径,无锁批量塞进每 CPU │     │ 兜底挂 irq work,延后走 kvfree_rcu │
│ 的 rcu sheaf                     │     │ 批处理                             │
└──────────────────────────────────┘     └────────────────────────────────────┘

快路径 kfree_call_rcu_nolock() 先排除不适合 sheaf 的对象:vmalloc 对象、large kmalloc、跨 NUMA 节点的 slab 对象都直接走兜底;slab 小对象交给 __kfree_rcu_sheaf() 并带上 SLAB_FREE_NOLOCK 标记。

让 sheaf 路径适应 unknown context 的三处改动:

  • free 路径引入 free_flags 描述上下文。不可 spin 时改从 spare sheaf 或 barn 拿空 sheaf,拿锁只用 trylock,失败就把空 sheaf 直接释放而不硬等。
  • sheaf 攒满时的提交分两种情形:IRQ 开着可以直接 call_rcu(),它内部用 local_irq_save() 保护 per-CPU 数据,IRQ 未禁就没有重入破坏;IRQ 禁用时把满 sheaf 挂到 irq work 上延后提交。
  • PREEMPT_RT 一律按不可 spin 处理:local_trylock 与 spinlock 只要不真正 spin、且不在 NMI 或 hardirq 里就安全,RT 上于是也能走 sheaf。

兜底路径同样做了瘦身。kvfree_rcu 批处理原来复用 rcu_head,函数指针加链表指针共两个指针,而回调固定是 kfree()、vfree()、free_large_kmalloc() 三选一,函数指针纯属浪费。新的 kvfree_rcu_head 只留一个指针串链表,对象起始地址改在慢路径重算,KASAN tag 不丢。kfree_rcu_nolock() 的兜底直接复用这套批处理。

收益

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

  • kfree_rcu_nolock() 任意上下文可调,调用方不再受死锁约束
  • slab 小对象多数走 sheaf 快路径,无锁批量释放
  • kvfree_rcu 批处理每对象元数据从两个指针降到一个
  • 兜底经 irq work 延后提交,不堵调用上下文