引用计数是内核对象生命周期管理的基础。但原子
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。
【操作系统百科】内核内存调试
内核内存 bug 是最难追的:UAF、OOB、double free、leak 都可能沉默数月。本文讲 KASAN 三种模式、KFENCE 生产采样、kmemleak、SLUB_DEBUG、UBSAN/KCSAN 联动。
【操作系统百科】VFS 四层抽象
Linux 的一切皆文件靠 VFS 实现——superblock、inode、dentry、file 四层抽象加 ops 表。本文讲 VFS 核心数据结构、dcache、inode cache、RCU lookup,以及文件系统如何插入 VFS。
操作系统百科
Linux 6.x 视角下的操作系统系列索引:110 篇覆盖调度、虚拟内存、文件系统与 I/O、并发、隔离、可观测性,按主题、阅读路径与关键问题三种入口组织。