同样表现为卡住,可能是锁顺序成环、错过通知,也可能只是持锁者迟迟未获 CPU。先明确缺失哪种进展,再选择证据。
| 查阅目标 | 阅读位置 |
| 依赖与竞争统计 | 第 1~4 节 |
| 数据竞争与上下文 | 第 5~6 节 |
| 停顿类型与修复验证 | 第 7~9 节 |
同步问题至少包括死锁、数据竞争、活锁、长时间持锁和丢失唤醒。它们可能都表现为“卡住”,但所需证据不同。先判断谁没有进展、在等待什么,再选工具;不要把所有告警都归成“锁没释放”。
1. 工具各自回答什么
| 工具/机制 | 主要回答 | 不能证明 |
| lockdep / PROVE_LOCKING | 已观察依赖是否可能构成锁顺序或上下文冲突? | 未运行路径一定正确 |
| lockstat | 哪些锁类竞争多、等待或持有时间长? | 长等待必然是死锁 |
| KCSAN | 抽样观察到的内存访问是否暴露竞争/相关问题? | 一轮无报告就没有竞争 |
| DEBUG_ATOMIC_SLEEP | 当前上下文是否错误调用可能睡眠的操作? | 所有阻塞 API 的业务等待都正确 |
| hung task | 某些不可中断等待任务是否长期无进展? | 等待原因一定是 mutex |
| soft/hard lockup | CPU 的调度或中断进展是否异常? | 某个具体应用一定持错锁 |
| RCU stall | 宽限期或相关处理是否迟迟无法推进? | 必然缺少一个 unlock |
2. Lockdep 为什么能在死锁发生前报告
当任务持有锁类 A 并获取 B 时,lockdep 记录 A→B。另一条路径若出现 B→A,即使它们从未实际同时卡住,也已经形成潜在循环;更长的依赖链同样需要分析。
lockdep 还跟踪 hardirq/softirq 等使用状态。例如某锁在中断 handler 中使用,另一条路径又在允许该中断打断时持有它,可能产生本 CPU 自锁死锁。此类上下文冲突不能只靠检查两个不同 CPU 的 ABBA 顺序发现。
把几条曾经观察到的获取顺序逐条加入图中。边可以来自不同时间,闭环就已经构成需要排查的顺序矛盾,不必等两个线程在现场同时卡住。
正在呈现知识画面
没有同时死锁,依赖环也能被发现lockdep 合并已观察到的锁类依赖。不同时间发生的 A→B 与 B→A 也形成潜在循环,不必等实际任务同时卡住。
{"id":"lockdep-order-graph","title":"没有同时死锁,依赖环也能被发现","summary":"lockdep 合并已观察到的锁类依赖。不同时间发生的 A→B 与 B→A 也形成潜在循环,不必等实际任务同时卡住。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label for=\"edge\" data-i18n=\"choose\"></label><select id=\"edge\"><option value=\"AB\">A → B</option><option value=\"BA\">B → A</option><option value=\"AC\">A → C</option><option value=\"CA\">C → A</option><option value=\"BC\">B → C</option><option value=\"CB\">C → B</option></select><div class=\"actions\"><button id=\"record\" data-i18n=\"record\"></button></div><svg viewBox=\"0 0 320 240\" role=\"img\" aria-label=\"A, B, C\"><defs><marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10z\" fill=\"context-stroke\"/></marker></defs><path id=\"edge-AB\" d=\"M80 45 Q160 5 240 45\" class=\"edge\" marker-end=\"url(#arrow)\"/><path id=\"edge-BA\" d=\"M240 70 Q160 105 80 70\" class=\"edge\" marker-end=\"url(#arrow)\"/><path id=\"edge-AC\" d=\"M50 80 Q40 150 140 194\" class=\"edge\" marker-end=\"url(#arrow)\"/><path id=\"edge-CA\" d=\"M135 180 Q95 120 74 81\" class=\"edge\" marker-end=\"url(#arrow)\"/><path id=\"edge-BC\" d=\"M246 83 Q224 140 180 182\" class=\"edge\" marker-end=\"url(#arrow)\"/><path id=\"edge-CB\" d=\"M184 200 Q289 150 270 80\" class=\"edge\" marker-end=\"url(#arrow)\"/><circle cx=\"60\" cy=\"60\" r=\"24\"/><text x=\"60\" y=\"66\" text-anchor=\"middle\">A</text><circle cx=\"260\" cy=\"60\" r=\"24\"/><text x=\"260\" y=\"66\" text-anchor=\"middle\">B</text><circle cx=\"160\" cy=\"200\" r=\"24\"/><text x=\"160\" y=\"206\" text-anchor=\"middle\">C</text></svg><div class=\"track-row\"><span data-i18n=\"edges\"></span><div id=\"edges\" class=\"track\"></div></div><div class=\"card\"><header data-i18n=\"cycle\"></header><div id=\"cycle\" class=\"readout\"></div></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\nsvg{width:100%;height:auto;max-height:310px;display:block;margin:15px auto}svg circle{fill:var(--surface);stroke:var(--accent);stroke-width:2}svg text{fill:var(--ink);font:18px var(--viz-font-ui)}.edge{fill:none;stroke:var(--rule);stroke-width:2;stroke-dasharray:4 5}.edge.on{stroke:var(--accent);stroke-dasharray:none;stroke-width:3}.edge.cycle{stroke:var(--second)}#edges{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.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 edges=new Set;function findCycle(){for(const start of 'ABC'){function walk(path){const n=path.at(-1);for(const to of 'ABC'){if(!edges.has(n+to))continue;if(to===start)return [...path,to];if(!path.includes(to)){const r=walk([...path,to]);if(r)return r;}}return null;}const r=walk([start]);if(r)return r;}return null;}function draw(){const cycle=findCycle(),cs=new Set(cycle?cycle.slice(0,-1).map((v,i)=>v+cycle[i+1]):[]);document.querySelectorAll('.edge').forEach(e=>{const key=e.id.slice(5);e.classList.toggle('on',edges.has(key));e.classList.toggle('cycle',cs.has(key));});cells('edges',['AB','BA','AC','CA','BC','CB'].map(k=>({text:k[0]+' → '+k[1]+(edges.has(k)?' ✓':' —'),cls:cs.has(k)?'gold':edges.has(k)?'on':'empty'})));set('cycle',cycle?cycle.join(' → '):t('none'));$('#record').disabled=edges.has($('#edge').value);set('explanation',t('note'));}$('#record').onclick=()=>{edges.add($('#edge').value);draw();};$('#edge').onchange=draw;$('#reset').onclick=()=>{edges=new Set;draw();};draw();","audio":false,"strings":{"choose":"持有左侧锁,再获取右侧锁","cycle":"检测到的循环","edges":"已观察依赖","heading":"没有同时死锁,依赖环也能被发现","intro":"选择“持有哪一类锁时,再获取哪一类锁”,记录一次独立执行。图会累积历史依赖,找出两条或三条边构成的环。","none":"当前无环","note":"先记录 A→B、B→C,再记录 C→A。最后一条边让已知路径闭环;告警涉及整条依赖链,不只是最后一个加锁点。","record":"记录这条已执行依赖","reset":"重置","scope":"三个锁类、无自递归;每次记录时操作已完成,不保留实际持锁者。这里只验证普通顺序图,不覆盖 IRQ 上下文、subclass 或 lockdep 的全部实现。无环仅表示当前已观察边无环,不证明未运行路径正确。","scopeLabel":"模型范围与约定"}}
锁类与锁实例
锁类通常由初始化位置/显式 class key 等决定,而不是“所有 spinlock 是一类”或“不同获取函数一定不同类”。多个同类对象中的锁可以归入同一类;一个对象在不同函数获取也不自动成为不同类。
如果算法确实按照父子层次同时获取同类对象锁,可以用恰当 subclass/nested API 表达经过证明的顺序。但添加 subclass 只是给验证器更多信息,不会自动使实际顺序正确。错误注解可能隐藏真正问题。
报告提供当前获取位置、已有依赖及相关栈,用于重建矛盾路径;它不是从启动以来每一次持锁的完整历史录制。见 lockdep 设计。
3. 如何读一次 lockdep 报告
按报告重建依赖,而不是只改最后触发的位置:
- 找出当前正在获取的锁和已经持有的锁。
- 沿已有依赖还原另一条或多条相反路径。
- 确认对象实例、并发可能性,以及中断/抢占状态。
这些条件共同决定依赖环如何形成,也决定应该改锁顺序还是对象关系。
不要只改触发告警的最后一个 lock 调用。常见修复是统一锁顺序、缩小临界区、分阶段操作、避免持锁等待回调,或者重新设计对象关系。若调整层次注解,需给出为什么同时持锁对象一定满足该层次的依据。
lockdep_assert_held() 可把内部函数的“调用者必须持某锁”前提变成可检查契约。长时间测试无报告仍需确认验证器实际启用、没有在较早错误后停止检查,也覆盖了相关错误和退出路径。
4. Lockstat:竞争次数、等待与持有时间一起看
/proc/lock_stat 在相应配置/运行控制启用后报告锁类统计,包含获取、竞争、等待和持有时间等字段。时间单位应读版本对应表头/文档,不能伪造一张固定字段的纳秒表后按第二列盲目排序。
| 统计表现 | 下一步验证 |
| 获取多,竞争少 | 这把锁是否真有优化价值 |
| 竞争少,单次等待很长 | 持锁者是否慢 I/O、被抢占、调度受限或等另一把锁 |
| 竞争多,单次持有短 | 共享 cache line、锁粒度与分片机会 |
次数和时长要结合起来,不能只按一个最大数字确定根因。
如果测量会改变调度/时序,要比较开启前后的负载表现。分片降低争用也可能增加汇总成本和一致性难度,不应只追求 lockstat 数字更小。见 lockstat 文档。
5. KCSAN:抽样检测,不是每次访问记完整时间戳
KCSAN 使用编译器插桩与软件 watchpoint/延迟等机制抽样观察冲突访问;不是对所有内存操作建立精确全历史 happens-before 图。配置可改变采样、延迟及对访问类型的处理,所以开销不应固定说成十倍。
报告中的地址、大小、读写类型和两个调用栈是分析入口。两个访问没有共同锁并不必然错误,因为它们可能通过原子/发布协议同步;反过来,代码中存在一把锁也不说明每条访问都被它保护。
READ_ONCE/WRITE_ONCE、atomic、data_race() 等标记必须反映真实协议。给报告位置套上 data_race() 只会表达“这是经过判断允许的竞争”,不会修复顺序、原子性或寿命。例外应解释读取不一致值为何无害以及后续使用为何安全。
对于无锁发布问题,可用 Linux Kernel Memory Model 的 litmus test 分析允许结果,再结合 KCSAN 和压力测试。用户态 C/C++ 原子模型的直觉不能未经核对就替代内核内存模型。见 KCSAN及 LKMM。
6. 原子上下文睡眠与等待协议
CONFIG_DEBUG_ATOMIC_SLEEP 等检查通常通过 might_sleep 等位置报告“sleeping function called from invalid context”。这类诊断不统一等于立即 BUG/panic;具体处置受路径和配置影响。
检查持有的锁、preempt count、中断状态及调用链,确认是否在 raw spinlock、hardirq 或其他不能睡眠位置调用可能分配/阻塞的 API。某次分配恰好命中快路径而未阻塞,也不能证明这种调用合法。
丢失唤醒未必产生锁依赖环:线程可能在条件检查之后、真正登记等待之前错过唯一一次 wakeup。应使用 wait_event/completion 等正确协议,并核实超时、取消和退出后对象寿命。
7. Hung task、softlockup 与 hardlockup
Hung task 关注某些长期处于不可中断等待且没有相应进展的任务。D 状态可能在等存储、网络文件系统、completion 或锁。超时阈值可配置,120 秒不是跨系统不可变常数。
Softlockup 表示 CPU 长时间没有让 watchdog 相关任务获得调度进展,可能仍能处理中断。长不可抢占循环、调度饥饿等都会影响它。
Hardlockup 在常见 NMI/perf 实现中,由仍能进入的 NMI 检查普通定时器中断是否推进。它不是“因为关闭普通中断,所以连 NMI 也收不到”这样判断。检测器实现依架构、PMU、配置及虚拟化支持不同;虚拟机暂停等也会干扰时序。
三者与 RCU stall 可以同时出现,但表达不同缺失的进展。先找重复栈和等待关系,再判断共同根因。见lockup watchdog。
8. 只读查看检测能力和阈值
from pathlib import Path
names = ('hung_task_timeout_secs', 'hung_task_panic', 'watchdog',
'watchdog_thresh', 'nmi_watchdog', 'softlockup_panic')
for name in names:
path = Path('/proc/sys/kernel') / name
try:
print(name, path.read_text().strip())
except OSError as exc:
print(name, type(exc).__name__)
for path in ('/proc/lockdep_stats', '/proc/lock_stat'):
try:
with open(path) as source:
head = ''.join(next(source, '') for _ in range(5))
print(path, head.strip())
except OSError as exc:
print(path, type(exc).__name__)
状态文件不存在可能因为配置未启用,权限拒绝也不等于功能不存在。/proc/<pid>/stack 需要相应配置和访问权限;某一个任务的单次栈只是一张快照,必要时关联其他任务和重复样本。
9. 修复后如何验证
使用与问题相同的配置和上下文先覆盖已知触发路径,再覆盖并发更新、错误返回、超时与退出清理。锁顺序问题确认依赖不再冲突,竞争问题确认不变量和发布/寿命协议成立,性能问题则重新测量等待分布与业务时延。
不要通过关闭 detector、抬高超时或加注解来代替解释。工具报告是证据,最终结论应落到具体不变量、锁顺序和对象生命周期上。