Linux 内核 RCU 完全解析:从"读、复制、更新"思想到核心 API、玩具实现与生产级源码
RCU(Read-Copy-Update,读、复制、更新)是 Linux 内核中专为"读多写少"场景设计的同步机制,诞生于 2.5 内核开发周期。本文基于内核树中 Documentation/RCU/whatisRCU.rst 的完整内容,并结合 include/linux/rcupdate.h 与 kernel/rcu/tree.c 中的真实实现,系统讲解 RCU 的更新三段式模型、五个核心 API、两种"玩具"实现、与读写锁/引用计数的类比,以及如何在生产代码中正确选用 synchronize_rcu() 与 call_rcu()。
1. RCU 总览:把更新拆成"移除"与"回收"两个阶段
RCU 的基本思想是将一次更新拆分为两个阶段:
- 移除阶段(removal):把指向某个数据项的引用从数据结构中摘掉(可以替换为新版本的引用)。移除阶段可以与读者并发执行,因为现代 CPU 的内存语义保证读者看到的要么是旧版本、要么是新版本的引用,绝不会是"更新了一半"的引用;
- 回收阶段(reclamation):真正释放(
kfree())被移除的数据项。由于释放内存可能干扰仍持有引用的并发读者,回收必须等"在移除阶段期间活跃的读者"全部完成之后才能开始。
之所以只需要关心"移除阶段期间活跃"的读者,是因为移除阶段之后才启动的读者根本不可能再获得指向被移除数据项的引用,自然不可能被回收阶段干扰。因此典型的 RCU 更新序列是:
a. 移除指向数据结构的指针,使后续读者无法再获得它的引用;
b. 等待所有在先的读者完成其 RCU 读侧临界区;
c. 此时不可能还有读者持有该数据结构的引用,可以安全回收(例如 kfree())。
步骤 (b) 正是 RCU"延迟销毁"的关键。正因为可以等所有读者做完再释放,RCU 读者可以使用极轻量的同步——在某些实现里读侧开销严格为零。相比之下,传统的基于锁的方案中,读者必须使用较重的同步来防止更新者把数据结构从它脚下删掉;因为锁式更新者通常原地更新数据项,必须排斥读者。而基于 RCU 的更新者利用"对单个对齐指针的写操作在现代 CPU 上是原子的"这一事实,可以在不打扰读者的前提下原子地插入、移除、替换链式结构中的数据项。并发的 RCU 读者可以继续访问旧版本,省掉了原子操作、内存屏障以及 SMP 系统上昂贵的跨核缓存一致性开销。
在上面的三步流程中,通常由更新者同时执行移除(步骤 a)与回收(步骤 c),但在 Linux 内核中常常由完全不同的线程来做回收——目录项缓存(dcache)就是典型例子。即使同一线程同时做更新与回收,把它们分开思考也有助于理解:RCU 读者与更新者之间不需要任何通信,但 RCU 在读者与回收者之间提供了隐式、低开销的通信,即步骤 (b)。
那么回收者如何知道读者何时完成?读者明明没有做任何同步操作?这正是 RCU API 要解决的问题。
2. RCU 核心 API:五个原语
RCU 的核心 API 非常小,一共五个:
a. rcu_read_lock()
b. rcu_read_unlock()
c. synchronize_rcu() / call_rcu()
d. rcu_assign_pointer()
e. rcu_dereference()
其余 API 都可以用这五个来表达(尽管多数实现是反过来用 call_rcu() 回调 API 来表达 synchronize_rcu())。
2.1 rcu_read_lock() / rcu_read_unlock()
void rcu_read_lock(void);
void rcu_read_unlock(void);
这两个"时间原语"(temporal primitive)用于向回收者通告读者进入/退出 RCU 读侧临界区。在 RCU 读侧临界区内禁止阻塞(除非内核以 CONFIG_PREEMPT_RCU 构建,此时允许抢占读侧临界区)。临界区内访问的 RCU 保护数据结构在整个临界区期间保证不会被回收;如需更长期的引用,可结合引用计数使用。
注意:
- 任何关闭半软中断(bottom halves)、禁止抢占或关闭中断的操作,都会隐式进入 RCU 读侧临界区;获取自旋锁同样会进入 RCU 读侧临界区——即使在
CONFIG_PREEMPT_RT=y的内核中,自旋锁并不禁止抢占; - 睡眠锁(sleeplock)不会进入 RCU 读侧临界区;
- 读侧临界区可以嵌套和/或重叠。
在源码中可以看到这些语义的落点:include/linux/rcupdate.h 中 rcu_read_lock() 定义为 static __always_inline void rcu_read_lock(void),非抢占内核下几乎为空操作;rcu_read_lock_bh()(行 899)与 rcu_read_lock_sched()(行 941)则分别封装了对应的抢占/BH 控制。
2.2 synchronize_rcu()
void synchronize_rcu(void);
这个时间原语标记"更新者代码的结束"与"回收者代码的开始":它阻塞,直到所有 CPU 上所有在先的 RCU 读侧临界区完成。注意 synchronize_rcu() 不保证等待在其之后才开始的临界区完成。原文档给出的经典时序:
CPU 0 CPU 1 CPU 2
----------------- ------------------------- ---------------
1. rcu_read_lock()
2. enters synchronize_rcu()
3. rcu_read_lock()
4. rcu_read_unlock()
5. exits synchronize_rcu()
6. rcu_read_unlock()
再次强调:synchronize_rcu() 只等待正在进行的 RCU 读侧临界区,不等待之后才开始的那些。同时它也不保证在最后一个在先临界区结束后立即返回:可能有调度延迟;许多 RCU 实现还会批量(batching)处理请求以提高效率,这会进一步延迟返回。
call_rcu() 是 synchronize_rcu() 的异步回调形式:不阻塞,而是注册一个函数和参数,在所有在先的 RCU 读侧临界区完成后被调用。这个回调形式特别适合"不允许阻塞"或"更新侧性能至关重要"的场景。但不应轻易使用 call_rcu()——synchronize_rcu() 通常带来更简单的代码,并且它有一个很好的性质:当宽限期(grace period)被延迟时会自动限制更新速率,从而在面对拒绝服务攻击时保持系统韧性;使用 call_rcu() 的代码需要自行限制更新速率(可参见 Documentation/RCU/checklist.rst 中的方法)。
源码印证:生产级实现位于 kernel/rcu/tree.c 的 synchronize_rcu()(行 3400 起)。函数头部注释明确写出了本文档所述的语义:返回后调用者可能与新的读侧临界区并发执行;v5.0 起,关闭中断/抢占/软中断的区域(含硬中断、软中断、NMI 处理程序)也构成读侧临界区;并且给出了比文档更进一步的内存排序保证(每个 CPU 自上一个在先临界区结束以来都执行过完整内存屏障等)。实现上它先通过 RCU_LOCKDEP_WARN() 禁止在读侧临界区内调用自己,再根据当前是否处于加急模式分派到 synchronize_rcu_expedited() 或 synchronize_rcu_normal();后者(可对照同文件中行 3351-3359 附近)先 start_poll_synchronize_rcu() 发起宽限期,再 wait_for_completion() 阻塞等待,正是"批量处理 + 完成量等待"的落地。单核/非 SMP 的极简实现则在 kernel/rcu/tiny.c。
2.3 rcu_assign_pointer()
void rcu_assign_pointer(p, typeof(p) v);
rcu_assign_pointer() 确实是一个宏("如果 C 允许这样声明函数就好了"是原文的感叹)。更新者用这个"空间原语"(spatial macro)给 RCU 保护指针赋新值,以安全地把变更从更新者传达给读者。它不求值出 rvalue,但提供当前编译器/CPU 架构所需的任何编译器指令与内存屏障指令。它的排序性质相当于一次 store-release:所有初始化该结构所需的前置读写,都被排序到"发布"该指针的写操作之前。
同样重要的一点:rcu_assign_pointer() 起到文档化作用——标记哪些指针受 RCU 保护、以及结构体在哪个点变得对其他 CPU 可见。实际代码中它大多通过 _rcu 系列链表操作原语(如 list_add_rcu())间接使用。源码中其宏定义位于 include/linux/rcupdate.h。
2.4 rcu_dereference()
typeof(p) rcu_dereference(p);
rcu_dereference() 同样必须实现为宏。读者用这个空间宏获取 RCU 保护指针,返回值随后可以安全地解引用。注意它并没有真正解引用指针,而是"保护该指针以供后续解引用",并执行当前 CPU 架构所需的内存屏障指令。文档指出:目前只有 Alpha 架构需要在 rcu_dereference() 内部使用内存屏障——其他 CPU 上它编译为一个 volatile load;但由于没有主流 C 编译器尊重地址依赖(address dependencies),所以它使用 volatile 强转,配合 Documentation/RCU/rcu_dereference.rst 中的编码准则,防止当前编译器破坏这些依赖。
常见编码实践是把 RCU 保护指针复制到一个局部变量再解引用:
p = rcu_dereference(head.next);
return p->data;
当然也可以合并成一句:
return rcu_dereference(head.next)->data;
如果要从 RCU 保护结构体中取多个字段,用局部变量更好。重复调用 rcu_dereference() 既难看,也不保证在临界区中发生过更新时仍返回同一个指针,还会在 Alpha CPU 上引入不必要的开销。
关键合法性约束:rcu_dereference() 的返回值仅在所包裹的 RCU 读侧临界区内有效。以下代码是非法的:
rcu_read_lock();
p = rcu_dereference(head.next);
rcu_read_unlock();
x = p->address; /* BUG!!! */
rcu_read_lock();
y = p->data; /* BUG!!! */
rcu_read_unlock();
把一个引用从一个 RCU 读侧临界区带到另一个临界区,与把一个基于锁的临界区引用带到另一个临界区一样非法。与 rcu_assign_pointer() 类似,rcu_dereference() 的重要职责之一也是文档化:标记哪些指针受 RCU 保护,并警示该指针随时可能改变——包括紧跟在 rcu_dereference() 之后。实践中它同样大多通过 list_for_each_entry_rcu() 等 _rcu 链表原语间接使用。
两个补充变体(原文脚注):
rcu_dereference_protected(p, lockdep表达式):可以在读侧临界区之外使用,前提是调用方持有了更新侧代码所获取的锁。它避免了rcu_read_lock()缺失时的 lockdep 告警,且允许rcu_dereference()必须禁止的编译器优化。若指示的保护未提供,会触发 lockdep splat。更多细节见 Documentation/RCU/Design/Requirements/Requirements.rst 与源码注释;list_for_each_entry_rcu()若可能被更新侧与 RCU 读者共同使用,可追加 lockdep 表达式参数(如lock_is_held(&mylock)),此时 lockdep 只在"既不在读侧临界区、又没有 mylock 保护"的调用处抱怨。
2.5 三类 RCU 用法变体
RCU 基础设施通过观察 rcu_read_lock()、rcu_read_unlock()、synchronize_rcu()、call_rcu() 调用的时间顺序,决定 (1) synchronize_rcu() 何时可以返回、(2) call_rcu() 回调何时可以被调用。高效的 RCU 实现大量使用批量处理来摊薄开销;而 rcu_assign_pointer() / rcu_dereference() 则通过对 RCU 保护指针的写与读,传达空间上的变更。五者之间的通信关系可概括为:rcu_assign_pointer() 服务于读者、更新者经 rcu_dereference() 读取;rcu_read_lock()/rcu_read_unlock() 保护读者;synchronize_rcu() 与 call_rcu() 推迟回收者介入。
Linux 内核中至少有三种 RCU 用法"风味"。更新侧使用的 rcu_assign_pointer()、synchronize_rcu()、call_rcu() 三种原语对三者完全相同,区别在读侧保护:
a. rcu_read_lock() / rcu_read_unlock() + rcu_dereference() —— 用于普通数据结构,绝大多数场景用这一种;
b. rcu_read_lock_bh() / rcu_read_unlock_bh()(以及 local_bh_disable()/local_bh_enable())+ rcu_dereference_bh() —— 用于可能遭受远程拒绝服务攻击的网络数据结构;
c. rcu_read_lock_sched() / rcu_read_unlock_sched()(以及 preempt_disable()/preempt_enable()、local_irq_save()/local_irq_restore()、硬中断进出、NMI 进出)+ rcu_dereference_sched() —— 用于调度器与中断/NMI 处理程序任务。
(b)、(c) 属于相对少见的专门用法。SRCU、RCU-Tasks、RCU-Tasks-Rude、RCU-Tasks-Trace 在各自行族中有着类似的原语关系。
3. 核心 API 实战:保护一个全局结构体指针
下面是一个使用核心 RCU API 保护"指向动态分配结构体的全局指针"的完整示例(原文示例完整继承,更多典型用法见 Documentation/RCU/listRCU.rst 与 Documentation/RCU/NMI-RCU.rst):
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo __rcu *gbl_foo;
/*
* Create a new struct foo that is the same as the one currently
* pointed to by gbl_foo, except that field "a" is replaced
* with "new_a". Points gbl_foo to the new structure, and
* frees up the old structure after a grace period.
*
* Uses rcu_assign_pointer() to ensure that concurrent readers
* see the initialized version of the new structure.
*
* Uses synchronize_rcu() to ensure that any readers that might
* have references to the old structure complete before freeing
* the old structure.
*/
void foo_update_a(int new_a)
{
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc_obj(*new_fp);
spin_lock(&foo_mutex);
old_fp = rcu_dereference_protected(gbl_foo, lockdep_is_held(&foo_mutex));
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
spin_unlock(&foo_mutex);
synchronize_rcu();
kfree(old_fp);
}
/*
* Return the value of field "a" of the current gbl_foo
* structure. Use rcu_read_lock() and rcu_read_unlock()
* to ensure that the structure does not get deleted out
* from under us, and use rcu_dereference() to ensure that
* we see the initialized version of the structure (important
* for DEC Alpha and for people reading the code).
*/
int foo_get_a(void)
{
int retval;
rcu_read_lock();
retval = rcu_dereference(gbl_foo)->a;
rcu_read_unlock();
return retval;
}
示例要点归纳:
- 用
rcu_read_lock()/rcu_read_unlock()保护 RCU 读侧临界区; - 在读侧临界区内用
rcu_dereference()解引用 RCU 保护指针; - 用某种扎实的机制(锁或信号量)防止并发更新彼此干扰;
- 用
rcu_assign_pointer()更新 RCU 保护指针——它保护的是读者不被更新者干扰,而不是更新者之间互不干扰!因此仍需加锁来串行化并发的rcu_assign_pointer(); - 在把数据元素从 RCU 保护结构体中移除之后、释放它之前调用
synchronize_rcu(),等待所有可能引用该元素的 RCU 读侧临界区完成。
示例中更新侧持 foo_mutex 自旋锁期间读取旧指针,用的是 rcu_dereference_protected(gbl_foo, lockdep_is_held(&foo_mutex))——正是 2.4 节所述"锁保护下可替代读侧临界区"的用法。
4. 更新线程不能阻塞时怎么办:call_rcu() 与 kfree_rcu()
上面的 foo_update_a() 会阻塞到宽限期结束。这很简单,但有些场景等不起——可能有其他高优先级工作要做。此时改用 call_rcu():
void call_rcu(struct rcu_head *head, rcu_callback_t func);
该函数在宽限期结束后调用 func(head)。这个调用可能发生在 softirq 或进程上下文中,因此回调函数不允许阻塞。结构体需要嵌入一个 rcu_head:
struct foo {
int a;
char b;
long c;
struct rcu_head rcu;
};
更新函数改写为:
/*
* Create a new struct foo that is the same as the one currently
* pointed to by gbl_foo, except that field "a" is replaced
* with "new_a". Points gbl_foo to the new structure, and
* frees up the old structure after a grace period.
*
* Uses rcu_assign_pointer() to ensure that concurrent readers
* see the initialized version of the new structure.
*
* Uses call_rcu() to ensure that any readers that might have
* references to the old structure complete before freeing
* the old structure.
*/
void foo_update_a(int new_a)
{
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc_obj(*new_fp);
spin_lock(&foo_mutex);
old_fp = rcu_dereference_protected(gbl_foo, lockdep_is_held(&foo_mutex));
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
spin_unlock(&foo_mutex);
call_rcu(&old_fp->rcu, foo_reclaim);
}
回收函数可以写成:
void foo_reclaim(struct rcu_head *rp)
{
struct foo *fp = container_of(rp, struct foo, rcu);
foo_cleanup(fp->a);
kfree(fp);
}
container_of() 是一个宏:给定结构体中某字段的指针、结构体类型、以及字段名,返回结构体起始处的指针。
使用 call_rcu() 使 foo_update_a() 的调用者立即重获控制权,无需再操心旧版本元素;它也让 RCU 中"更新者(foo_update_a())"与"回收者(foo_reclaim())"的区分清晰可见。
源码印证:call_rcu() 的声明在 include/linux/rcupdate.h(void call_rcu(struct rcu_head *head, rcu_callback_t func);),Tree RCU 的实现在 kernel/rcu/tree.c(行 3299 起,EXPORT_SYMBOL_GPL 于行 3303 附近),单核极简实现见 kernel/rcu/tiny.c。
如果 call_rcu() 回调做的只是 kfree(),可以直接用 kfree_rcu() 省掉自己写回调:
kfree_rcu(old_fp, rcu);
若偶尔多睡眠一下也无妨,可以用单参数形式,从而不必在结构体里嵌 rcu_head:
kfree_rcu_mightsleep(old_fp);
这个变体几乎从不阻塞,但可能在内存分配失败时通过调用 synchronize_rcu() 而阻塞。其宏定义见 include/linux/rcupdate.h:#define kfree_rcu(ptr, rhf) kvfree_rcu_arg_2(ptr, rhf) 与 #define kfree_rcu_mightsleep(ptr) kvfree_rcu_arg_1(ptr),最终汇入 kvfree_call_rcu()。
5. 两个"玩具"实现:从锁到经典 RCU
RCU 的一个美妙之处是存在极其简单的"玩具"实现,作为理解内核中生产级实现的第一步。两个实现都过于简陋、缺乏功能与性能,不能用于真实场景,但有助于建立直觉。生产级实现见 kernel/rcu/update.c 与 kernel/rcu/tree.c。
5A. 玩具实现 #1:基于读写锁
这个玩具实现基于熟悉的锁原语。开销让它无法用于真实场景,可扩展性差,且不适合实时系统——它允许调度延迟从一个读侧临界区"渗透"到另一个。它还假设递归读写锁:若用不可重入的锁并允许嵌套 rcu_read_lock(),会死锁。但它是与既有知识最容易对标的实现,是很好的起点:
static DEFINE_RWLOCK(rcu_gp_mutex);
void rcu_read_lock(void)
{
read_lock(&rcu_gp_mutex);
}
void rcu_read_unlock(void)
{
read_unlock(&rcu_gp_mutex);
}
void synchronize_rcu(void)
{
write_lock(&rcu_gp_mutex);
smp_mb__after_spinlock();
write_unlock(&rcu_gp_mutex);
}
rcu_assign_pointer() 与 rcu_dereference() 在玩具实现里可以忽略,不过简化版本如下(提交使用 RCU 的补丁时千万别忘记它们):
#define rcu_assign_pointer(p, v) \
({ \
smp_store_release(&(p), (v)); \
})
#define rcu_dereference(p) \
({ \
typeof(p) _________p1 = READ_ONCE(p); \
(_________p1); \
})
原理:rcu_read_lock()/rcu_read_unlock() 读获取/读释放一个全局读写锁;synchronize_rcu() 写获取同一把锁再释放。于是 synchronize_rcu() 退出后,所有在调用它之前已经开始的 RCU 读侧临界区必然已完成——否则它不可能拿到写锁。smp_mb__after_spinlock() 把 synchronize_rcu() 提升为完整内存屏障,以满足 Documentation/RCU/Design/Requirements/Requirements.rst 中列出的"内存屏障保证"。
由于读写锁可以递归获取,rcu_read_lock() 可以嵌套;且它免疫死锁(这是 RCU 的重要属性):唯一能阻塞 rcu_read_lock() 的是 synchronize_rcu(),而 synchronize_rcu() 在持有 rcu_gp_mutex 期间不获取任何其他锁,因此不存在死锁环。
快速问答 #1:上述"无死锁"论证为什么天真?在真实内核中可能出现怎样的死锁,如何避免?
答案:考虑如下事件序列:
- CPU 0 获取一个无关锁(称为
problematic_lock),通过spin_lock_irqsave()关闭了本地中断; - CPU 1 进入
synchronize_rcu(),写获取rcu_gp_mutex; - CPU 0 进入
rcu_read_lock(),因 CPU 1 持有rcu_gp_mutex而必须等待; - CPU 1 被中断,其中断处理程序试图获取
problematic_lock。
系统死锁。一种避免方式是 CONFIG_PREEMPT_RT 的思路:所有普通自旋锁都变成可阻塞锁,中断处理程序在专门任务的上下文中执行——此时第 4 步的中断处理程序会阻塞,CPU 1 得以释放 rcu_gp_mutex,死锁解除。
即便没有死锁,该实现也允许延迟"渗透":任务 A 在读侧临界区中(读持有 rcu_gp_mutex),任务 B 阻塞在写获取 rcu_gp_mutex 上,任务 C 阻塞在 rcu_read_lock() 中尝试读获取——任务 A 的读侧延迟经由任务 B 间接拖住了任务 C。实时 RCU 实现因此改用基于计数器的方法,使读侧临界区中的任务不会被执行 synchronize_rcu() 的任务阻塞。
5B. 玩具实现 #2:经典 RCU
这个玩具实现基于"经典 RCU",性能(仅限更新侧)和功能(如 CPU 热插拔、CONFIG_PREEMPTION 内核下的运行)同样不足。rcu_dereference() 与 rcu_assign_pointer() 的定义同前,此处省略:
void rcu_read_lock(void) { }
void rcu_read_unlock(void) { }
void synchronize_rcu(void)
{
int cpu;
for_each_possible_cpu(cpu)
run_on(cpu);
}
注意 rcu_read_lock()/rcu_read_unlock() 什么都不做。这是经典 RCU 在非抢占内核中的巨大优势:读侧开销精确为零(至少对非 Alpha CPU),而且 rcu_read_lock() 绝无可能参与任何死锁环!
synchronize_rcu() 的实现只是依次把自己调度到每个 CPU 上运行。run_on() 原语可以用 sched_setaffinity() 直接实现。当然,"不太玩具"的实现会在完成后恢复亲和性,而不是让所有任务都留在最后一个 CPU 上——但既然说了是"玩具",就是玩具!
它凭什么能工作?记住:在 RCU 读侧临界区内阻塞是非法的。因此,如果某个 CPU 执行了一次上下文切换,就可以断定它已完成了之前所有的 RCU 读侧临界区。一旦所有 CPU 都执行过上下文切换,所有在先的 RCU 读侧临界区就都完成了。于是,把数据项从结构中移除并调用 synchronize_rcu() 之后,等它返回时就保证不存在持有该数据项引用的读侧临界区,可以安全回收。
快速问答 #2:给出一个经典 RCU 读侧开销为负的例子。
答案:想象一个单 CPU 系统、非 CONFIG_PREEMPTION 内核,路由表被进程上下文代码使用、但可被中断上下文代码更新(例如由"ICMP REDIRECT"包触发)。常规做法是进程上下文代码在查路由表时关闭中断;使用 RCU 就可以省去这种关中断。于是没有 RCU 时要付出关中断的代价,有 RCU 则不用——相对于单 CPU 关中断方案,RCU 的开销可以说是负的。也有人认为开销只是零,把正开销的关中断方案换成零开销的 RCU 方案算不上负开销。现实中当然更复杂,但一个同步原语"理论上可能负开销"本身就够出乎意料的。
快速问答 #3:既然读侧临界区内禁止阻塞,那在普通自旋锁都可能阻塞的 CONFIG_PREEMPT_RT 下怎么办?
答案:CONFIG_PREEMPT_RT 允许抢占自旋锁临界区,同样允许抢占 RCU 读侧临界区,也允许在 RCU 读侧临界区内阻塞等待自旋锁。表面上的不一致性可以解释:必要时可以用优先级提升(priority boosting)保持 RCU 宽限期短(例如内存不足时);而如果在等待网络接收这类事件上阻塞,则无从知道该提升谁——需要提升的进程可能是一个出去吃披萨的人类,而计算机既难以"电击"人家,也不知道他去了哪家披萨店。
6. 与读写锁的类比
RCU 最常见的一种用法与读写锁非常类似。原文给出了一段 unified diff 展示两者的接近程度:
@@ -5,5 +5,5 @@ struct el {
int data;
/* Other data fields */
};
-rwlock_t listmutex;
+spinlock_t listmutex;
struct el head;
@@ -13,15 +14,15 @@
struct list_head *lp;
struct el *p;
- read_lock(&listmutex);
- list_for_each_entry(p, head, lp) {
+ rcu_read_lock();
+ list_for_each_entry_rcu(p, head, lp) {
if (p->key == key) {
*result = p->data;
- read_unlock(&listmutex);
+ rcu_read_unlock();
return 1;
}
}
- read_unlock(&listmutex);
+ rcu_read_unlock();
return 0;
}
@@ -29,15 +30,16 @@
{
struct el *p;
- write_lock(&listmutex);
+ spin_lock(&listmutex);
list_for_each_entry(p, head, lp) {
if (p->key == key) {
- list_del(&p->list);
- write_unlock(&listmutex);
+ list_del_rcu(&p->list);
+ spin_unlock(&listmutex);
+ synchronize_rcu();
kfree(p);
return 1;
}
}
- write_unlock(&listmutex);
+ spin_unlock(&listmutex);
return 0;
}
差异其实很小:读侧锁换成 rcu_read_lock()/rcu_read_unlock(),更新侧锁从读写锁换成普通自旋锁,kfree() 之前多一个 synchronize_rcu()。但要注意两个坑:
- 读侧与更新侧临界区现在可以并发执行,多数情况没问题,但必须仔细检查。例如多个独立的链表更新必须被看作一次原子更新时,转换为 RCU 需要特别小心;
synchronize_rcu()的存在意味着 RCU 版delete()现在可能阻塞。若这是问题,可以改用从不阻塞的回调机制call_rcu()或kfree_rcu()替代synchronize_rcu()。
7. 与引用计数的类比
读写锁类比并非总是最优思考方式。另一个有用的类比是:把 RCU 看作对所有 RCU 保护对象的有效引用计数。
引用计数通常不阻止被引用对象的值变化,但阻止"类型"变化——特别是对象内存被释放并重新分配后发生的那种剧烈类型变化。一旦获得类型安全的对象引用,就需要其他机制来保证数据的一致访问:可以加自旋锁,但在 RCU 中典型做法是:读用 SMP 感知操作(如 smp_load_acquire())、更新用原子读-改-写操作,并提供必要的排序。RCU 提供了一系列内嵌必要操作与排序的辅助函数,例如上一节的 list_for_each_entry_rcu()。
更聚焦的视角:在 rcu_read_lock() 与 rcu_read_unlock() 之间,对标记为 __rcu 的指针用 rcu_dereference() 取得的任何引用,都可以视为"该对象的引用计数被临时加一",从而阻止对象类型变化。这通常意味着:对象中的自旋锁仍可安全获取、普通引用计数器可安全操作、__rcu 指针可安全解引用。
持有 RCU 引用时对该对象可以做的操作包括:
- 拷贝出由对象类型保证稳定的数据;
- 用
kref_get_unless_zero()等获取更长期的引用(当然可能失败); - 获取对象中的自旋锁,确认对象仍是期望的对象后自由操作它。
"RCU 提供的引用只阻止类型变化"这一理解在从 SLAB_TYPESAFE_BY_RCU 标记的 slab cache 分配的对象上表现尤为明显:RCU 操作可能得到一个已被并发释放、内存被重新分配给"同一类型但完全不同"的对象的引用——此时 RCU 甚至不保护对象身份,只保护类型。找到的对象可能不是期望的那个,但一定可以安全地对其"取引用(再可能加自旋锁)",让后续代码检查身份是否匹配。想跳过取引用直接加自旋锁是诱人的,但 SLAB_TYPESAFE_BY_RCU 对象中的自旋锁必须在每次 kmem_cache_alloc() 之后初始化,这使得"无引用的直接加锁"完全不安全。因此使用 SLAB_TYPESAFE_BY_RCU 时必须正确使用引用计数:若用 refcount_t,应使用专门的 refcount_add_not_zero_acquire() / refcount_inc_not_zero_acquire() 与 refcount_set_release() API——前者的 acquire 栅栏保证身份检查发生在取引用之后,后者的 release 栅栏保证新值在他人能成功取引用之前已可见,一旦调用 refcount_set_release() 对象即应视为对其他任务可见。(愿意在 kmem_cache 构造器里初始化锁的人也可以直接用锁,包括对缓存友好的序列锁。)
与传统引用计数(如 Linux 的 kref 库)不同:kref 在最后一个引用被释放时运行传给 kref_put() 的终结函数;而使用 RCU 时,终结代码必须等到所有引用该对象的 __rcu 指针都更新完毕、且一个宽限期经过之后才能运行。对象每个仍全局可见的指针都要被视为潜在的计数引用,终结代码通常在所有这些指针都变更后、通过 call_rcu() 运行。
如何在这两个类比之间选择?想想被保护对象本身的规模:读写锁类比着眼于链表这类多部件大对象,展示 RCU 如何方便元素增删时的并发;引用计数类比着眼于单个对象,研究它在其所属整体中如何被安全访问。
8. RCU API 全列表与选型决策树
RCU API 在源码的 docbook 格式头注释中有文档,但有一份完整清单有助于检索。按类别列出(完整继承原文档清单):
RCU 链表遍历:list_entry_rcu、list_entry_lockless、list_first_entry_rcu、list_first_or_null_rcu、list_tail_rcu、list_next_rcu、list_next_or_null_rcu、list_for_each_entry_rcu、list_for_each_entry_continue_rcu、list_for_each_entry_from_rcu、list_for_each_entry_lockless、hlist_first_rcu、hlist_next_rcu、hlist_pprev_rcu、hlist_for_each_entry_rcu、hlist_for_each_entry_rcu_notrace、hlist_for_each_entry_rcu_bh、hlist_for_each_entry_from_rcu、hlist_for_each_entry_continue_rcu、hlist_for_each_entry_continue_rcu_bh、hlist_nulls_first_rcu、hlist_nulls_next_rcu、hlist_nulls_for_each_entry_rcu、hlist_nulls_for_each_entry_safe、hlist_bl_first_rcu、hlist_bl_for_each_entry_rcu。
RCU 指针/链表更新:rcu_assign_pointer、rcu_replace_pointer、INIT_LIST_HEAD_RCU、list_add_rcu、list_add_tail_rcu、list_del_rcu、list_replace_rcu、list_splice_init_rcu、list_splice_tail_init_rcu、hlist_add_behind_rcu、hlist_add_before_rcu、hlist_add_head_rcu、hlist_add_tail_rcu、hlist_del_rcu、hlist_del_init_rcu、hlist_replace_rcu、hlist_nulls_del_init_rcu、hlist_nulls_del_rcu、hlist_nulls_add_head_rcu、hlist_nulls_add_tail_rcu、hlist_nulls_add_fake、hlists_swap_heads_rcu、hlist_bl_add_head_rcu、hlist_bl_del_rcu、hlist_bl_set_first_rcu。
RCU 主族(读侧临界区 / 宽限期 / 屏障三列):
| 读侧临界区 | 宽限期 | 屏障 |
|---|---|---|
rcu_read_lock |
synchronize_net |
rcu_barrier |
rcu_read_unlock |
synchronize_rcu |
|
guard(rcu)() |
synchronize_rcu_expedited |
|
scoped_guard(rcu) |
synchronize_rcu_mult |
|
rcu_dereference |
call_rcu |
|
rcu_dereference_check |
call_rcu_hurry |
|
rcu_dereference_protected |
kfree_rcu |
|
rcu_read_lock_held |
kvfree_rcu |
|
rcu_read_lock_any_held |
kfree_rcu_mightsleep |
|
rcu_pointer_handoff |
cond_synchronize_rcu |
|
unrcu_pointer |
cond_synchronize_rcu_full |
|
cond_synchronize_rcu_expedited |
||
cond_synchronize_rcu_expedited_full |
||
get_completed_synchronize_rcu |
||
get_completed_synchronize_rcu_full |
||
get_state_synchronize_rcu |
||
get_state_synchronize_rcu_full |
||
poll_state_synchronize_rcu |
||
poll_state_synchronize_rcu_full |
||
same_state_synchronize_rcu |
||
same_state_synchronize_rcu_full |
||
start_poll_synchronize_rcu |
||
start_poll_synchronize_rcu_full |
||
start_poll_synchronize_rcu_expedited |
||
start_poll_synchronize_rcu_expedited_full |
bh 变体(宽限期与屏障同 RCU 族):rcu_read_lock_bh、rcu_read_unlock_bh、local_bh_disable 及同族、rcu_dereference_bh、rcu_dereference_bh_check、rcu_dereference_bh_protected、rcu_read_lock_bh_held。
sched 变体(宽限期与屏障同 RCU 族):rcu_read_lock_sched、rcu_read_unlock_sched、preempt_disable 及同族、rcu_read_lock_sched_notrace、rcu_read_unlock_sched_notrace、rcu_dereference_sched、rcu_dereference_sched_check、rcu_dereference_sched_protected、rcu_read_lock_sched_held。
RCU 初始化/清理/排序:RCU_INIT_POINTER、RCU_INITIALIZER、RCU_POINTER_INITIALIZER、init_rcu_head、destroy_rcu_head、init_rcu_head_on_stack、destroy_rcu_head_on_stack、SLAB_TYPESAFE_BY_RCU。
RCU 静止状态(quiescent state)与控制:cond_resched_tasks_rcu_qs、rcu_all_qs、rcu_softirq_qs_periodic、rcu_end_inkernel_boot、rcu_expedite_gp、rcu_gp_is_expedited、rcu_unexpedite_gp、rcu_cpu_stall_reset、rcu_head_after_call_rcu、rcu_is_watching。
RCU-sync 原语:rcu_sync_is_idle、rcu_sync_init、rcu_sync_enter、rcu_sync_exit、rcu_sync_dtor。
RCU-Tasks:读侧临界区 N/A;宽限期 call_rcu_tasks、synchronize_rcu_tasks;屏障 rcu_barrier_tasks。
RCU-Tasks-Rude:读侧临界区 N/A;宽限期 synchronize_rcu_tasks_rude、call_rcu_tasks_rude;屏障 rcu_barrier_tasks_rude。
RCU-Tasks-Trace:读侧临界区 rcu_read_lock_trace、rcu_read_unlock_trace、guard(rcu_tasks_trace)()、scoped_guard(rcu_tasks_trace);宽限期 call_rcu_tasks_trace、synchronize_rcu_tasks_trace;屏障 rcu_barrier_tasks_trace。
SRCU 链表遍历:list_for_each_entry_srcu、hlist_for_each_entry_srcu。
SRCU 主族:
| 读侧临界区 | 宽限期 | 屏障 |
|---|---|---|
srcu_read_lock |
call_srcu |
srcu_barrier |
srcu_read_unlock |
synchronize_srcu |
|
srcu_read_lock_fast |
synchronize_srcu_expedited |
|
srcu_read_unlock_fast |
get_state_synchronize_srcu |
|
srcu_read_lock_nmisafe |
start_poll_synchronize_srcu |
|
srcu_read_unlock_nmisafe |
start_poll_synchronize_srcu_expedited |
|
srcu_read_lock_notrace |
poll_state_synchronize_srcu |
|
srcu_read_unlock_notrace |
||
srcu_down_read |
||
srcu_up_read |
||
srcu_down_read_fast |
||
srcu_up_read_fast |
||
guard(srcu)() |
||
scoped_guard(srcu) |
||
srcu_read_lock_held |
||
srcu_dereference / srcu_dereference_check / srcu_dereference_notrace |
SRCU 初始化/清理/排序:DEFINE_SRCU、DEFINE_STATIC_SRCU、DEFINE_SRCU_FAST(配合 srcu_read_lock_fast() 等)、DEFINE_STATIC_SRCU_FAST、init_srcu_struct、init_srcu_struct_fast、cleanup_srcu_struct、smp_mb__after_srcu_read_unlock。
所有 RCU:lockdep 检查工具:RCU_LOCKDEP_WARN、rcu_sleep_check。
所有 RCU:无检查的 RCU 保护指针访问:rcu_dereference_raw。禁止解引用的无检查访问:rcu_access_pointer。
8.1 内核有不少于四族 RCU API,如何选?
原文给出的选型清单(按顺序判断):
a. 读者需要阻塞吗? 需要则必须用 SRCU;
b. 读者需要阻塞且你在做追踪(如 ftrace 或 BPF)?则需要 RCU-tasks、RCU-tasks-rude 和/或 RCU-tasks-trace;
c. 考虑 -rt 补丁集? 若读者在非 rt 内核中就需要阻塞,需要 SRCU;若读者只在 -rt 内核中因获取自旋锁而阻塞(-rt 把自旋锁变成睡眠锁,非 rt 则不会),则不需要 SRCU;
d. 需要把 NMI 处理程序、硬中断处理程序、以及禁止抢占的代码段(经 preempt_disable()、local_irq_save()、local_bh_disable() 或其他机制)视为显式 RCU 读者吗? 若是,只有 RCU-sched 读者可用;但自 v4.20 起可以使用原版 RCU 更新原语;
e. 需要 RCU 宽限期在某个/某些 CPU 被软中断独占时也能完成吗? 例如代码会遭受基于网络的拒绝服务攻击。若是,应在使用者一侧关闭软中断,例如使用 rcu_read_lock_bh();自 v4.20 起同样可使用原版 RCU 更新原语;
f. 负载对普通 RCU 来说更新过于密集,又不适合其他同步机制? 考虑 SLAB_TYPESAFE_BY_RCU(原名 SLAB_DESTROY_BY_RCU)。务必小心!
g. 需要即使在 CPU 深度空闲循环、进入/退出用户态、或 CPU 已下线时也被尊重的读侧临界区? 此时只有 SRCU 和 RCU Tasks Trace 可行,绝大多数情况强烈推荐 SRCU;
h. 否则,用 RCU。
以上当然都以"你已经确定 RCU 确实是这个任务的正确工具"为前提。
9. 快速问答(Quick Quiz)汇总与延伸资源
- 快速问答 #1(锁玩具实现的死锁):CPU 0 持
problematic_lock(spin_lock_irqsave()关了中断)→ CPU 1 进入synchronize_rcu()写获取rcu_gp_mutex→ CPU 0 的rcu_read_lock()被挡 → CPU 1 被中断、中断处理程序要拿problematic_lock——死锁。CONFIG_PREEMPT_RT式"自旋锁全部可阻塞 + 中断在专门任务上下文执行"可解;且该实现的延迟渗透问题促使实时 RCU 改用计数器方案。 - 快速问答 #2(负开销的例子):单 CPU、非抢占内核中,路由表由进程上下文查询、由中断上下文更新(ICMP REDIRECT)。无 RCU 时必须关中断查表,有 RCU 则可以免去——相对关中断方案,开销可以为"负"。
- 快速问答 #3(PREEMPT_RT 下为何允许在读侧临界区内阻塞):
CONFIG_PREEMPT_RT允许抢占 RCU 读侧临界区并允许在内核区阻塞等待自旋锁;理由是宽限期可以通过优先级提升保持短暂,而"等待网络包"这类阻塞无法确定该提升谁。
延伸阅读(仓库内文档,均位于 Documentation/RCU/):
- Documentation/RCU/index.rst——RCU 文档总索引;
- Documentation/RCU/listRCU.rst——链表上的 RCU 用法;
- Documentation/RCU/NMI-RCU.rst——NMI 安全的 RCU;
- Documentation/RCU/checklist.rst——使用 RCU 的规则清单与限制更新速率的方法;
- Documentation/RCU/lockdep.rst 与 Documentation/RCU/lockdep-splat.rst——lockdep 集成与告警解读;
- Documentation/RCU/Design/——数据结构(Data-Structures)、加急宽限期(Expedited-Grace-Periods)、内存排序(Memory-Ordering)、需求(Requirements)等设计文档;
- 源码入口:include/linux/rcupdate.h(API 定义)、kernel/rcu/update.c(宽限期推进)、kernel/rcu/tree.c(Tree RCU 主实现)、kernel/rcu/tiny.c(非 SMP 极简实现)。
原文档对 LWN 上的 "What is RCU?" 系列文章及 Linux Foundation 的网络研讨会亦有引用,可作为仓库文档之外的补充学习路径;本文则完整覆盖了仓库文档自身的概念脉络、API 语义、代码示例、类比分析、API 全清单与选型决策,并标注了每个论断在内核源码中的对应落点,可直接作为开发中查阅 RCU 的参考底稿。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00