RCU (Read, Copy, Update)
RCU is a library that allows kernel subsystems to synchronize access to shared data in an efficient manner.
QEMU 也有自己的 RCU 实现。QEMU RCU implementation
Optimized for read-mostly situations. RCU supports concurrency between a single updater and multiple readers. 如果要用多个 updater,需要额外安排锁来进行同步(spinlock, etc.)。
参考了数据库中 MVCC (Multi-version concurrency control) 的设计思路,给 object 设定多个版本。其实相当于是一个对于读写锁(rwlock)的优化,至于为什么读锁和写锁需要互斥,之前的文章有介绍(为什么读锁和写锁要互斥?^)。
RCU 的本质是,把之前非原子性更新的问题,改成了进行原子性操作的问题。(而写者要更新一个新版本,它首先创建新的版本,然后保存旧版本的指针,然后让指针指向新版本,这个切换(指针赋值)是一个原子操作)。
我们可以把对于数据的写看作是一个很长的旅途,假设一个 object 有 100 个 porperties,需要对每一个 property 进行更新,所以耗时比较长。在过程中会有人来读,如果任凭随意读,那么读出来就是中间状态。如果上 rwlock,那么在写的过程中没有人能读,对性能不好。因此,我们设计另一个指针指向更新前的 object,在更新过程中让大家都读在这个 object 上,然后更新完直接对指针切换即可,因为指针切换(rcu_assign_pointer()
)会是一个原子操作(not by nature,而是我们加了内存屏障 smp_mb()
来保证其原子性,因为指针并不是一个 byte,可以原子性更新),所以不用担心。
RCU 的极其强大之处在于,它可以等待几千个不同的事情完成,而不需要显式的追踪它们。和自旋锁等不同,RCU 不用定义一个类似 spinlock_t 的变量来显式的追踪每一个 RCU 保护区。RCU 是全局的,这是由 RCU 的设计决定的,这也是 RCU 不同于其它同步机制的一个显著特点。
RCU 有以下三个基本要素:Reader/Updater/Reclaimer。
- Reader:
- 使用
rcu_read_lock()
和rcu_read_unlock()
来界定读者的临界区,访问受 RCU 保护的数据时,需要始终在该临界区域内访问; - 在访问受保护的数据之前,需要使用
rcu_dereference()
来获取 RCU-protected 指针; - 当使用不可抢占的 RCU 时,
rcu_read_lock/rcu_read_unlock
之间不能使用可以睡眠的代码; - Reader 不管读旧指针还是新指针,读就好了,为什么需要临界区? 这是为了告诉 reclaimer 现在有 reader 在读,先延缓一下 reclaim,不然可能处理到一半发现数据被 reclaim 掉了?所以需要所有 reader 都退出之后,reclaimer 才能进行 reclaim。
- 使用
- Updater:
- RCU 机制是面向 single updater 的,当有多个 Updater 更新数据时,需要额外的互斥机制进行保护(比如 spinlock);
- Updater 使用
rcu_assign_pointer()
来移除旧的指针指向,指向更新后的临界资源; - Updater 使用
synchronize_rcu()
或call_rcu
来启动 Reclaimer,对旧的临界资源进行回收,其中synchronize_rcu
表示同步等待回收,call_rcu
表示异步回收;因此,在调用synchronize_rcu()
时,Updater 和 Reclaimer 是同一个线程;在调用call_rcu()
时,两者仍然是同一个线程。
- Reclaimer:
- Reclaimer 回收旧的临界资源;
- 为了确保没有读者正在访问要回收的临界资源,Reclaimer 需要等待所有的读者退出临界区,这个等待的时间叫做宽限期(Grace Period);
- 需要澄清的是 Reclaimer 需要等的是所有在其之前进入临界区(
rcu_read_lock()
)的 reader 要离开临界区,在 Reclaimer 等的过程中,如果有新的 Reader 进入了临界区,那么不需要等它退出,因为这个 reader 读到的已经是最新的数据了,和我们要 reclaim 的老数据并不冲突。
【原创】Linux RCU原理剖析(一)-初窥门径 - LoyenWang - 博客园
这篇文章讲的比较详细:内核RCU原理和用法 - ILD
What is RCU? -- "Read, Copy, Update" — The Linux Kernel documentation
What is RCU, Fundamentally? [LWN.net]
一个写的不错的 RCU 例子:
RCU Reclaimer
Grace period
从 Updater 调用 synchronize_rcu()
开始,到 Reclaimer 等待所有的读者退出临界区(rcu_read_unlock()
)的时间叫做宽限期(Grace Period)。那是不是在 synchronize_rcu()
开始后,就不能有新的 reader 来了?新的 reader 会 block 在 rcu_read_lock()
?
SRCU (Sleepable RCU)
Permits arbitrary sleeping (or blocking) within RCU read-side critical sections.
CONFIG_PROVE_RCU
This config is not configurable.
RCU Core APIs
rcu_assign_pointer(p, v)
- @p: pointer to assign to
- @v: value to assign (publish)
This function can be used to replace gp = p;
with rcu_assign_pointer(gp, p);
.
Writer 使用这个接口来更新指针,从而更新一个版本。
rcu_dereference()
rcu_assign_pointer()
's counterpart.
Reader 用这个来获取一个版本。注意,要在临界区内使用这个函数,也就是说这个函数要放在 lock 和 unlock 中间:
rcu_read_lock();
rcu_dereference();
rcu_read_unlock();
rcu_read_lock()
/ rcu_read_unlock()
/ RCU read-side critical section
It is illegal to block while in an RCU read-side critical section, though kernels built with CONFIG_PREEMPT_RCU
can preempt RCU read-side critical sections.
Basically do nothing. rcu_read_lock()
prevents timer interrupts from forcing a pre-emptive context switch on the current CPU, and rcu_read_unlock()
enables pre-emptive context switches. This enable/disable is very cheap.
RCU 的读者虽然拥有无限的优先级,但是读者在读时仍然需要有一个关键区:RCU read-side critical sections。关键区以 rcu_read_lock()
开始,以 rcu_read_unlock()
结束。RCU 需要创建关键去是因为 RCU 需要防止关键区被抢占等(依赖于 RCU 的类型),rcu_read_lock()
的作用是关闭抢占、软中断等。RCU 的关键区可以嵌套,可以包含非常多的代码,只要代码不显式的阻塞或者睡眠。内核要根据“是否发生过切换”来判断读者是否已结束读操作。但是也有一种特殊的 RCU 叫做 SRCU,允许在 SRCU 的关键区睡眠。
总之,在进入这个关键区时(rcu_read_lock()
),并不会因为别人目前拿着锁而阻塞,这个关键区的作用可能仅仅就是避免被抢占而已。
为什么在读的时候需要关闭抢占?
Any RCU-protected data structure accessed during an RCU read-side critical section is guaranteed to remain unreclaimed for the full duration of that critical section. 确保在关键区内,要读的数据没有被 reclaim 掉。因为如果被抢占从而调度出去了被 reclaim 掉了,那么回来再访问数据就没有了。只有在所有读者离开了它们的 RCU read-side critical section 之后,数据才会被 reclaim。
这么做也可能是为了支持下面这个机制:当 Updater 更新数据之后,所有的 CPU 核都发生了一次上下文切换之后(那么肯定都已经 rcu_read_unlock()
过了,不然是没法被抢占的),那么所有对旧版本的数据的访问都已结束,此时可以安全的释放旧版本的数据。但是不知道这个机制是因还是果:
- 是为了支持这个机制,所以才让设计一个不能抢占的关键区;
- 还是说因为关键区不能 block,推导出了这个机制,可以用来作为释放旧版本的依据。
synchronize_rcu()
Synchronize_rcu() doesn't return until all cores have gone through at least one context switch.
call_rcu()
call_rcu(f,x)
returns immediately, after adding <f,x>
to a list of callbacks. The callback is called after all cores have gone through at least one context switch.
Will asynchronously invokes a specified callback after all CPUs have passed through at least one context switch.