配置指针已经换成新对象,旧读者却还没结束。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() 必须按相应读侧或受保护条件使用,不能只因为用了这个宏就认为任意对象都安全。
2. Grace period 等待的不是世界上所有读者
移除旧对象后,一个合适的 grace period 保证相关的、此前可能持有旧对象的读侧临界区已经结束。新读者可以继续进入,系统不需要等到“所有 CPU 同时没有任何 RCU 读者”的瞬间。
对象必须先从所有相关的发布入口撤销,读者也必须只按约定入口取得它。若另一条链表仍指向旧对象,或读者把裸指针带出临界区继续使用,单独等待一个 grace period 不会修复寿命错误。
宽限期不是从指针赋值那条指令开始固定计时,也不是固定几毫秒。调度、配置、读侧持续时间及实现都会影响等待。同步等待可以睡眠;异步回调避免让更新者同步阻塞,但会积累尚未回收对象。
读者可在替换前后进入,也可退出后再次进入。下面跟踪的是每次读侧区间的身份:等待旧的一批结束,不意味着阻止所有后来的读者。
正在呈现知识画面
宽限期等的是哪一批读侧临界区?撤销旧入口、等待读侧区间、执行回收回调是三个不同边界。新读者可继续进入;任务名字相同,不意味着临界区属于同一批。
{"id":"rcu-reader-generations","title":"宽限期等的是哪一批读侧临界区?","summary":"撤销旧入口、等待读侧区间、执行回收回调是三个不同边界。新读者可继续进入;任务名字相同,不意味着临界区属于同一批。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"enterA\" data-i18n=\"enterA\"></button><button id=\"exitA\" data-i18n=\"exitA\"></button></div><div class=\"actions\"><button id=\"enterB\" data-i18n=\"enterB\"></button><button id=\"exitB\" data-i18n=\"exitB\"></button></div><div class=\"track-row\"><span data-i18n=\"entry\"></span><div id=\"entry\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"readers\"></span><div id=\"readers\" class=\"track\"></div></div><div class=\"actions\"><button id=\"publish\" data-i18n=\"publish\"></button><button id=\"start\" data-i18n=\"start\"></button></div><div class=\"track-row\"><span data-i18n=\"wait\"></span><div id=\"wait\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"objects\"></span><div id=\"objects\" class=\"track\"></div></div><div class=\"card\"><header data-i18n=\"phase\"></header><div id=\"phase\" class=\"readout\"></div></div><div class=\"actions\"><button id=\"callback\" data-i18n=\"callback\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.track#readers,.track#wait{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.track#wait .cell{min-height:64px}.readout{min-height:5.3em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let entry='O',readers=[null,null],serial=0,snapshot=null,alive=true;function done(){return snapshot!==null&&snapshot.every(v=>!readers.some(r=>r&&r.token===v.token));}function draw(){cells('entry',[{text:'entry → '+entry,cls:'on'}]);cells('readers',readers.map((r,i)=>({text:'AB'[i]+' → '+(r?r.token+' → '+r.object:'—'),cls:r?'on':'empty'})));cells('wait',snapshot===null||!snapshot.length?[{text:'—',cls:'empty'},{text:'—',cls:'empty'}]:[0,1].map(i=>{const v=snapshot[i],pending=v&&readers.some(r=>r&&r.token===v.token);return{text:v?v.token+(pending?' …':' ✓'):'—',cls:pending?'gold':'empty'};}));cells('objects',[{text:'O '+(alive?'●':'×'),cls:alive?'gold':'empty'},{text:'N '+(entry==='N'?'●':'—'),cls:entry==='N'?'on':'empty'}]);for(let i=0;i<2;i++){$('#enter'+'AB'[i]).disabled=!!readers[i];$('#exit'+'AB'[i]).disabled=!readers[i];}$('#publish').disabled=entry==='N';$('#start').disabled=entry!=='N'||snapshot!==null;$('#callback').disabled=!done()||!alive;set('phase',t(!alive?'reclaimed':done()?'done':snapshot===null?'initial':'waiting'));set('explanation',t('note'));}for(let i=0;i<2;i++){$('#enter'+'AB'[i]).onclick=()=>{readers[i]={token:'AB'[i]+(++serial),object:entry};draw();};$('#exit'+'AB'[i]).onclick=()=>{readers[i]=null;draw();};}$('#publish').onclick=()=>{entry='N';draw();};$('#start').onclick=()=>{snapshot=readers.filter(Boolean).map(r=>({...r}));draw();};$('#callback').onclick=()=>{if(done())alive=false;draw();};$('#reset').onclick=()=>{entry='O';readers=[null,null];serial=0;snapshot=null;alive=true;draw();};draw();","audio":false,"strings":{"callback":"执行 O 的回收回调","done":"宽限期完成,回调尚未执行","enterA":"A 进入读侧","enterB":"B 进入读侧","entry":"当前发布入口","exitA":"A 退出读侧","exitB":"B 退出读侧","heading":"宽限期等的是哪一批读侧临界区?","initial":"尚未开始宽限期","intro":"让 A、B 分别进入或退出读侧,发布新对象后开始宽限期。退出后立即重新进入,观察新的区间为何不接替旧区间继续阻挡回收。","note":"试着让 A 持有 O,发布 N,再让 B 持有 N,然后开始宽限期。开始时的两个区间都要结束;此后 A 新进入的区间不会延长这一批等待。","objects":"对象内存:O / N","phase":"回收阶段","publish":"发布 N,撤销 O 入口","readers":"读者 → 区间 token → 对象","reclaimed":"O 已回收","reset":"重置","scope":"两个无嵌套读者、一个串行写者,O/N 发布后不可变,只从当前入口取指针且不带出区间。宽限期快照包含开始时所有活跃 token;后进入的 token 不加入。实际 RCU 通过实现特有的静止状态/任务跟踪证明条件,不维护这张教学 token 表。","scopeLabel":"模型范围与约定","start":"开始回收 O 的宽限期","wait":"本次宽限期固定等待的区间","waiting":"宽限期等待中"}}
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;
}
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_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 警告与同步调试。