当前位置:首页>Linux>彻底搞懂Linux内核的RCU锁:读者无锁,写的艺术

彻底搞懂Linux内核的RCU锁:读者无锁,写的艺术

  • 2026-10-10 19:48:29
彻底搞懂Linux内核的RCU锁:读者无锁,写的艺术

大家好,我是大禾。

每天进步一点点

今天介绍RCU锁。

在Linux内核的并发编程世界里,读写锁(RW Lock)曾是保护共享数据的“主力军”。但随着多核CPU的普及,一个令人头疼的问题浮出水面:当“读”远多于“写”时,读写锁本身的维护开销(缓存抖动、原子操作)成了性能瓶颈。

怎么办?内核开发者祭出了一把“屠龙刀”——RCU(Read-Copy-Update,读-拷贝-更新)。

今天,我们就用一张思维导图的脉络,带你一图搞懂RCU的核心精髓。


一、为什么需要RCU?传统锁的痛点

想象一下图书馆的登记册(共享数据):

  • 传统加锁方式:无论是管理员修改登记册,还是读者查阅,都得先抢到一把“大铁锁”。哪怕只是瞄一眼,也得排队等锁。

  • 痛点:在“读多写少”的场景(例如文件查找、路由表查询)下,99%的操作是读,但读操作依然要承担加锁、解锁的开销。这好比每次进图书馆都要过安检,效率极低。

RCU的革命性在于:让读者(Reader)彻底摆脱锁的束缚,畅行无阻;而写者(Writer)则悄悄复制修改,等没人用了再销毁旧数据。


二、RCU核心思想:读者无锁,写者“借尸还魂”

RCU的精髓可以总结为两个字:等待。

它不是通过锁来阻止并发,而是通过等待所有正在进行的读操作完成,来安全地回收旧数据。

  • 读者(Reader):进入临界区时调用 rcu_read_lock(),退出时调用 rcu_read_unlock()。注意,这里的 lock/unlock 并不是真的加锁,而是用来标识一个“读侧临界区”(RCU读端临界区)的开始和结束。

  • 写者(Writer):

    1. Copy(复制):复制一份旧数据,在新副本上修改。

    2. Update(更新):使用原子操作,把全局指针从指向旧数据,猛地切换到指向新数据。

    3. Reclaim(回收):调用 synchronize_rcu() 或 kfree_rcu()。这一步是关键——它不会立即释放内存,而是会阻塞等待,直到所有在指针切换前进入读临界区的读者都退出,才真正释放旧数据。


三、时间线图解:多CPU并发下的“时空交错”

假设有三个CPU同时在干活:

  • CPU0:正在读取旧数据(进入了RCU读临界区)。

  • CPU1:正在读取旧数据(也进入了RCU读临界区)。

  • CPU2(写者):此刻开始更新数据。

时间线推演:

  1. 写入时刻:CPU2 复制旧数据 -> 修改完成 -> 原子替换全局指针。

    • 此时,指针已经指向新数据了。

  2. 关键分水岭:

    • CPU3(新来的读者):我不管,我看到指针指向哪我就读哪。CPU3直接读取到新数据,无任何阻塞。

    • CPU0和CPU1(老读者):我们不管指针变不变,我们手里拿的是进入临界区之前的旧数据指针,我们继续安心读完旧数据。

  3. 静默期(Grace Period):CPU2 调用 synchronize_rcu() 进入休眠。内核会监测 CPU0 和 CPU1 是否退出了读临界区。

  4. 回收时刻:当 CPU0 和 CPU1 都执行完 rcu_read_unlock() 后,内核唤醒 CPU2。此时,CPU2 知道全世界已经没有人在看旧数据了,于是放心地释放(free)旧内存。


四、代码实战:RCU保护链表操作

理论说再多,不如看代码。以下是RCU保护链表的典型写法:

1. 读者(无锁畅游)

struct list_head __rcu *head;void traverse_list(void){    struct list_head *p;    // 1. 标记进入RCU读临界区(仅仅是禁止抢占,不是加锁)    rcu_read_lock();    // 2. 遍历链表,注意使用 _rcu 版本的遍历宏    //    这里面不会加任何锁,并发读性能极高    list_for_each_entry_rcu(p, head, node) {        process(p); // 读取数据    }    // 3. 退出临界区    rcu_read_unlock();}

2. 写者(更新 + 延迟回收)

voidupdate_list(struct list_head *new_entry){    struct list_head *old;    // 1. 获取旧的链表头(受保护的数据)    old = rcu_dereference_protected(head, 1);    // 2. 原子地添加新节点到链表头部(更新指针)    list_add_rcu(new_entry, head);    // 3. 等待所有的读者离开临界区(阻塞等待)    //    这是RCU最核心的“延迟”所在    synchronize_rcu();    // 4. 静默期已过,安全地删除并回收旧节点    list_del_rcu(old);    // kfree_rcu 是 synchronize_rcu + kfree 的异步合并版本    kfree_rcu(old, rcu); }

五、关键数据结构:rcu_head 无处不在

你可能注意到了代码里的 kfree_rcu(old, rcu)。它之所以能延迟回收,是因为数据结构里内嵌了一个struct rcu_head。

struct rcu_head {    struct rcu_head *next;    void (*func)(struct rcu_head *head);};

当你调用 

  • kfree_rcu 时,旧对象并不会立刻被释放,而是被挂接到一个“回调链表”上。

  • 内核的软中断(Softirq)会在每次时钟中断或特定时机检查:如果“静默期”已过,就会遍历这个链表,调用 func 回调(通常是默认的释放函数)来真正归还内存。


六、RCU链表示意图(脑补画面)

想象一条双向链表(A <-> B <-> C):

  • Head 指向节点 A。

  • 写者要删除节点 B:

    1. 把 B 的 next 和 prev 指针断开,绕过 B,将 A 和 C 连起来(Update)。

    2. 调用 synchronize_rcu()。

    3. 此时,B 依然存在于内存中,物理上并未被释放。如果有读者正在遍历,它依然能通过 B 的指针走下去(因为断开指针是原子的,读者要么看到旧的 B,要么看到新的 C,绝不会看到中间态)。

    4. 等所有读者离开,B 才被加入“垃圾回收”队列。


七、小结:RCU 的取舍与辉煌

RCU 并非银弹,它是一种取舍的智慧:

  • 牺牲了写者的实时性(写者要等待一个不确定的“静默期”)。

  • 换取了读侧极致的高并发性能(读者不涉及任何原子操作、缓存行失效,几乎等同于纯内存访问)。

正是这种“读者无锁,写者延迟”的设计哲学,让 RCU 成为 Linux 内核中 路由表查找、文件系统目录缓存、进程管理 等高频读场景的基石。

一句话总结:RCU 让读者跑得像脱缰的野马,让写者学会在寂静中等待,最终在无人察觉时完成新老交替。

    参考:本文参考自 Linux 内核源码 Documentaion/RCU 及 Paul E. McKenney 经典著作。

最新文章

随机文章