[TOC]
lib/percpu-refcount.c percpu_ref(percpu 引用计数) 为高并发对象提供按 CPU 本地计数并可切换到原子模式的引用计数机制
介绍
percpu_ref 是内核通用引用计数基础设施,主体实现位于 lib/percpu-refcount.c,对外 API 与数据结构主要在 include/linux/percpu-refcount.h。它提供 struct percpu_ref 抽象:热路径用每 CPU 本地计数(this_cpu_add()/this_cpu_sub())降低争用,进入销毁阶段后切换到全局原子计数并在归零时调用释放回调。内核态的接入点通常是“对象查找/获取引用”与“对象释放/销毁”两端:运行期用 percpu_ref_get()/percpu_ref_put() 或 percpu_ref_tryget_live(),销毁期用 percpu_ref_kill() / percpu_ref_kill_and_confirm()。用户态不会直接调用 percpu_ref,但常通过系统调用触发其生命周期(例如 fs/aio.c 中 io_setup()/io_destroy() 路径驱动 struct kioctx 的创建与销毁)。典型入口函数是对象创建时的 percpu_ref_init(),以及 teardown 时的 percpu_ref_kill(),而热路径则集中在 percpu_ref_get_many()/percpu_ref_put_many()。
历史与背景
为了解决什么特定问题而诞生?
问题 1:原子引用计数在高并发下争用严重
传统atomic_inc()/atomic_dec_and_test()(或refcount_t/kref背后的原子操作)会在多核上造成同一 cacheline 抖动;percpu_ref 将热路径拆到每 CPU 本地计数,核心热路径函数是percpu_ref_get_many()/percpu_ref_put_many(),分别走this_cpu_add()/this_cpu_sub()分支以避开全局原子更新。问题 2:需要统一的“两阶段销毁”抽象,避免销毁窗口发放新引用
对象销毁时,必须先阻止新的引用获取,再等待存量引用归零。percpu_ref 用percpu_ref_kill_and_confirm()设置__PERCPU_REF_DEAD并触发模式切换;对“查找即取引用”的路径提供percpu_ref_tryget_live()/percpu_ref_tryget_live_rcu(),在__PERCPU_REF_DEAD后拒绝新引用(并可用 confirm 回调建立“全 CPU 可见”的时序点)。问题 3:并发边界与可维护性:切换/释放必须可被严格约束
模式切换依赖 RCU:__percpu_ref_switch_to_atomic()通过call_rcu_hurry()触发percpu_ref_switch_to_atomic_rcu()汇总 per-cpu 计数;确认回调走percpu_ref_call_confirm_rcu(),并用wait_event_lock_irq()+percpu_ref_switch_lock/percpu_ref_switch_waitq序列化切换。由于释放回调可能在 RCU 回调上下文触发,percpu_ref_init()明确要求ref->data->release不得睡眠;confirm 回调同样要求不阻塞。
发展经历了哪些重要的里程碑或版本迭代?
里程碑 1:对象模型与两种计数模式确立
struct percpu_ref(指针低位编码__PERCPU_REF_ATOMIC/__PERCPU_REF_DEAD)+percpu_ref_init()初始化 per-cpu 计数与全局atomic_long_t count(在struct percpu_ref_data),“percpu 模式热路径 + atomic 模式可归零释放”成为基本模型。里程碑 2:能力扩展:kill/confirm、可重用与显式切换
percpu_ref_kill_and_confirm()提供 kill 语义与确认点;percpu_ref_resurrect()/percpu_ref_reinit()支持“死而复生”(受PERCPU_REF_ALLOW_REINIT/allow_reinit控制);percpu_ref_switch_to_atomic()/percpu_ref_switch_to_percpu()支持非 kill 场景下的模式管理(由force_atomic与__percpu_ref_switch_mode()统一裁决)。里程碑 3:可维护性增强:更清晰的切换状态机与诊断
切换进行中用ref->data->confirm_switch != NULL表示,__percpu_ref_switch_mode()通过wait_event_lock_irq()等待上一次切换完成;切换汇总阶段在percpu_ref_switch_to_atomic_rcu()中对 underflow 做WARN_ONCE()、mem_dump_obj()、pr_err()诊断,减少静默错误。里程碑 4:后续完善:内存占用与 fast path 结构布局优化
将非热字段移出struct percpu_ref,集中到struct percpu_ref_data(注释明确“仅 fast path 需要 percpu_count_ptr”),以降低嵌入对象的常驻大小;同时在percpu_ref_exit()/__percpu_ref_exit()中细化退出与允许 reinit 时的资源保留/释放策略。
目前该技术的社区活跃度和主流应用情况如何?
- 主线长期维护:代码位于
lib/与include/linux/,接口以EXPORT_SYMBOL_GPL()形式提供,属于通用基础设施;切换逻辑依赖 RCU、waitqueue、spinlock 等核心原语,维护通常随内核并发模型演进而持续调整。 - 主流应用:典型是“RCU 保护查找 + 高频 get/put”的内核对象,例如
fs/aio.c的struct kioctx,查找路径用percpu_ref_tryget_live()确保对象未进入 kill,系统调用结束时用percpu_ref_put()归还引用,销毁路径用percpu_ref_kill()进入两阶段 teardown。 - 变化趋势:围绕“切换成本可控、并发语义可证明、嵌入对象占用更低”持续演进;受影响的关键点集中在
__ref_is_percpu()(READ_ONCE + release/acquire 配对)、__percpu_ref_switch_mode()(等待与串行化)、percpu_ref_switch_to_atomic_rcu()(汇总与健壮性检查)等路径。
核心原理与设计
它的核心工作原理是什么?
组件/层次划分:
- 框架层/核心层:
lib/percpu-refcount.c负责模式切换、汇总与 kill/reinit;关键入口包括percpu_ref_init()、percpu_ref_exit()、percpu_ref_kill_and_confirm()、percpu_ref_switch_to_atomic()、percpu_ref_switch_to_percpu(),以及切换回调percpu_ref_switch_to_atomic_rcu()/percpu_ref_call_confirm_rcu()。 - 驱动/实现层:嵌入对象在自身结构体中包含
struct percpu_ref,并提供percpu_ref_func_t release(如fs/aio.c的free_ioctx_users()、free_ioctx_reqs());对象 teardown 时由业务逻辑调用percpu_ref_kill()(或_and_confirm())进入销毁阶段。 - 用户态接口层(间接):用户态通过系统调用驱动对象生命周期;例如 AIO 路径中
io_setup()创建对象后通过percpu_ref_get()持有初始引用,后续 syscall 查找对象时通过percpu_ref_tryget_live()成功才可继续,结束时percpu_ref_put();销毁由io_destroy()/进程退出路径触发percpu_ref_kill()。
- 框架层/核心层:
关键数据结构:
struct percpu_ref:unsigned long percpu_count_ptr:指向 per-cpu 计数的指针,低__PERCPU_REF_FLAG_BITS位复用为标志(__PERCPU_REF_ATOMIC/__PERCPU_REF_DEAD);__ref_is_percpu()用READ_ONCE()取值并判定是否走 per-cpu 快路径。struct percpu_ref_data *data:指向慢路径元数据与全局计数(可能在percpu_ref_exit()中被置 NULL 并转存 count 到percpu_count_ptr高位)。
struct percpu_ref_data:atomic_long_t count:atomic 模式下的全局计数;切换到 atomic 时在percpu_ref_switch_to_atomic_rcu()汇总 per-cpu 后用atomic_long_add()合并,并用PERCPU_COUNT_BIAS防止过早归零。percpu_ref_func_t *release:计数归零时由percpu_ref_put_many()触发调用(atomic 分支atomic_long_sub_and_test()为真则调用)。percpu_ref_func_t *confirm_switch:非 NULL 表示“切换进行中”;percpu_ref_call_confirm_rcu()调用后清空并wake_up_all(&percpu_ref_switch_waitq)。bool force_atomic/bool allow_reinit:由percpu_ref_switch_to_atomic()、percpu_ref_switch_to_percpu()与percpu_ref_init()flags(PERCPU_REF_INIT_ATOMIC、PERCPU_REF_ALLOW_REINIT)驱动,最终在__percpu_ref_switch_mode()决策走向。struct rcu_head rcu:承载call_rcu_hurry()回调。struct percpu_ref *ref:反向指针,供 RCU 回调中取回对象。
per-cpu 计数存储:
- 分配:
percpu_ref_init()中用__alloc_percpu_gfp(sizeof(unsigned long), align, gfp)分配。 - 访问:
percpu_count_ptr(ref)(在lib/percpu-refcount.c中使用)+per_cpu_ptr()/this_cpu_*()。 - 释放:
__percpu_ref_exit()与percpu_ref_exit()负责释放,且在切换确认阶段percpu_ref_call_confirm_rcu()可按allow_reinit决定是否释放。
- 分配:
并发与同步承载体:
percpu_ref_switch_lock:串行化切换/exit/部分状态读取(如percpu_ref_is_zero()读取data->count也会短暂持锁防并发销毁)。percpu_ref_switch_waitq:等待confirm_switch清空,避免重入切换。
关键流程:
初始化:
percpu_ref_init()→__alloc_percpu_gfp()分配 per-cpu 计数 →kzalloc(sizeof(struct percpu_ref_data))→ 设置data->force_atomic/data->allow_reinit与初始atomic_long_set(&data->count, start_count)(percpu 起始会加入PERCPU_COUNT_BIAS并再+1初始引用)→ref->data = data。运行时:
- 热路径(percpu 模式):
percpu_ref_get_many()→rcu_read_lock()→__ref_is_percpu()成功 →this_cpu_add(*percpu_count, nr)→rcu_read_unlock();percpu_ref_put_many()→rcu_read_lock()→__ref_is_percpu()成功 →this_cpu_sub(*percpu_count, nr)(不检查 0)→rcu_read_unlock()。 - 慢路径(atomic 或 kill 后):
percpu_ref_get_many()/percpu_ref_put_many()在__ref_is_percpu()失败时走 atomic:atomic_long_add()或atomic_long_sub_and_test();后者为真则ref->data->release(ref)。
获取新引用但需要防止已 kill:percpu_ref_tryget_live()→rcu_read_lock()→percpu_ref_tryget_live_rcu():若非 percpu 且ref->percpu_count_ptr & __PERCPU_REF_DEAD为真则拒绝,否则atomic_long_inc_not_zero()。
- 热路径(percpu 模式):
销毁/卸载:
- teardown 触发:
percpu_ref_kill()(inline)→percpu_ref_kill_and_confirm(ref, NULL) - kill 主体:
percpu_ref_kill_and_confirm()→ 持percpu_ref_switch_lock→ 设置ref->percpu_count_ptr |= __PERCPU_REF_DEAD→__percpu_ref_switch_mode()(必要时wait_event_lock_irq()等待前次切换完成)→__percpu_ref_switch_to_atomic():设置__PERCPU_REF_ATOMIC、设置data->confirm_switch、percpu_ref_get()(为回调期间保活)→call_rcu_hurry(..., percpu_ref_switch_to_atomic_rcu)→percpu_ref_put(ref)(丢弃“初始引用”) - 汇总与确认:RCU 回调
percpu_ref_switch_to_atomic_rcu()→ 遍历for_each_possible_cpu(cpu)汇总 per-cpu 计数 →atomic_long_add(count - PERCPU_COUNT_BIAS, &data->count)→percpu_ref_call_confirm_rcu():调用 confirm(若有)→ 清空confirm_switch+ 唤醒等待者 → 按allow_reinit决定是否__percpu_ref_exit()释放 per-cpu 区 →percpu_ref_put(ref)(对__percpu_ref_switch_to_atomic()里get的对称释放)。 - 资源释放收尾:对象 release 回调中通常调用
percpu_ref_exit()释放 percpu_ref 自身资源;percpu_ref_exit()会__percpu_ref_exit()释放 per-cpu 区,并在持percpu_ref_switch_lock下将data->count迁移到percpu_count_ptr高位并kfree(data)。
- teardown 触发:
它的主要优势体现在哪些方面?
优势 1:一致性与通用抽象
struct percpu_ref+percpu_ref_init()/percpu_ref_exit()提供统一的引用计数生命周期接口;percpu_ref_kill()将“进入销毁阶段”的语义显式化,配合percpu_ref_tryget_live()把“是否还能发放新引用”固化为可复用接口。优势 2:性能与可扩展性
热路径percpu_ref_get_many()/percpu_ref_put_many()在 percpu 模式下仅做__ref_is_percpu()+this_cpu_add/sub(),避免全局atomic_long_*争用;切换仅在 teardown 或显式切换时触发,且通过call_rcu_hurry()将“全 CPU 视角一致化”外包给 RCU 回调percpu_ref_switch_to_atomic_rcu()。优势 3:资源管理/安全边界/可维护性
kill/release 分离:percpu_ref_kill_and_confirm()先把系统带到“不会再发放新引用”的状态,再由percpu_ref_put_many()的 atomic 分支在归零时触发data->release,减少 use-after-free 窗口;切换状态由data->confirm_switch显式表示并可用percpu_ref_switch_waitq等待;underflow 检测集中在percpu_ref_switch_to_atomic_rcu(),便于定位错误使用。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
局限 1:切换与汇总开销集中且不可忽略
percpu_ref_switch_to_atomic_rcu()需要遍历for_each_possible_cpu()汇总 per-cpu 计数,并做一次atomic_long_add()合并;在 CPU 数很多或 teardown 频繁的场景,切换成本会显著。局限 2:上下文限制严格
percpu_ref_init()要求release不得睡眠(可能在 RCU 回调链路触发);percpu_ref_kill_and_confirm()/percpu_ref_switch_to_atomic()的 confirm 回调也要求不阻塞;__percpu_ref_switch_mode()可能通过wait_event_lock_irq()阻塞等待前次切换完成,因此调用者若无法保证“无并行切换”就需要接受潜在睡眠点。局限 3:语义更复杂,误用风险更高
需要遵循“两阶段销毁”:先percpu_ref_kill()再依赖存量引用归零触发释放;若仅用percpu_ref_put()试图完成 teardown,在 percpu 模式下不会检查 0,无法触发release。同时percpu_ref本身不提供 RCU 宽限期语义,若对象指针通过 RCU 发表,则仍需像fs/aio.c那样显式call_rcu()/rcu_work同步。局限 4:可重用(reinit/resurrect)带来额外状态机复杂度
percpu_ref_resurrect()/percpu_ref_reinit()依赖__PERCPU_REF_DEAD、allow_reinit与切换状态组合;若release可能释放包含percpu_ref的外层对象,调用方必须自行保证 resurrect 期间不会并发触发release(percpu_ref_resurrect()文档要求调用者保证这一点)。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
场景 1:RCU 保护查找表中的高频对象引用获取
典型模式是“查找返回对象指针时立刻取引用”;查找路径用percpu_ref_tryget_live()(或percpu_ref_tryget_live_rcu())确保对象未进入 kill,成功后调用方持有引用,结束时percpu_ref_put();teardown 端先让查找路径不可再成功(percpu_ref_kill_and_confirm()设置__PERCPU_REF_DEAD并在确认后保证不再发放新引用)。场景 2:AIO 场景下的
struct kioctx生命周期管理(fs/aio.c)- 创建:
io_setup()路径中percpu_ref_init(&ctx->users, free_ioctx_users, 0, GFP_KERNEL)、percpu_ref_init(&ctx->reqs, free_ioctx_reqs, 0, GFP_KERNEL),并用percpu_ref_get()提前持有引用用于跨阶段初始化。 - 运行:查找
kioctx时用percpu_ref_tryget_live(&ctx->users)成功才返回;多个 syscall 结束点(如提交/取消/获取事件等)统一用percpu_ref_put(&ctx->users)归还引用。 - 销毁:
kill_ioctx()中percpu_ref_kill(&ctx->users)进入 teardown;当ctx->users归零后触发free_ioctx_users(),其内部再对ctx->reqs执行percpu_ref_kill(&ctx->reqs)+percpu_ref_put(&ctx->reqs),形成级联释放链路。
- 创建:
场景 3:需要“保留 per-cpu 热路径但允许显式降级为 atomic”的对象
对象在某些阶段需要严格归零检测或更强的全局一致性时,可用percpu_ref_switch_to_atomic()/percpu_ref_switch_to_atomic_sync()进入 atomic 模式;若允许恢复 per-cpu(PERCPU_REF_ALLOW_REINIT/allow_reinit),可在安全点调用percpu_ref_switch_to_percpu()重新启用 per-cpu 快路径(内部__percpu_ref_switch_to_percpu()会清零各 CPU 计数并用smp_store_release()清除__PERCPU_REF_ATOMIC)。
是否有不推荐使用该技术的场景?为什么?
不推荐场景 1:引用操作不频繁或并发度低
若热路径 get/put 频率不高,直接用refcount_t/kref的原子计数更简单;percpu_ref 的percpu_ref_kill()两阶段 teardown 与切换链路(call_rcu_hurry()→percpu_ref_switch_to_atomic_rcu())会引入额外复杂度与切换成本。不推荐场景 2:释放回调必须睡眠或需要复杂阻塞收尾
percpu_ref_put_many()在 atomic 归零时直接调用ref->data->release(ref),而percpu_ref_init()明确release可能在 RCU 回调上下文触发,不能睡眠;若必须做可睡眠的销毁工作,应改为在release中仅做唤醒/调度,把重活迁移到 workqueue 等可睡眠上下文。
对比分析
请将其 与 其他相似技术 进行详细对比。
(选择对象:refcount_t/kref、percpu_counter、纯 RCU 生命周期管理)
| 维度 | 本技术/文件 | 相似技术 A(refcount_t/kref) | 相似技术 B(percpu_counter) | 相似技术 C(纯 RCU 生命周期) |
|---|---|---|---|---|
| 实现方式 | struct percpu_ref + struct percpu_ref_data;热路径 percpu_ref_get_many()/percpu_ref_put_many();切换 percpu_ref_kill_and_confirm()→call_rcu_hurry()→percpu_ref_switch_to_atomic_rcu() |
单一全局原子计数;热路径原子 RMW;释放通常在 refcount_dec_and_test()/kref_put() 触发 |
每 CPU 计数为主,聚合用 percpu_counter_sum() 等;通常用于统计而非对象生命周期释放 |
依赖 kfree_rcu()/call_rcu() 等宽限期;不提供“引用计数归零触发释放”语义 |
| 性能开销 | percpu 模式下 this_cpu_add/sub,切换阶段一次性汇总;kill 后退化为 atomic_long_* |
始终原子 RMW,争用随并发上升 | 热路径 per-cpu 更新,读取/汇总开销不小;缺少 kill/归零释放语义 | 读侧几乎零开销(RCU 读锁),但写侧需要宽限期等待;不适合需要显式引用计数的场景 |
| 资源占用 | 每对象一个 per-cpu unsigned long 区 + struct percpu_ref_data;通过拆分 percpu_ref_data降低嵌入对象大小 |
每对象一个原子计数(通常 4/8 字节) | 每对象 per-cpu 存储,通常比 percpu_ref 更偏“统计容器” | 需要 RCU 发表指针与释放路径;不需要 per-cpu 计数存储 |
| 隔离级别 | 明确 kill 边界:__PERCPU_REF_DEAD + percpu_ref_tryget_live();确认点 confirm_kill |
仅靠原子计数归零,难表达“禁止新引用发放”的阶段性语义 | 无生命周期隔离语义 | 以宽限期隔离“旧读者”,但无法表达“持引用直到完成”的计数语义 |
| 启动速度 | percpu_ref_init() 需 __alloc_percpu_gfp() + kzalloc();比单原子更重 |
初始化最轻 | 初始化依赖 per-cpu 分配,成本较高 | 取决于对象发表方式;通常不需要 per-cpu 分配 |
总结
关键特性总结
- percpu 热路径引用计数:
percpu_ref_get_many()/percpu_ref_put_many()在__ref_is_percpu()成功时走this_cpu_add/sub,把高频引用操作从全局原子争用中剥离到每 CPU。 - 两阶段 teardown 与“禁止新引用”语义:
percpu_ref_kill()(→percpu_ref_kill_and_confirm())设置__PERCPU_REF_DEAD并切换到 atomic,配合percpu_ref_tryget_live()在 kill 后拒绝新引用发放。 - RCU 驱动的模式切换与确认点:
__percpu_ref_switch_to_atomic()通过call_rcu_hurry()触发percpu_ref_switch_to_atomic_rcu()汇总并合并到atomic_long_t count,再由percpu_ref_call_confirm_rcu()执行 confirm 回调并唤醒percpu_ref_switch_waitq等待者。
percpu_ref_kill __ref_is_percpu percpu_ref_get_many percpu_ref_get percpu_ref_tryget_many percpu_ref_tryget percpu_ref_tryget_live_rcu percpu_ref_tryget_live percpu_ref_put_many percpu_ref_put percpu_ref_is_dying 快路径引用获取与死亡态门控
作用与实现要点
- 快路径分流:通过
percpu_count_ptr低位标志判断当前是 percpu 模式还是 atomic/死亡态模式,快路径尽量只做本地 CPU 计数增减。 - RCU 包裹读侧:get/put/tryget 的模式判定与指针使用放在
rcu_read_lock()内,保证并发切换模式时不会把已变化的值当作稳定指针使用。 - 死亡态门控:
DEAD置位后,tryget_live在 atomic 路径上拒绝再发放新引用;需要更强边界时依赖 kill 的确认回调。 - 释放触发位置:percpu 模式不在 put 里做归零检测;只有切到 atomic 后才允许
put在减到 0 时调用release()。
percpu_ref_kill 进入死亡态并触发切到atomic
1 | /** |
__ref_is_percpu 判定是否可走percpu计数路径
1 | /** |
percpu_ref_get_many 增加多个引用
1 | /** |
percpu_ref_get 增加单个引用
1 | /** |
percpu_ref_tryget_many 在未归零时尝试增加多个引用
1 | /** |
percpu_ref_tryget 在未归零时尝试增加单个引用
1 | /** |
percpu_ref_tryget_live_rcu 在已持有RCU时尝试获取未死亡引用
1 | /** |
percpu_ref_tryget_live 自动持有RCU后尝试获取未死亡引用
1 | /** |
percpu_ref_put_many 减少多个引用并在atomic模式下可能触发释放
1 | /** |
percpu_ref_put 减少单个引用并在atomic模式下可能触发释放
1 | /** |
percpu_ref_is_dying 查询死亡态标志位
1 | /** |








