3. RCU 核心 API 详解
RCU(Read-Copy-Update)同步机制的核心在于其“宽限期”(Grace Period)概念与读写分离的设计。本章深入剖析四个最关键的 RCU 接口:rcu_read_lock、rcu_read_unlock、synchronize_rcu 与 call_rcu,理解它们的语义、执行流与协作关系是掌握 RCU 锁机制的基础。
3.1 读者端 API:rcu_read_lock / rcu_read_unlock
读者端 API 是 RCU 性能优势的直接体现。它们不涉及原子操作、内存屏障或锁竞争,在大多数架构上仅编译为轻量级的内存序标记,甚至为空操作。
| API | 原型 | 核心语义 | 开销特征 |
|---|---|---|---|
rcu_read_lock |
void rcu_read_lock(void) |
标记进入 RCU 读端临界区,禁止内核抢占(非自愿调度) | 极低,通常仅禁用抢占 |
rcu_read_unlock |
void rcu_read_unlock(void) |
标记退出 RCU 读端临界区,恢复内核抢占 | 极低,通常仅启用抢占 |
关键约束:在
rcu_read_lock 与 rcu_read_unlock 之间的临界区内,不允许执行可能导致睡眠的操作(如 kmalloc(GFP_KERNEL)、mutex_lock、copy_from_user)。这是 RCU 读者必须遵守的硬规则。
以下是一个典型的使用模式,读者通过 RCU 保护指针的读取:
struct foo __rcu *gp;
struct foo *p;
rcu_read_lock();
p = rcu_dereference(gp); // 获取受 RCU 保护的指针
if (p) {
// 安全访问 p 指向的数据结构
do_something_with(p);
}
rcu_read_unlock();
// 此时 p 可能已被回收,不能再使用
3.2 写者端同步 API:synchronize_rcu
synchronize_rcu 是写者最常用的同步等待接口。它阻塞当前写者进程,直到所有在调用时刻之前已经开始的 RCU 读端临界区全部完成。这是 RCU 实现“安全回收”的关键屏障。
核心机制:
synchronize_rcu 等待一个完整的“宽限期”(Grace Period)。宽限期的结束意味着所有先前存在的读者都已经离开临界区,此时写者可以安全地释放旧数据。
典型用法:
struct foo *old, *new;
new = alloc_foo();
// 初始化 new
spin_lock(&foo_mutex);
old = rcu_dereference_protected(gp, lockdep_is_held(&foo_mutex));
rcu_assign_pointer(gp, new); // 发布新指针
spin_unlock(&foo_mutex);
synchronize_rcu(); // 等待所有旧读者退出
kfree(old); // 安全释放旧数据
性能警告:
synchronize_rcu 是阻塞调用,其等待时间取决于系统中 RCU 读者的最长执行时间。在实时或高吞吐场景中,频繁调用可能导致写者延迟显著增加。此时应考虑使用 call_rcu 回调机制。
3.3 写者端异步 API:call_rcu
call_rcu 提供了非阻塞的回收方式。写者注册一个回调函数,当宽限期结束后,该回调会被内核的 RCU 软中断(RCU_SOFTIRQ)异步执行。写者无需等待,可以立即返回继续处理其他任务。
| API | 原型 | 行为 | 执行上下文 |
|---|---|---|---|
call_rcu |
void call_rcu(struct rcu_head *head, rcu_callback_t func) |
注册回调,宽限期结束后执行 func |
回调在软中断上下文执行,不可睡眠 |
synchronize_rcu |
void synchronize_rcu(void) |
阻塞等待宽限期结束 | 进程上下文,可睡眠 |
异步回收示例:
struct foo {
int data;
struct rcu_head rcu; // 内嵌 RCU 回调头
};
static void foo_reclaim(struct rcu_head *rp)
{
struct foo *p = container_of(rp, struct foo, rcu);
// 在软中断中安全释放
kfree(p);
}
// 写者侧
old = rcu_dereference_protected(gp, ...);
rcu_assign_pointer(gp, new);
call_rcu(&old->rcu, foo_reclaim); // 注册回调,立即返回
// 写者继续执行其他工作,无需等待
3.4 API 协作流程与宽限期
下图展示了读者、写者与宽限期之间的完整协作关系:
3.5 API 选择决策树
在实际开发中,根据场景选择合适的写者回收 API 至关重要:
- 场景 A:写者频率低,对延迟不敏感 → 使用
synchronize_rcu,代码简单直观。 - 场景 B:写者频率高,或写者处于性能关键路径 → 使用
call_rcu,避免阻塞。 - 场景 C:需要确保回收操作在特定进程上下文中执行 → 使用
synchronize_rcu。 - 场景 D:回收操作简单(如 kfree) → 可使用
kfree_rcu宏,进一步简化。
最佳实践:在绝大多数设备驱动和内核模块中,推荐优先使用
call_rcu 或 kfree_rcu 来避免写者路径的阻塞。仅在原型验证或低频率回收场景中使用 synchronize_rcu。
3.6 常见错误与避坑指南
| 错误模式 | 后果 | 正确做法 |
|---|---|---|
| 在 RCU 读端临界区内睡眠 | 系统死锁或 RCU stall 警告 | 确保临界区内不调用任何可能睡眠的函数 |
在 call_rcu 回调中调用 synchronize_rcu |
软中断上下文阻塞,导致内核崩溃 | 回调中只执行原子操作或使用异步回收链 |
忘记使用 rcu_dereference 访问 RCU 指针 |
内存序问题,读取到过期数据 | 始终使用 rcu_dereference 进行读取 |
在 synchronize_rcu 之后继续使用旧指针 |
UAF(释放后使用)漏洞 | 宽限期结束后立即置空或释放旧指针 |