futex 是 Linux 提供的低层等待与唤醒机制。常见互斥锁把无竞争路径留在用户态,只在需要阻塞或唤醒时进入内核。它不是一把完整的锁,也不是所有语言、平台或所有同步原语必经的唯一实现。
设计同步协议时,要分别回答:谁拥有资源、数据如何发布、谁需要睡眠、状态改变后谁应被唤醒。futex 主要解决后两项与共享状态之间的衔接。
为什么不能简单地先检查再睡眠
假设 W 检查 ready 为 0,正准备睡眠;P 随后把 ready 改为 1 并唤醒,但 W 此时还没入队。若 W 接着无条件睡眠,而之后再无通知,就会错过已经发生的变化。
FUTEX_WAIT 将读取 futex word、与 expected 比较、进入等待合成对相关并发操作有原子保证的“比较并阻塞”。若值已不相等,它返回 EAGAIN,不会再睡下去。FUTEX_WAIT
这要求发布方先修改状态,再按协议唤醒。单独调用 WAKE 而不留下可观察的状态,不能可靠地通知尚未等待的线程。值若反复变化回同一值,还需用代数、序号或更完整的协议区分代次,不能认为一次比较消除了所有 ABA 问题。
两个基本操作
| 操作 | 含义 | 返回后还要做什么 |
| FUTEX_WAIT(word, expected) | 仅当值仍匹配才入睡 | 重新检查条件或争取锁 |
| FUTEX_WAKE(word, n) | 最多唤醒 n 个匹配等待者 | 发布方仍需遵守共享状态协议 |
传统 futex word 是按 4 字节对齐的 32 位值,即使进程运行于 64 位平台。普通 WAIT 不替用户修改锁状态,WAKE 不交付锁所有权,也没有普遍的 FIFO 公平保证。futex(2)
WAIT 返回 0 只说明此次等待被唤醒,不能据此直接进入临界区。EAGAIN 表示比较时状态已改变,EINTR 表示被信号中断,超时也不表示业务条件必然不成立;这些路径都需回到同步协议处理。
FUTEX_WAIT 的 timeout 是相对时间,其他操作可能使用绝对截止时间。重试相对等待若每次重新给足原始时长,会意外延长总超时;应以同一截止时间计算剩余量,或使用适合的绝对时间接口。
唤醒没有存下来,WAIT 为什么仍不会漏睡?
先发布再 WAKE,唤醒数可以为 0;此后 WAIT 比较到 word=1,返回 EAGAIN 而不入睡。反过来,先 WAIT 再发布和 WAKE,等待者会出队。还可以只 WAKE、不发布:等待者虽醒来,word 仍为 0,说明唤醒与条件成立是两回事。
正在呈现知识画面
唤醒没有存下来,WAIT 为什么仍不会漏睡?WAKE 不保存未来通知,WAIT 通过比较当前 word 决定是否入队。被唤醒仍需重新检查条件,不能直接视为获得锁。
{"id":"futex-compare-and-block","title":"唤醒没有存下来,WAIT 为什么仍不会漏睡?","summary":"WAKE 不保存未来通知,WAIT 通过比较当前 word 决定是否入队。被唤醒仍需重新检查条件,不能直接视为获得锁。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"publish\" data-i18n=\"publish\"></button><button id=\"wake\" data-i18n=\"wake\"></button><button id=\"wait\" data-i18n=\"wait\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"track-row\"><span data-i18n=\"queue\"></span><div id=\"queue\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"word\"></span><strong id=\"word\"></strong></div><div><span data-i18n=\"returned\"></span><strong id=\"returned\"></strong></div><div><span data-i18n=\"woken\"></span><strong id=\"woken\"></strong></div></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/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.metrics strong{font-size:17px}#returned{min-height:4em}","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)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let word=0,waiting=false,result='none',woken=null;function draw(){set('word',word);set('returned',t(result));set('woken',woken??'—');cells('queue',[{text:waiting?'W':'∅',cls:waiting?'on':'empty'}]);$('#wait').disabled=waiting;$('#publish').disabled=word===1;$('#queue').dataset.waiting=waiting;}$('#publish').onclick=()=>{word=1;draw()};$('#wake').onclick=()=>{woken=waiting?1:0;if(waiting){waiting=false;result='check'}draw()};$('#wait').onclick=()=>{if(word===0){waiting=true;result='sleep'}else result='again';draw()};$('#reset').onclick=()=>{word=0;waiting=false;result='none';woken=null;draw()};draw();","audio":false,"strings":{"again":"EAGAIN:未入睡","check":"0:唤醒后重查","heading":"唤醒没有存下来,WAIT 为什么仍不会漏睡?","intro":"分别执行发布、WAKE 与 WAIT(expected=0),可以任意安排顺序。","none":"尚未调用","publish":"发布 word=1","queue":"等待队列","reset":"重置","returned":"最近 WAIT 结果","scope":"一个发布者与一个等待者,word 只由0变1。忽略超时与信号;不是完整互斥锁,数据发布仍需要适当内存序。","scopeLabel":"模型范围与约定","sleep":"等待中","wait":"WAIT(expected=0)","wake":"WAKE(1)","woken":"最近 WAKE 唤醒数","word":"共享 word"}}
一个三状态普通互斥锁模型
下面是解释算法的伪代码,不是 pthread_mutex_t 的内部布局或可直接替换 libc 的实现。状态 0 表示空闲,1 表示已锁定且尚无等待标记,2 表示已锁定并可能存在等待者。
lock:
若 CAS_acquire(state, 0, 1) 成功:返回
循环:
old = exchange_acquire(state, 2)
若 old == 0:返回
WAIT(state, expected=2)
按返回值处理错误后,重新竞争
unlock:
old = exchange_release(state, 0)
若 old == 2:WAKE(state, 1)
将状态标成 2,是让释放者知道可能需要唤醒,不是精确统计等待者数量。被唤醒线程成功从 0 改成 2 后保留标记,可让后续解锁继续推动其他等待者。普通新到线程可能抢在被唤醒线程前拿到锁,因此这个模型不保证公平。
若释放发生在 WAIT 前,比较不匹配避免漏睡;若等待先入队,后续 WAKE 把它唤起。算法还需要处理地址生命周期、取消、超时、异常退出和边界错误;这也是应用通常使用成熟 pthread 或语言标准库锁的原因。
被唤醒者可能仍然输给新到达线程
释放者先把 state 写为 0,再唤醒 W;W 还在等待调度时,新线程 N 已经 CAS(0, 1) 成功。W 运行后看到 state=1,仍须重新竞争,不能因 WAIT 返回 0 就直接进入临界区。
| 顺序 | state | 谁拥有锁 |
| P 仍在临界区,W 等待 | 2 | P |
| P 释放并唤醒 W | 0 | 暂时无人 |
| N 抢先 CAS 成功 | 1 | N |
| W 恢复运行,exchange 为 2 得到旧值 1 | 2 | 仍是 N |
| N 释放,看到旧值 2,继续唤醒 | 0 | 等待下一次竞争 |
状态 2 的价值在于保留“后续释放可能需要通知”的信息。它不是 W 的所有权凭证,也不是当前等待人数。这个调度解释了为什么普通 futex 锁可能有较好吞吐,却不能直接承诺等待公平或最坏等待时间。
比较不匹配时,WAIT 不会睡眠
下面只验证单线程 futex 接口的比较与返回,不实现并发锁。word 已为 1,却请求等待值 0;即使超时指针为 NULL,也应立即得到 EAGAIN。
#define _GNU_SOURCE
#include <errno.h>
#include <linux/futex.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
int main(void) {
_Alignas(4) uint32_t word = 1;
errno = 0;
long result = syscall(SYS_futex, &word, FUTEX_WAIT_PRIVATE,
0, NULL, NULL, 0);
int saved = errno;
printf("mismatch=%d\n", result == -1 && saved == EAGAIN);
return !(result == -1 && saved == EAGAIN);
}
保存为 futex-mismatch.c,编译运行预期 mismatch=1。此例通过 libc syscall,所以看到 -1 与 errno;它没有并发修改普通 uint32_t。将其扩展为多线程协议时,必须重新满足下面的原子对象与内存序约束。WAIT 比较语义
原子变量的内存序不能省略
锁保护的数据通常在 unlock 的 release 与成功 lock 的 acquire 之间建立同步关系。futex 的队列原子性不能替代语言内存模型中的数据发布协议,也不能把普通共享整数的并发读写自动变成合法原子操作。
volatile 只影响特定编译器访问语义,不能替代跨线程原子性和 acquire/release。若直接实现 futex 包装,还需确认原子对象的大小、对齐、无锁支持和目标 ABI 与内核接口兼容。
PRIVATE 标志表示等待双方在同一地址空间中使用该对象,允许内核优化查找。进程间共享对象须使用适合共享映射的操作,不能把 PRIVATE 当作无条件更快选项。销毁、解除映射或复用 futex 地址前,也必须确保没有旧操作仍引用它。
PI 与 robust 解决不同的问题
优先级继承 PI 针对低优先级持锁者被中优先级任务不断抢占、导致高优先级等待者无法前进的情况。内核可以沿等待链提升持锁者的有效优先级,帮助它完成临界区。PI 需要特定所有者编码与操作协议,不能随意混用普通 WAIT/WAKE 实现。
pthread 接口通过互斥锁属性选择 PTHREAD_PRIO_INHERIT。它不解决死锁、不缩短临界区本身,也不保证任意调度条件下达到截止时间。
Robust mutex 则处理持锁线程死亡:下一个 pthread_mutex_lock 可能返回 EOWNERDEAD,此时调用方已经获得锁,但保护的数据可能不一致。应用应修复数据并调用 pthread_mutex_consistent,再解锁。若在未恢复一致性的情况下解锁,后续可能得到 ENOTRECOVERABLE。
不能对 EOWNERDEAD 直接当作“加锁失败,没有拿到锁”,也不能不检查数据就一律标记 consistent。内核提供异常所有者状态,不会自动回滚业务修改。Robust mutex
完整恢复例子:用已提交副本恢复未完成更新
以下只在进程内创建一个工作线程。正常不变量为 working == committed;工作线程持锁把 working 改为 99 后直接结束,模拟未提交更新。恢复方选择“丢弃未提交内容”,将 working 恢复为 committed=7,再标记一致。
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
static pthread_mutex_t mutex;
static int committed = 7, working = 7;
static void require(int result) { if (result != 0) abort(); }
static void *unfinished(void *unused) {
(void)unused;
require(pthread_mutex_lock(&mutex));
working = 99;
return NULL;
}
int main(void) {
pthread_mutexattr_t attr;
require(pthread_mutexattr_init(&attr));
require(pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST));
require(pthread_mutex_init(&mutex, &attr));
require(pthread_mutexattr_destroy(&attr));
pthread_t thread;
require(pthread_create(&thread, NULL, unfinished, NULL));
require(pthread_join(thread, NULL));
int result = pthread_mutex_lock(&mutex);
if (result != EOWNERDEAD) abort();
if (working != 99 || committed != 7) abort();
working = committed;
require(pthread_mutex_consistent(&mutex));
require(pthread_mutex_unlock(&mutex));
require(pthread_mutex_lock(&mutex));
int recovered = working == 7 && working == committed;
require(pthread_mutex_unlock(&mutex));
require(pthread_mutex_destroy(&mutex));
printf("recovered=%d\n", recovered);
return !recovered;
}
保存为 robust-demo.c,用 cc -Wall -Wextra -pthread robust-demo.c -o robust-demo 编译。预期 recovered=1,且恢复后的下一次 lock 正常返回 0。Robust mutex 恢复协议
本例的提交副本没有被异常线程修改,因此恢复规则明确。复杂对象可能还涉及部分写入、链表结构和外部副作用;如果 committed 本身可能不完整,简单赋值就不构成有效恢复。应先定义可验证的不变量与恢复数据,再决定何时调用 consistent。
EOWNERDEAD:拿到锁了,数据却还没修好
先模拟退出再 lock,可以同时看到 EOWNERDEAD 与“恢复线程持锁”。比较两条后续路径:直接 unlock 会进入不可恢复;恢复 working、声明 consistent、再 unlock,下一次 lock 才正常成功。修复数据和声明一致是两个操作,少做后者也不能完成恢复协议。
正在呈现知识画面
EOWNERDEAD:拿到锁了,数据却还没修好EOWNERDEAD 表示获得锁但数据可能不一致。恢复不变量并声明 consistent 后可继续;未恢复一致状态就解锁,会使后续获得 ENOTRECOVERABLE。
{"id":"robust-mutex-recovery","title":"EOWNERDEAD:拿到锁了,数据却还没修好","summary":"EOWNERDEAD 表示获得锁但数据可能不一致。恢复不变量并声明 consistent 后可继续;未恢复一致状态就解锁,会使后续获得 ENOTRECOVERABLE。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"crash\" data-i18n=\"crash\"></button><button id=\"lock\" data-i18n=\"lock\"></button><button id=\"repair\" data-i18n=\"repair\"></button><button id=\"consistent\" data-i18n=\"consistent\"></button><button id=\"unlock\" data-i18n=\"unlock\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"data\"></h3><div id=\"data\"></div></section><div class=\"metrics\"><div><span data-i18n=\"owner\"></span><strong id=\"owner\"></strong></div><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div><div><span data-i18n=\"status\"></span><strong id=\"status\"></strong></div></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/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#data .tr{grid-template-columns:1fr 1fr}.metrics strong{font-size:16px}#status{min-height:5em}","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)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let owner='original',state='healthy',working=7,result='—';function draw(){table('data',[['committed',7],['working',working]]);set('owner',t(owner));set('result',result);set('status',t(state));$('#status').dataset.state=state;$('#crash').disabled=owner!=='original';$('#lock').disabled=owner!=='none';$('#repair').disabled=owner!=='recovery'||state!=='inconsistent'||working===7;$('#consistent').disabled=owner!=='recovery'||state!=='inconsistent'||working!==7;$('#unlock').disabled=owner!=='recovery';}$('#crash').onclick=()=>{working=99;owner='none';state='dead';draw()};$('#lock').onclick=()=>{if(state==='unrecoverable')result='ENOTRECOVERABLE';else {owner='recovery';result=state==='dead'?'EOWNERDEAD':'0';if(state==='dead')state='inconsistent'}draw()};$('#repair').onclick=()=>{working=7;draw()};$('#consistent').onclick=()=>{state='healthy';draw()};$('#unlock').onclick=()=>{owner='none';if(state==='inconsistent')state='unrecoverable';draw()};$('#reset').onclick=()=>{owner='original';state='healthy';working=7;result='—';draw()};draw();","audio":false,"strings":{"consistent":"声明 consistent","crash":"持锁者未提交就退出","data":"保护的数据","dead":"所有者死亡,待恢复","heading":"EOWNERDEAD:拿到锁了,数据却还没修好","healthy":"一致","inconsistent":"持锁但待声明一致","intro":"模拟持锁者写到一半退出,选择修复后解锁或直接解锁。","lock":"恢复方 lock","none":"无人","original":"原持锁者","owner":"当前所有者","recovery":"恢复线程","repair":"恢复 working=committed","reset":"重置","result":"最近 lock 结果","scope":"单个 robust mutex;提交副本 committed=7 完整可信,恢复规则为 working=committed。模型不允许虚假声明一致;这不是 API 自动检查业务数据的能力。","scopeLabel":"模型范围与约定","status":"互斥锁一致性状态","unlock":"恢复方 unlock","unrecoverable":"不可恢复"}}
其他操作与 futex2 的边界
FUTEX_REQUEUE 等操作可以移动等待队列,减少某些条件变量模式的惊群。FUTEX_WAKE_OP 将对第二个 word 的原子操作与条件唤醒结合;它不是“原子加法之后再 WAIT”,FUTEX_OP 是编码操作的宏。FUTEX_WAKE_OP
futex_waitv 接受一组待比较 word,并等待其中某个被唤醒,适合多个等待源。返回某个索引后仍要检查业务状态,不代表同时取得多个锁。该接口使用绝对超时与指定时钟。
“futex2”曾涉及多项扩展设想,不能把提案里的所有宽度、NUMA 和调度特性写成所有 6.x 内核已具备的事实。本文核对的内核 futex2 文档对 waitv 明确要求 FUTEX_32,虽然结构字段为扩展保留了空间。内核 futex2 文档
如何理解性能
无竞争路径可能没有系统调用,但原子读改写仍会带来缓存一致性开销。竞争时还可能包含自旋、队列操作、上下文切换、调度延迟与跨 NUMA 数据传输。不存在固定“20 ns”或每次竞争必定恰好一次 WAIT 加一次 WAKE 的普遍结论。
观察锁慢时,区分持锁时间、等待时间、争用频率与调度影响。函数入口到返回追踪方法见 ftrace 与 bpftrace。
关键词:futex, compare and block, lost wakeup, FUTEX_WAIT, FUTEX_WAKE, acquire release, PI, robust mutex, EOWNERDEAD, futex_waitv