
大家好,我是情报小哥~
写多线程代码时,保护共享数据,第一反应就是加锁。
但有一类场景很特别:读得特别多,写得特别少。
比如内核的路由表、文件系统的目录缓存,一秒钟可能被读几百万次,却难得改一次。
如果每次读都要拿锁、放锁,性能就全耗在锁竞争上了。
Linux 内核专门为这种场景设计了一套机制,叫 RCU。
全称 Read-Copy-Update,翻译过来就是"读-复制-更新"。
核心思想一句话:
读操作几乎零开销、不加锁;写操作通过"复制-修改-替换"完成,旧数据等所有读者都走了再回收。

(图1:RCU 核心思想——读者无锁直读,写者复制副本、替换指针、旧数据待回收)
读者无需加锁,只是进入临界区时标记一下"我进去了",出来时再标记一下"我出来了"。
这个动作,比拿一把锁便宜太多。
那开销去哪了?转嫁给写者了。写者怎么干活,下一节说。
写者的思路是:绝不在原处改,另起炉灶。
一共五步:

(图2:写者五步流程——复制、修改、发布、等宽限期、回收)
第 3、4 步是 RCU 最精巧的地方。
发布之后,新读者马上能看到新数据;正在读的老读者,还拿着旧数据。
新旧两版共存一段时间,直到最后一个老读者离开。
这个窗口,就叫宽限期(Grace Period)。
这里得说清一个前提:第 3 步的"马上看到",并不是简单的指针赋值。
发布动作的本质是"写屏障 + 原子替换"。
写者发布前,要保证副本里的数据都写完了;读者拿到新指针后,也要保证读到的是完整新数据。
内核里对应两个 API:rcu_assign_pointer() 和 rcu_dereference()。
有个坑要记住:x86 是强内存模型,硬件基本兜底;但 ARM 这类嵌入式多核是弱内存模型,读者侧的屏障省不得。否则可能出现"拿到新指针,却读到旧数据的半截"。
原理听着不难,真要在用户态实现,宽限期的检测是个大麻烦。
先用读写锁模拟下,看清 RCU 的接口和生命周期。
思路是这样的:读者持读锁,写者持写锁。
写锁天然排他,写者一拿到写锁,就说明没有读者还待在临界区里,拿它来充当"宽限期"。
严格说这不是真 RCU:读者仍要加锁,写者会阻塞读者。
但"复制-修改-发布-回收"的骨架一模一样,足够理解 RCU 怎么工作。
完整代码 rcu_sim.c:
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<pthread.h>#include<unistd.h>/* 被保护的数据结构 */structconfig {int timeout;char message[64];};/* 全局共享指针,读者通过它访问数据 */structconfig *g_config = NULL;/* 保护指针替换和旧数据回收的读写锁 */pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER;/* ---- RCU 风格的接口(模拟版) ---- *//* 读者:获取一个不可变的引用 */struct config *rcu_read_lock(void){ pthread_rwlock_rdlock(&g_rwlock);return g_config; /* 在锁内读取,保证可见性 */}voidrcu_read_unlock(void){ pthread_rwlock_unlock(&g_rwlock);}/* 写者:更新全局配置 */intrcu_update_config(int new_timeout, constchar *new_msg){/* 1. 复制 */structconfig *new_cfg = malloc(sizeof(structconfig));if (!new_cfg) {fprintf(stderr, "malloc failed\n");return-1; }/* 若旧配置存在,先复制过来(模拟部分更新) *//* 注意:仅单写者演示下安全,多写者需把复制移入锁内 */if (g_config) {memcpy(new_cfg, g_config, sizeof(struct config)); } else {memset(new_cfg, 0, sizeof(struct config)); }/* 2. 修改副本 */ new_cfg->timeout = new_timeout;strncpy(new_cfg->message, new_msg, sizeof(new_cfg->message) - 1); new_cfg->message[sizeof(new_cfg->message) - 1] = '\0';/* 3. 发布:获取写锁,原子替换指针,释放旧数据 */ pthread_rwlock_wrlock(&g_rwlock);structconfig *old_cfg = g_config; g_config = new_cfg; /* 原子发布(在锁保护下) *//* 4. 宽限期:写锁被持有 → 没有读者持有旧指针,可以安全回收 */if (old_cfg) {free(old_cfg); } pthread_rwlock_unlock(&g_rwlock);return0;}/* ---- 测试线程 ---- */void *reader_thread(void *arg){long id = (long)arg;int i;for (i = 0; i < 5; i++) {structconfig *cfg = rcu_read_lock();/* 临界区:安全读取数据 */printf("Reader %ld: timeout=%d, msg='%s'\n", id, cfg->timeout, cfg->message);/* 模拟处理时间 */ usleep(100000); rcu_read_unlock(); usleep(50000); }returnNULL;}void *writer_thread(void *arg){int i;for (i = 0; i < 3; i++) {char new_msg[64];snprintf(new_msg, sizeof(new_msg), "update-%d", i);printf("Writer: updating to timeout=%d, msg='%s'\n", 100 + i, new_msg); rcu_update_config(100 + i, new_msg); sleep(1); /* 给读者时间观察变化 */ }returnNULL;}intmain(){pthread_t readers[3], writer;long i;/* 初始化第一个配置 */structconfig *init_cfg = malloc(sizeof(structconfig)); init_cfg->timeout = 30;strcpy(init_cfg->message, "initial"); g_config = init_cfg;/* 创建线程 */for (i = 0; i < 3; i++) { pthread_create(&readers[i], NULL, reader_thread, (void*)i); } pthread_create(&writer, NULL, writer_thread, NULL);/* 等待结束 */for (i = 0; i < 3; i++) { pthread_join(readers[i], NULL); } pthread_join(writer, NULL);/* 清理最后一份数据 */free(g_config); pthread_rwlock_destroy(&g_rwlock);return0;}这样写者每发布一次新值,读者下一轮就能读到新数据,而正在读的老读者手上还是旧数据。
这就是 RCU 的"新旧并存"。
如果有内核中的RCU对比,有如下几点差异:

代价不小。
那为什么内核态又相对比较容易呢?
内核 RCU 之所以简单,靠的是特权。打个比方:
内核像一家纪律严明的公司内部图书馆。读者进门必须打卡且严禁打瞌睡(禁止抢占),因此必定很快离开。管理员(调度器)只需扫一眼系统,就能瞬间知道“旧书已没人看了”(宽限期结束),回收旧书毫无风险。
用户态则像完全开放的公共图书馆。读者随时可能看一半就出门接电话(被系统暂停),你却无权阻止。管理员管不到每个人,写者只能自己费力挨个去问“谁还在看旧书”,而这偏偏极不可靠。
技术上看,内核的读者只需轻量级的 preempt_disable,调度器在每次线程切换时都能天然捕捉到“不在读”的时刻,宽限期检测直接内建。而用户态失去了这种全局监控能力,必须让读者主动打报告(显式静默态),并用复杂的内存屏障或信号来猜测状态,实现自然变得困难重重。
真 RCU 到了用户态,宽限期怎么检测、内存顺序怎么保证、旧内存怎么安全回收,全得自己来。
每一件都不省心。
好在有现成的轮子:liburcu(Userspace RCU)。
它提供了 QSBR、内存屏障、信号等多种后端,直接调它的 API 就行。
想深入研究的,可以从 liburcu 的源码看起。
星标公众号,第一时间看文章!
小哥搜集了一些嵌入式学习资料,公众号内回复【1024】即可找到下载链接!
推荐好文点击蓝色字体即可跳转
☞专辑|Linux应用程序编程大全 ☞ 专辑|学点网络知识 ☞ 专辑|手撕C语言 ☞ 专辑|手撕C++语言 ☞ 专辑|经验分享 ☞ 专辑|从单片机到Linux ☞ 专辑|电能控制技术 ☞ 专辑|嵌入式必备数学知识 ☞ MCU进阶专辑
☞ 经验分享