Skip to main content

Linux Kernel 内核同步之 RCU

尽管 rwlock 相对于 spinlock 做了很大的改进,rwlock 允许多个读者同时并发访问。但是进行写操作时,仅有一个写者具有访问权限,读者不可访问。RCU 进一步改善了这个问题,其允许多个读者和写者并发执行。而且 RCU 是不使用锁的,其对读者几乎没有限制,访问效率极高。本文简单概述这项技术。

一、RCU 核心思想

读者无锁(Read),写者副本(Copy),延迟释放(Update)。

读者:

  • 调用 rcu_read_lock() / rcu_read_unlock() 包裹访问区域。
  • 读操作只需简单的内存屏障,不加锁,非常快,不会阻塞。

写者:

  • 写操作流程是 复制-修改-替换
    • 分配一份新的数据副本,在新副本上修改。
    • 使用 rcu_assign_pointer() 替换全局指针,让读者开始访问新版。

回收:

  • 旧版本不能立刻释放,因为可能仍有读者在 rcu_read_lock() 临界区访问它。
  • 写者调用 call_rcu()synchronize_rcu(),通知系统在 宽限期(grace period) 结束后释放旧版本。
  • 宽限期保证:所有读者都退出了临界区 → 没有线程再访问旧版本。

基于 RCU 的工作原理,RCU 只保护被动态分配并且通过指针引用的数据结构

二、RCU core API

The core RCU API is quite small:

  1. Updateer rcu_assign_pointer
struct foo {
int a;
int b;
int c;
};
struct foo *gp = NULL;

/* . . . */

p = kmalloc(sizeof(*p), GFP_KERNEL);
p->a = 1;
p->b = 2;
p->c = 3;
gp = p;

根据优化和内存屏障部分我们知道 11 12 13 14 行代码不一定按照我们想要的顺序执行,其很有可能先执行 14 行,再依次赋值。因此我们需要内存和优化屏障。Linux Kernel 已经封装了 rcu_assign_pointer 帮我们实现上述过程。其能保证下面代码先赋值 abc 结束后,再对 gp 赋值。

p->a = 1;
p->b = 2;
p->c = 3;
rcu_assign_pointer(gp, p);
  1. rcu_read_lock
  2. rcu_read_unlock
  3. rcu_dereference
p = gp;
if (p != NULL) {
do_something_with(p->a, p->b, p->c);
}

同样,我们在读取端也要有类似的操作,同时读取端还要通过上锁,保证访问内容暂时不要被回收(旧作业)。

rcu_read_lock();
p = rcu_dereference(gp);
if (p != NULL) {
do_something_with(p->a, p->b, p->c);
}
rcu_read_unlock();
  1. synchronize_rcu call_rcu 用于回收旧资源,启用 synchronize_rcu 是同步,其会 block 等待旧资源上所有的锁都释放,call_rcu 是异步。

三、RCU 基本流程

Ref

[1] What is RCU? -- “Read, Copy, Update” https://www.kernel.org/doc/html/next/RCU/whatisRCU.html

[2] What is RCU, Fundamentally? https://lwn.net/Articles/262464/