同步调试:依赖、竞争与停顿

本页目录

同样表现为卡住,可能是锁顺序成环、错过通知,也可能只是持锁者迟迟未获 CPU。先明确缺失哪种进展,再选择证据。

查阅目标阅读位置
依赖与竞争统计第 1~4 节
数据竞争与上下文第 5~6 节
停顿类型与修复验证第 7~9 节

同步问题至少包括死锁、数据竞争、活锁、长时间持锁和丢失唤醒。它们可能都表现为“卡住”,但所需证据不同。先判断谁没有进展、在等待什么,再选工具;不要把所有告警都归成“锁没释放”。

1. 工具各自回答什么

工具/机制主要回答不能证明
lockdep / PROVE_LOCKING已观察依赖是否可能构成锁顺序或上下文冲突?未运行路径一定正确
lockstat哪些锁类竞争多、等待或持有时间长?长等待必然是死锁
KCSAN抽样观察到的内存访问是否暴露竞争/相关问题?一轮无报告就没有竞争
DEBUG_ATOMIC_SLEEP当前上下文是否错误调用可能睡眠的操作?所有阻塞 API 的业务等待都正确
hung task某些不可中断等待任务是否长期无进展?等待原因一定是 mutex
soft/hard lockupCPU 的调度或中断进展是否异常?某个具体应用一定持错锁
RCU stall宽限期或相关处理是否迟迟无法推进?必然缺少一个 unlock

2. Lockdep 为什么能在死锁发生前报告

当任务持有锁类 A 并获取 B 时,lockdep 记录 A→B。另一条路径若出现 B→A,即使它们从未实际同时卡住,也已经形成潜在循环;更长的依赖链同样需要分析。

两条分别执行过的路径,也能暴露依赖环 路径一先持有 A 再获取 B 记录 A → B 再获取 A 路径二先持有 B 记录 B → A lockdep 汇总已观察到的锁类依赖;不必等两条路径实际同时死锁才报告。

lockdep 还跟踪 hardirq/softirq 等使用状态。例如某锁在中断 handler 中使用,另一条路径又在允许该中断打断时持有它,可能产生本 CPU 自锁死锁。此类上下文冲突不能只靠检查两个不同 CPU 的 ABBA 顺序发现。

把几条曾经观察到的获取顺序逐条加入图中。边可以来自不同时间,闭环就已经构成需要排查的顺序矛盾,不必等两个线程在现场同时卡住。

正在呈现知识画面
没有同时死锁,依赖环也能被发现

lockdep 合并已观察到的锁类依赖。不同时间发生的 A→B 与 B→A 也形成潜在循环,不必等实际任务同时卡住。

锁类与锁实例

锁类通常由初始化位置/显式 class key 等决定,而不是“所有 spinlock 是一类”或“不同获取函数一定不同类”。多个同类对象中的锁可以归入同一类;一个对象在不同函数获取也不自动成为不同类。

如果算法确实按照父子层次同时获取同类对象锁,可以用恰当 subclass/nested API 表达经过证明的顺序。但添加 subclass 只是给验证器更多信息,不会自动使实际顺序正确。错误注解可能隐藏真正问题。

报告提供当前获取位置、已有依赖及相关栈,用于重建矛盾路径;它不是从启动以来每一次持锁的完整历史录制。见 lockdep 设计。

3. 如何读一次 lockdep 报告

按报告重建依赖,而不是只改最后触发的位置:

  1. 找出当前正在获取的锁和已经持有的锁。
  2. 沿已有依赖还原另一条或多条相反路径。
  3. 确认对象实例、并发可能性,以及中断/抢占状态。

这些条件共同决定依赖环如何形成,也决定应该改锁顺序还是对象关系。

不要只改触发告警的最后一个 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、抬高超时或加注解来代替解释。工具报告是证据,最终结论应落到具体不变量、锁顺序和对象生命周期上。