Skip to content

模型Model

Read-Copy Update

RCU · Read-copy update

先发布完整新版本,再等待相关旧读侧区间结束,以分离共享更新和旧对象回收。

形式陈述 ​

Read-Copy Update(RCU)把共享结构更新分成发布新版本和等待旧读者结束两个阶段。其核心不是一条特殊CPU指令,而是一组共享内存访问与生命周期契约。

本页固定单个配置根指针P、不可变的已发布配置对象和串行化更新者。根发布与读取采用SC原子操作;配置对象在发布前完整初始化,发布后不原位修改。读者只在读侧临界区内保留和使用借来的指针。

抽象接口为:

  1. read_lock/read_unlock标出读侧临界区
  2. publish(P,new)原子地让后续读取P可得到新对象,并保证初始化先于相应读取对象内容
  3. synchronize()开始一个宽限期并等待:在该调用开始前已经进入、尚未结束的相关读侧临界区全部结束后,才可返回

宽限期不必等待所有随后新进入的读者。实现可以额外等待某些读者,也可以因调度或批处理晚些返回,但不能早于必须等待的旧读者。

这里的“先于”需有语言/机器承诺的同步次序。SC模型下先研究生命周期即可;Linux的 rcu_assign_pointer/rcu_dereference及不同用户态RCU各自负责实际内存模型中的发布与读取要求,不能把它们替换成普通变量赋值。

直觉

更新配置时,不去抢读者正在看的纸,而是先写好一张新纸,把入口指向新纸。旧读者仍可以把手里的旧纸看完。等所有可能持有旧纸的人结束这一轮,才回收旧纸。

“入口已经换了”与“旧纸已经没人用”是不同事件。前者是一个指针发布,后者由宽限期保证。把这两个时刻混为一谈,就会在读多写少的简单代码里制造悬空访问。

例子与边界

配置替换的正确次序 ​

读者执行:

text
read_lock()
p = load(P)
answer = p.limit + p.offset
read_unlock()
return answer

更新者执行:

text
new = allocate_and_initialize(limit=20, offset=2)
old = exchange(P, new)
synchronize()
free(old)

初始old的字段为limit=10、offset=1。所有已发布对象不可变,所以一次读者若取得old,得到11;若取得new,得到22。读者只加载一次P,不会从old读limit再从new读offset,产生一个不存在的混合配置。

考虑以下时间线:

时刻 事件
t₀ R1进入读区间并取得old
t₁ 更新者完整初始化new
t₂ 更新者交换P,发布new
t₃ 更新者开始synchronize
t₄ R2进入,读取P得到new
t₅ R1完成old的最后访问并退出
t₆ 宽限期满足条件,synchronize可以返回,释放old
t₇ R2稍后才退出

R1必须被t₃开始的宽限期等待。R2在t₃之后进入,按本契约不必被此次宽限期等待;它持有new,也不会因释放old而受损。实际实现可能在t₆之后才安排更新者运行,故“可以返回”不等于有实时完成保证。

若R0在t₂之前进入,却拖到t₂之后才读取P,它可能取得new,但宽限期仍允许保守地等待R0。RCU跟踪的是访问区间,不是精确记录每个读者到底读了哪一个对象。

两种错误次序 ​

第一种是发布后立即free(old),省略宽限期。R1若恰在取得old后暂停,恢复时访问已释放字段。

第二种是先synchronize、后把P从old换成new。第一次等待结束之后、发布之前仍可有新读者从P取得old;更新者随后释放old时,这些读者没有被之前的等待覆盖。正确顺序必须是先断开新访问者取得old的共享路径,再等待覆盖可能的旧访问者。

还有一个常见错误:在read_unlock后把p放进缓存继续读取。退出动作已经告诉回收协议“这次借来的指针不再使用”,后续访问不再受本次读区间保护。要跨区间保留对象,需另用引用计数、所有权转移或其他经过证明的协议。

推论与应用

RCU不自动使任意读写对象线性化。上述例子的读结果是一份不可变配置快照,因此可以围绕那次P读取理解;若对象发布后还被原位改动,或者读者读取多个分别发布的根,就可能需要额外锁、版本号或一致性协议。宽限期只解决规定访问区间的生命周期与排序,不凭空提供多字段事务。

串行更新者的假设也重要。多个写者都先读old再各自复制,最后依次发布,可能覆盖彼此的逻辑修改;需在写侧加锁、用CAS重试,或使用专门更新协议。读侧可伸缩不意味着写侧无需协调。

抽象契约没有给read_lock和synchronize的统一渐近成本。某些实现读侧只是很短的本地记录,另一些支持抢占或嵌套,需要不同状态;同步宽限期可阻塞任意长时间。异步回调接口把“等待后释放”交给稍后的回调执行,可以缩短更新者同步等待,却仍会积累待回收对象,不能消除慢读者的内存成本。

EBR可用epoch公告实现类似的批量安全条件,但RCU不是某一套固定三桶计数伪代码的别名。选择具体系统时,应逐项确认读区间是否可阻塞、哪些任务必须登记、宽限期覆盖谁,以及回调/注销如何完成;不同内核与用户态变体不能拼成一份未定义的契约。

参考资料
关系图谱7 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系