引用计数是内核对象生命周期管理的基础。但原子
refcount_inc/dec 在高频路径上 → cache line
bouncing → 性能崩溃。
一、先看图
flowchart TD
subgraph 活跃模式
CPU0[CPU 0<br/>local_count++]
CPU1[CPU 1<br/>local_count++]
CPU2[CPU 2<br/>local_count++]
end
subgraph 关闭模式
KILL[percpu_ref_kill] --> CONVERGE[汇总 per-CPU 到 atomic]
CONVERGE --> ATOMIC[atomic_long_t count<br/>正常 dec]
ATOMIC --> ZERO[count == 0<br/>→ 释放]
end
classDef active fill:#3fb95022,stroke:#3fb950,color:#adbac7;
classDef shutdown fill:#f0883e22,stroke:#f0883e,color:#adbac7;
class CPU0,CPU1,CPU2 active
class KILL,CONVERGE,ATOMIC,ZERO shutdown
二、问题
atomic_inc(&ref); // I/O 提交
// ... 使用对象 ...
atomic_dec(&ref); // I/O 完成NVMe 驱动每秒百万次 I/O → atomic 操作 → 同一 cacheline 被所有核争抢。
三、percpu_refcount 设计
3.1 活跃阶段
percpu_ref_get(&ref); // → __this_cpu_inc(ref->data->count)每 CPU 独立计数器 → 零竞争。
3.2 关闭阶段(kill)
percpu_ref_kill(&ref);- 切换到原子模式
- 汇总所有 per-CPU 计数到全局
atomic_long_t - 后续 get/put 走原子路径
- 计数归零 → 调用 release 回调
3.3 两阶段的原因
活跃时不能知道全局 refcount 是否为零(分散在各 CPU)→ 只在关闭路径收敛。
四、API
struct percpu_ref ref;
percpu_ref_init(&ref, release_fn, 0, GFP_KERNEL);
percpu_ref_get(&ref); // 活跃时 per-CPU,关闭后 atomic
percpu_ref_put(&ref);
percpu_ref_kill(&ref); // 切换到关闭模式
// release_fn 在 count → 0 时被调用五、使用场景
- blk-mq:request queue 引用计数
- io_uring:io_uring_ctx 引用
- cgroup:cgroup 子系统引用
- 设备关闭路径:确保所有 I/O 完成后再释放
六、SRCU(Sleepable RCU)
RCU 读侧不能睡眠。SRCU 解决:
int idx = srcu_read_lock(&my_srcu);
// 可以睡眠的读临界区
srcu_read_unlock(&my_srcu, idx);
synchronize_srcu(&my_srcu); // 等待读者退出6.1 实现
SRCU 内部使用 per-CPU 计数器(类似 percpu_refcount 思路)。
6.2 代价
比 RCU 慢:
- 读侧有原子 inc/dec(per-CPU)
- grace period 需要每 CPU 采样
6.3 使用场景
- 需要在读侧阻塞 I/O、分配内存
- 设备驱动热拔插路径
七、kref
struct kref ref;
kref_init(&ref);
kref_get(&ref);
kref_put(&ref, release_fn);kref 是 refcount_t 的简单包装:
refcount_t比atomic_t多了饱和保护(防止 use-after-free overflow)kref_put自动在归零时调用 release
八、正确关闭顺序
// 1. 停止新请求
percpu_ref_kill(&ref);
// 2. 等待 in-flight 请求完成
wait_for_completion(&done); // release_fn 里 complete()
// 3. 释放资源
cleanup();错误顺序 → use-after-free 或永远等不到释放。
九、观察
# per-CPU refcount 状态
bpftrace -e 'kprobe:percpu_ref_kill { printf("%s %s\n", comm, kstack(3)); }'
# SRCU grace period
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata十、小结
- percpu_refcount:活跃时 per-CPU 零竞争,关闭时收敛为原子
- SRCU:允许读侧睡眠的 RCU 变体
- kref:简单引用计数 + 饱和保护
- 关闭路径的正确顺序是关键
参考文献
lib/percpu-refcount.cinclude/linux/srcu.hinclude/linux/kref.h- Kent Overstreet, “percpu_ref: switch to atomic before killing.” 2013
工具
- bpftrace
/sys/kernel/debug/rcu/
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】RCU 深度
RCU 是 Linux 内核使用最广泛的同步机制——读侧零开销。本文讲经典 RCU、Tree RCU、Tasks RCU、SRCU、grace period、rcu_dereference、call_rcu、lazy RCU、nocb CPU。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。