Linux 内核 RCU 机制学习与示例
1. 什么是 RCU?
RCU(Read-Copy-Update,读-拷贝-更新) 是 Linux 内核中一种高性能的同步机制,专为 读多写少 的场景设计。
核心思想:
- 读者几乎无锁(或极低开销),可并发读取
- 写者不原地修改共享数据,而是 拷贝一份 → 修改副本 → 原子替换指针
- 旧数据在所有读者退出临界区(grace period,宽限期)后再释放
RCU 不是传统意义上的“锁”,而是一套 发布-订阅 + 延迟回收 的协议。
时间线 ─────────────────────────────────────────────────────────►
读者 A: [── rcu_read_lock ── 读旧指针 ── rcu_read_unlock ──]
读者 B: [── rcu_read_lock ── 读旧指针 ── rcu_read_unlock ──]
写者: [分配新对象] [rcu_assign_pointer 替换] [synchronize_rcu 等待] [kfree 旧对象]
↑ grace period 结束
2. 为什么需要 RCU?
| 场景 | 传统读写锁 | RCU |
|---|---|---|
| 读者开销 | 加锁/解锁 | 极低开销(禁止抢占或轻量标记) |
| 读者并发 | 共享锁,可能竞争 | 读者之间几乎无竞争 |
| 写者开销 | 中等 | 较高(需等待 grace period) |
| 适用模式 | 读写均衡 | 读远多于写 |
典型应用:路由表、模块链表、文件系统 dentry、BPF map 遍历等。
3. RCU 三大规则
- 读侧:在
rcu_read_lock()与rcu_read_unlock()之间,读者看到的指针快照不会被写者“原地破坏” - 写侧:更新通过
rcu_assign_pointer()发布新指针,旧数据仍可能被旧读者访问 - 回收:写者必须等 grace period 结束后才能
kfree()旧对象(synchronize_rcu()或call_rcu())
4. 内核 RCU 核心 API
4.1 读侧临界区
rcu_read_lock(); /* 进入读侧临界区,禁止抢占(不可睡眠) */
p = rcu_dereference(head); /* 读取 RCU 保护的指针 */
/* 使用 *p ... */
rcu_read_unlock(); /* 退出读侧临界区 */
rcu_dereference():防止编译器重排,保证读到的是一致指针- 读侧 不可睡眠,不可阻塞
4.2 写侧发布
struct foo *new = kmalloc(sizeof(*new), GFP_KERNEL);
/* 初始化 new ... */
rcu_assign_pointer(head, new); /* 原子发布新指针 */
rcu_assign_pointer()等价于带内存屏障的指针写入
4.3 等待宽限期(同步回收)
synchronize_rcu(); /* 阻塞直到所有先前读者退出 */
kfree(old);
- 会 阻塞当前上下文,不可在中断/原子上下文中使用
4.4 异步回收
call_rcu(&old->rcu, foo_free_rcu); /* grace period 后回调释放 */
static void foo_free_rcu(struct rcu_head *head)
{
struct foo *p = container_of(head, struct foo, rcu);
kfree(p);
}
- 适合写路径不能长时间阻塞的场景
- Linux 5.13+ 可用
kfree_rcu(ptr, rcu)简化
4.5 辅助宏
#define rcu_dereference(p) READ_ONCE(p)
#define rcu_assign_pointer(p, v) (p = (typeof(p))(v))
LIST_FOR_EACH_ENTRY_RCU(pos, head, member) /* 安全遍历 RCU 链表 */
5. RCU 变体
| 类型 | 说明 |
|---|---|
| Classic RCU | 不可抢占读侧,最常用 |
| PREEMPT_RCU | 可抢占内核,读侧可用 rcu_read_lock() 被抢占 |
| SRCU | Sleepable RCU,读侧可睡眠,每域独立 |
| Tasks RCU | 跟踪用户态/内核线程而非抢占 |
本示例项目模拟 Classic RCU 语义。
6. 完整内核风格示例(链表查找 + 更新)
/* 查找(读侧) */
struct item *find_item(int key)
{
struct item *item;
rcu_read_lock();
list_for_each_entry_rcu(item, &head_list, list) {
if (item->key == key) {
rcu_read_unlock();
return item;
}
}
rcu_read_unlock();
return NULL;
}
/* 删除(写侧) */
void delete_item(int key)
{
struct item *item;
list_for_each_entry(item, &head_list, list) {
if (item->key != key)
continue;
list_del_rcu(&item->list);
synchronize_rcu();
kfree(item);
return;
}
}
/* 替换(写侧) */
void replace_item(struct item *new)
{
struct item *old;
list_for_each_entry(old, &head_list, list) {
if (old->key != new->key)
continue;
list_replace_rcu(&old->list, &new->list);
synchronize_rcu();
kfree(old);
return;
}
}
7. 常见陷阱
- 在读侧临界区内睡眠 → 宽限期永不结束,内存泄漏或死锁
- 未等 grace period 就 free → use-after-free
- 写侧原地修改读者可见字段 → 数据竞争(应 copy-update-replace)
- 在中断上下文调用 synchronize_rcu → 可能死锁
- 忘记 rcu_dereference → 编译器优化导致读到撕裂指针
8. 本示例项目说明
由于 RCU 依赖内核调度器与 per-CPU 状态,本仓库提供 用户态 RCU 模拟器,在 Linux/WSL 上运行,API 与内核命名一致,便于学习。
目录结构
rcu_test/
├── README.md # 本文档
├── Makefile
├── include/
│ └── rcu.h # RCU API 声明
└── src/
├── rcu.c # 用户态 RCU 实现(grace period 检测)
├── rcu_list.c # RCU 保护链表
├── rcu_list.h
└── main.c # 多线程演示
编译与运行
# 需要 Linux 或 WSL,以及 pthread
make
./rcu_demo
演示内容
- 多读者线程并发
rcu_read_lock+rcu_dereference读取全局链表 - 写者线程
rcu_assign_pointer替换头指针,或list_del_rcu+call_rcu异步释放 - 统计 grace period 等待时间与读侧并发安全性
9. 进一步阅读
- Kernel RCU documentation
Documentation/RCU/内核源码目录- Paul E. McKenney 的 RCU 系列文章