RCU:发布、宽限期与回收

本页目录

配置指针已经换成新对象,旧读者却还没结束。RCU 把入口更新与旧内存回收分开处理。

查阅目标阅读位置
发布与宽限期第 1~3 节
配置替换与回调排空第 4~5 节
SRCU、逃逸引用与 stall第 6~8 节

RCU 把“从共享结构中移除对象”和“释放对象内存”分开。读者可以在较低同步开销下访问已发布对象;更新者撤销旧对象的可达入口后,等待适当的宽限期,才允许回收旧内存。

它不要求每次更新都复制整个结构,也不自动给读者一个全局最新、跨多个对象一致的快照。RCU 更像一组关于发布和寿命的协议,需要与数据结构自身的更新规则共同设计。

1. 三项责任必须分别落实

责任常见机制不负责什么
发布初始化后的对象rcu_assign_pointer 等不串行多个更新者
读侧取得并使用指针rcu_read_lock + rcu_dereference不保证对象字段永不被其他机制修改
撤销后延迟回收synchronize_rcu / call_rcu / kfree_rcu不保证已逃逸引用全部消失

__rcu 注解有助于静态检查,并不是运行时自动引用计数。rcu_dereference() 必须按相应读侧或受保护条件使用,不能只因为用了这个宏就认为任意对象都安全。

撤销旧指针之后,等待已有读者再回收 读者 A 更新者 读者 B 持有旧对象 结束读侧 发布新指针 可读取新对象 适当宽限期后释放旧对象 无需等待所有未来读者;发布、写者串行化与旧对象回收是不同责任。

2. Grace period 等待的不是世界上所有读者

移除旧对象后,一个合适的 grace period 保证相关的、此前可能持有旧对象的读侧临界区已经结束。新读者可以继续进入,系统不需要等到“所有 CPU 同时没有任何 RCU 读者”的瞬间。

对象必须先从所有相关的发布入口撤销,读者也必须只按约定入口取得它。若另一条链表仍指向旧对象,或读者把裸指针带出临界区继续使用,单独等待一个 grace period 不会修复寿命错误。

宽限期不是从指针赋值那条指令开始固定计时,也不是固定几毫秒。调度、配置、读侧持续时间及实现都会影响等待。同步等待可以睡眠;异步回调避免让更新者同步阻塞,但会积累尚未回收对象。

读者可在替换前后进入,也可退出后再次进入。下面跟踪的是每次读侧区间的身份:等待旧的一批结束,不意味着阻止所有后来的读者。

正在呈现知识画面
宽限期等的是哪一批读侧临界区?

撤销旧入口、等待读侧区间、执行回收回调是三个不同边界。新读者可继续进入;任务名字相同,不意味着临界区属于同一批。

3. 可抢占 RCU 与 quiescent state

不能统一说 rcu_read_lock() 总是 preempt_disable()。在可抢占 RCU 配置中,读侧任务可以被抢占,内核会跟踪相关读者;不同实现的读侧指令开销也不同。

普通 RCU 读侧应遵守不可任意阻塞的 API 规则;被调度器抢占与主动调用可能睡眠的操作不是一回事。需要在读侧正常睡眠时应考虑 SRCU 等适当接口,而不是自行插入 schedule。

quiescent state 是实现确认相关读侧已经过去的证据,可能涉及用户态、idle、上下文切换等状态;并不要求所有 CPU 在每个 grace period 内都必须发生一次上下文切换。被抢占的 RCU 读者也不能只因 CPU 切走就算已经退出。见 What is RCU。

4. 一个可替换配置对象

以下是内核组件的独立翻译单元示例,不包含注册入口。对象发布后 value 不再原地修改,写者用 mutex 串行,读者只把标量复制出去。

#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>

struct kb_config {
    int value;
    struct rcu_head rcu;
};

static DEFINE_MUTEX(config_lock);
static struct kb_config __rcu *current_config;

int kb_config_get(int *value);
int kb_config_replace(int value);
void kb_config_shutdown(void);

static void free_config(struct rcu_head *head)
{
    struct kb_config *p = container_of(head, struct kb_config, rcu);
    kfree(p);
}

int kb_config_get(int *value)
{
    struct kb_config *p;
    int ret = -ENOENT;

    rcu_read_lock();
    p = rcu_dereference(current_config);
    if (p) {
        *value = p->value;
        ret = 0;
    }
    rcu_read_unlock();
    return ret;
}

int kb_config_replace(int value)
{
    struct kb_config *next, *old;

    next = kmalloc(sizeof(*next), GFP_KERNEL);
    if (!next)
        return -ENOMEM;
    next->value = value;

    mutex_lock(&config_lock);
    old = rcu_replace_pointer(current_config, next,
                              lockdep_is_held(&config_lock));
    mutex_unlock(&config_lock);
    if (old)
        call_rcu(&old->rcu, free_config);
    return 0;
}

/* 调用方必须先关闭所有入口并等待正在执行的 API 调用退出。 */
void kb_config_shutdown(void)
{
    struct kb_config *old;

    mutex_lock(&config_lock);
    old = rcu_replace_pointer(current_config, NULL,
                              lockdep_is_held(&config_lock));
    mutex_unlock(&config_lock);
    synchronize_rcu();
    kfree(old);
    rcu_barrier(); /* 等待此前排队、会执行本组件代码的回调。 */
}

value 输出地址由调用者保证有效,不应指向需要缺页处理的用户指针。shutdown 的入口关闭与调用退出等待是外部组件职责:仅加 config_lock 不能阻止一个尚未执行到加锁位置的 replace 在 shutdown 后重新发布。

写者之间,RCU 不代替互斥

设两个写者都读到旧对象 O,再分别发布 N1、N2。如果它们各自回收自己读到的 O,就可能重复回收同一对象。示例用更新侧 mutex 串行化这段“取得旧值、替换、安排回收”的操作。

5. 同步等待与异步回调

call_rcu() 接收嵌入的 rcu_head,回调参数就是 struct rcu_head *;通过 container_of 找回外层对象。kfree_rcu(p, rcu) 是常见释放接口,但不能解释为把 kfree 强制转换成 callback 指针,二者参数和实现并不相同。

call_rcu() 的回调应遵守不可任意睡眠的执行约束。回调只在宽限期之后具备执行资格,并不保证宽限期一结束就在某个固定 CPU 立即运行。

模块退出:读者结束与回调执行完毕不是同一个条件

等待建立什么保证
synchronize_rcu等待读者宽限期,不保证此前全部 call_rcu 回调已经执行完
先阻止新回调,再 rcu_barrier等待相应已提交回调排空,避免卸载代码仍被调用

模块回调位于模块自身代码中时,必须先解决新提交入口,再等待回调完成,最后才能卸载。见 RCU 与可卸载模块。

异步更新速率很高时,还需控制待回收内存增长。expedited grace period 可减少某些等待,但增加系统干扰,不能作为所有 RCU stall 的通用修复。

6. SRCU 与 Tasks RCU

SRCU 使用独立的 srcu_struct 域。读者获取的索引 token 必须传回同一域的 unlock:

int idx = srcu_read_lock(&domain);
/* 按 SRCU 协议取得并使用对象;在允许条件下可以睡眠。 */
srcu_read_unlock(&domain, idx);

更新者使用 synchronize_srcu(&domain) 或对应 callback API。不能从自己的同域读侧临界区中等待包含自身的宽限期,也不能在回收完成前销毁域。不同域的读者和等待不是互相通用的。

RCU Tasks、Tasks Rude、Tasks Trace 面向特殊任务/追踪同步需求,等待条件不同。它们不是一个叫“trace 专用关抢占 RCU”的单一变体。没有相应底层需求时,不应仅因名字包含 Tasks 就替换普通 RCU。

7. 读侧结束后继续使用怎么办

如果需要在退出读侧后长期持有对象,可在临界区内按对象协议尝试取得独立引用,并确认成功;回收与从零获取的规则必须一起设计。简单把裸指针存入全局或异步 work,再退出 RCU 读侧,会失去原本的寿命保障。

RCU 也不自动序列化同一对象字段的并发更新。不可变对象替换是较易推理的模式;需要原地更新时,必须额外说明锁、原子操作和读者可接受的一致性。

8. RCU stall 应按证据分析

stall 表示 RCU 在预期时间内没有获得必要进展,原因可能是长读侧、长时间屏蔽中断/抢占、CPU 饥饿、定时器或调度异常等。不能看到告警就断言一定漏写 rcu_read_unlock。

结合报告中的 CPU/task、grace period、重复栈和运行配置定位。提高 stall 超时只改变报告阈值,不修复进展问题。见 RCU stall 警告与同步调试。