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 协作流程与宽限期

下图展示了读者、写者与宽限期之间的完整协作关系:

RCU 核心 API 协作流程与宽限期 时间 rcu_read_lock() 读端临界区 rcu_read_unlock() rcu_read_lock() 读端临界区 rcu_read_unlock() rcu_assign_pointer() synchronize_rcu() kfree(old) 宽限期(Grace Period):等待所有已有读者退出 读者1在写者 发布前已退出 读者2在写者 发布后仍活跃 宽限期必须 等待读者2退出

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(释放后使用)漏洞 宽限期结束后立即置空或释放旧指针