RCU到底是什么?为什么快?为什么可以读写并行?

RCU(Read-Copy Update)是一种用于并发控制的数据同步机制,主要特点是允许读写并行且高效。它不使用传统的引用计数,而是通过禁止读端上下文切换来跟踪活跃读者。当写操作完成后,通过全局上下文切换检测所有读操作是否结束,然后异步释放资源。RCU的宽限期可能导致延迟,但避免了锁竞争,适合多核扩展。尽管与垃圾回收类似,但RCU需要程序员手动指定读临界区和释放位置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RCU经典文献

这几个连接简直就是一个宝藏,里面的干活很多很多!!!有时间一定要多看看。下文中的内容是看第一个和第二个链接的笔记和整理。直接观看原文,效果更佳。

LWN.net Logo

RCU锁特点介绍

Read-copy_update(简称RCU)技术是一种数据同步机制。常见的数据同步机制有:互斥锁,自旋锁,读写锁,顺序锁,信号量等手段。而RCU锁是一种比较高效的并发编程技术,它与2002年10月被添加到Linux内核中,在很多场景中它是用来替代读写锁的。

RCU锁的特点有:

  • 允许读写同时进行
  • 任意读;写操作时先拷贝一个副本,在副本上进行修改、发布,并在合适时间释放原来的旧数据
  • 读端不存在睡眠、阻塞、轮询,不会形成死锁,相比读写锁效率更高。

RCU也有自己的缺点:

  • 低优先级的读操作可阻塞高优先级的写操作
  • 宽限期可能比较长

这是由于RCU 写操作完毕后,会等待读端的完毕,等所有的读操作完毕后,宽限期结束,此时写端才会将资源释放。这里没有区分优先级,因此低优先级的读操作可能会影响到高优先级的写操作。

其实,RCU技术的特点还不仅如此,而有一个更重要的特点:多核扩展性。RCU虽然我们将其称之为RCU锁,但它并没有采用锁技术,而读写锁则是一个真正的锁,在扩展性上很差,设备CPU核越多,锁的竞争会越激烈,效率会越低。因此在现在很多的实现中,越来越注重无锁技术的实现。 下图是在文章What is RCU? Part 2: Usage中的一张RCU和rwlock在多核扩展性上对比图片:
请添加图片描述

RCU中常见问题

1. 有没有使用引用计数

关于RCU实现原理还有一个误解:RCU采用了引用计数的方式确定是否存在读者。 这个观点也是错的。

首先RCU并没有采用引用计数的机制,而是采用了一种非常简单的技术来实现;其次如果非要深究到底有没有引用计数,按照What is RCU? Part 2: Usage中的说法是“RCU is a Restricted Reference-Counting Mechanism”,但RCU本质上是没有采用引用计数的机制。它的解释如下:

“rcu_read_lock() 语句可以被认为是获取对 p 的引用,相当于一个引用计数。因为在 rcu_dereference() 分配给 p 之后开始的宽限期不可能在我们到达匹配的 rcu_read_unlock() 之前结束。 这种引用计数方案受到限制,因为我们不允许在 RCU 读端临界区中阻塞,也不允许我们将 RCU 读端临界区从一个任务切换到另一个任务“。

虽然有点像引用计数,但绝对不是
请添加图片描述

至于它怎么实现:检测是否有人在引用当前变量呢? 我在下面介绍下

2. RCU既然没有使用引用计数,那又是确定是否存在读者引用之?

RCU 读锁加锁和去锁最基本的函数是:

#define rcu_read_lock()		preempt_disable()

#define rcu_read_unlock()	preempt_enable()

加锁实际上是禁止上下文切换;而解锁是允许上下文切换。它们是一个全局设置,不与任何一项锁绑定。这也是为啥RCU读锁不需要任何参数的原因。

基于此,便可以通过让CPU进行一次上下文切换来实现检测读端是否完成,而不必跟踪每一个引用的进程。 这是一个RCU非常重要的特点
请添加图片描述
进行这个操作是:synchronize_rcu(), 这个函数在内核中实现有点复杂,它毕竟还需要考虑中断,热插拔等因素。如果只考虑RCU部分,它的功能可以概括为:

第一个函数用来遍历所有的CPU;第二行run_on()函数用来将当前线程切换到指定的CPU上。如果这个任务顺利完成,则说明所有的核已经经历过一次上下文切换,此刻必定读端已经结束,否则无法触发切换。

关于这里,我曾经有一个疑问:假如最初CPU1读端结束,执行了一次上下文切换;然后在等待其他CPU过程中,又再次进入RCU读临界区,如果此时释放,会导致严重后果吗? 有点类似于下图(黄色部分表示可以进行上下文切换)
请添加图片描述
不过后来想明白了。以替换一个节点为例进行说明:

  • 链表最初状态如下:
    请添加图片描述

  • <font — color="#0000ff">插入一个元素时,先复制一个副本,再次基础上修改,然后完成发布
    请添加图片描述从此刻开始,rcu宽限期也开始。在发布之前的RCU读端访问的是2号节点(如果有的话),发布之后,新来的RCU读端读取的是2_new新节点。这是RCU的一个重要特点,实时性很好

    这里还没有完,旧的2号节点还没有被释放。

  • 更新操作(资源回收)

那么什么时候释放呢? 当然是所有使用2号节点的读端都完成了再释放

那么什么时候所有的读端完成呢?就是刚才提到的,synchronize_rcu()函数的任务。此函数返回后我们就可以释放了(异步释放也差不多,只不过通过回调函数来完成此项操作)

再回到刚才的问题:如果CPU都已经完成了一次上下文切换,准备进行资源释放时(例如将上面的2节点释放),其他CPU又重新进入临界区怎么办?

答案是:再次进入临界区的读端获取的是新的节点2_new, 原来的节点2已经不能再被新的读者访问到了。

这种检查原理上简单了很多,但也导致了rcu宽限期比较长。毕竟这个上下文切换是个全局设置,得照顾到所有的CPU。

3. RCU 读端和写端同时操作,不会触发段错误吗?

这个问题也是我当初百思不得其解的地方。
请添加图片描述场景:

  • 读端已经进入到1号节点
  • 写端已经复制了2号节点2_new,完成了数据更新,并且将2_new的后继指针指向了3号节点
  • 写端开始修改1号节点的后继指针,从2号节点切换到2_new节点;但是读端也正好在1号节点的后继节点,此时会出现后继节点为空导致链表遍历提前结束的情况吗?

这个问题困扰了很久。不过当我再次读What is RCU, Fundamentally?时,发现了上面有这个问题的回答。最初看时竟然没有注意到这个问题。

请添加图片描述简单的说:在Linux系统中,所有的加载和存储操都是原子的,不可能被分割。针对上述场景,读端要么读取到2节点,要么读取到2_new节点。不会出现第三结果。

4. RCU 与资源回收

请添加图片描述

最初怎么也不会将这两个联系起来。但是慢慢发现有那么一丁点像

RCU会等待资源不再被引用时释放对应的资源。从这么一点看还确实有点像GC的赶脚

不过他们最大的区别在于(从资源回收上看):程序员必须手动指定RCU读端临界区资源,甚至手动指定释放的位置。

最初怎么也不会将这两个联系起来。但是慢慢发现有那么一丁点像

RCU会等待资源不再被引用时释放对应的资源。从这么一点看还确实有点像GC的赶脚

不过他们最大的区别在于(从资源回收上看):程序员必须手动指定RCU读端临界区资源,甚至手动指定释放的位置。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叨陪鲤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值