Skip to content

第一百九十四章:死锁检测

大乘期

涉及内核源码:

林小源离开调试工坊,来到一片寂静的森林。森林里的每一棵树都是一把锁——有的粗壮如 mutex,有的纤细如 spinlock,有的枝叶繁茂如 rwlock。

但森林并不安宁。林小源看到两棵树的根纠缠在一起,互相拉扯,谁也无法移动。两只鸟分别停在两棵树的树梢上,各自叼着对方的树枝,僵持不下。

"ABBA 死锁。"一个冷静的声音说。

林小源循声望去,只见一个中年人站在两棵树之间,身穿一袭灰色长袍,手持一根银色的探针。他的表情严肃,目光在两棵树之间来回扫视。

"你是 lockdep?"

"我是锁依赖的分析者。"中年人说,"线程 A 先拿锁 A 再拿锁 B,线程 B 先拿锁 B 再拿锁 A——这就是 ABBA 死锁。两条路径形成了环,谁也走不动。"

lockdep 用探针在空中画出一个圆:"我记录每一次锁的获取顺序,构建依赖图。如果图中有环——"他指向那两棵纠缠的树,"就是潜在的死锁。"

"更准确地说,我记录的是 lock-class。"lockdep 把探针插进树根,根须上浮现出一串类名,"同一种逻辑规则下的锁是一类,即使它有成千上万个实例。比如每个 inode 都有自己的锁实例,但它们可以属于同一个锁类。依赖图上的边是类与类之间的顺序:L1 -> L2 表示某条路径曾经持有 L1 时尝试获取 L2。"

"所以你不等死锁真的发生?"

"不等。只要简单锁链出现过一次,我就能把它们组合起来证明某类死锁是否可能。"

破关试炼

锁类初试

lockdep 依赖图的基本对象是具体锁实例,还是逻辑上的 lock-class?

答对后才能继续滑动和进入下一章。
c
/*
 * lockdep:
 *
 * 功能:
 *   分析锁依赖关系
 *   检测潜在死锁
 *   运行时检测
 *
 * 死锁类型:
 *   ABBA 死锁:
 *     线程 1: 获取 A, 获取 B
 *     线程 2: 获取 B, 获取 A
 *
 *   递归死锁:
 *     同一线程重复获取同一锁
 *
 *   中断死锁:
 *     中断获取被进程持有的锁
 *
 * lockdep 概念:
 *   锁类 — 锁的类型
 *   锁实例 — 具体的锁
 *   依赖图 — 锁的获取顺序
 *
 * 检测方法:
 *   记录锁的获取顺序
 *   构建依赖图
 *   检测环形依赖
 *
 * 使用:
 *   CONFIG_LOCKDEP=y
 *   运行时自动检测
 *   dmesg 查看报告
 */

/* 模拟锁依赖图 */
struct lock_dep {
    char name[16];
    int depends_on[4];
    int dep_count;
};

void print_lock_dep(struct lock_dep *lock) {
    printf("  %s -> ", lock->name);
    for (int i = 0; i < lock->dep_count; i++) {
        printf("%d ", lock->depends_on[i]);
    }
    printf("\n");
}

printf("=== 死锁检测 — lockdep 的智慧 ===\n\n");

printf("lockdep 检测潜在死锁:\n\n");

printf("--- ABBA 死锁 ---\n");
printf("线程 1:\n");
printf("  获取锁 A\n");
printf("  获取锁 B\n\n");
printf("线程 2:\n");
printf("  获取锁 B\n");
printf("  获取锁 A\n\n");
printf("结果: 死锁!\n\n");

printf("--- lockdep 检测 ---\n");
printf("记录锁获取顺序:\n");
printf("  线程 1: A -> B\n");
printf("  线程 2: B -> A\n\n");
printf("构建依赖图:\n");
printf("  A -> B\n");
printf("  B -> A\n\n");
printf("检测到环形依赖!\n");
printf("报警: 潜在死锁\n\n");

printf("--- 死锁类型 ---\n");
printf("1. ABBA 死锁:\n");
printf("   锁获取顺序不一致\n\n");
printf("2. 递归死锁:\n");
printf("   同一线程重复获取\n\n");
printf("3. 中断死锁:\n");
printf("   中断获取被持有的锁\n\n");

printf("--- lockdep 概念 ---\n");
printf("锁类:\n");
printf("   锁的类型\n");
printf("   struct mutex 的实例\n\n");
printf("锁实例:\n");
printf("   具体的锁\n\n");
printf("依赖图:\n");
printf("   锁的获取顺序\n");
printf("   环形依赖 = 死锁\n\n");

printf("--- 使用方法 ---\n");
printf("1. 启用 lockdep:\n");
printf("   CONFIG_LOCKDEP=y\n\n");
printf("2. 运行时检测:\n");
printf("   自动分析锁依赖\n\n");
printf("3. 查看报告:\n");
printf("   dmesg | grep lockdep\n\n");

printf("--- 避免死锁 ---\n");
printf("1. 固定锁顺序:\n");
printf("   总是按相同顺序获取锁\n\n");
printf("2. 减少锁嵌套:\n");
printf("   避免同时持有多把锁\n\n");
printf("3. 使用 trylock:\n");
printf("   失败则释放已持有的锁\n\n");
printf("4. 中断安全:\n");
printf("   spin_lock_irqsave\n");

lockdep 领着林小源走进森林深处的一座石亭。亭子中央有一张石桌,桌上铺着一张巨大的网——由无数丝线交织而成,每一个节点都标注着锁的名字。

"这是依赖图。"lockdep 说。

林小源凑近看,发现有些丝线是绿色的——正常的依赖关系;有些是红色的——环形依赖。红色的丝线在图中形成刺眼的回路。

"每次有线程获取锁,我就在这张图上添加一条边。"lockdep 用探针指着一条绿色丝线,"线程 1 先拿 A 再拿 B,我就画 A -> B。如果以后有线程先拿 B 再拿 A,我就画 B -> A。两条边合在一起——"他画了一条弧线,"环出现了。"

"在运行时就能发现?"林小源问。

"对。"lockdep 点头,"不需要真的死锁,只要依赖图中出现环,我就报警。CONFIG_LOCKDEP=y 打开后,每次获取锁我都会检查。你用 dmesg | grep lockdep 就能看到报告。"

"我还记录锁在 hardirq、softirq 等状态下的使用历史。"lockdep 指向石桌边缘的四个符号,".、-、+、? 分别暗示锁是在中断上下文、开关中断状态下怎样被拿过。一个锁若曾在 hardirq 中拿过,就不能又在开中断状态下被普通路径持有,否则中断打断后递归拿同一类锁,死锁就会发生。"

"那同一类锁有层级关系怎么办?"

"用 _nested() 系列告诉我自然顺序。"lockdep 说,"比如整体磁盘与分区这类父子层级,开发者必须把层级映射清楚。标错会有误报,也可能漏报。"

破关试炼

状态之试

lockdep 会跟踪 hardirq 和哪一种软中断上下文的锁使用状态?

答对后才能继续滑动和进入下一章。

"怎么避免死锁?"林小源问。

lockdep 收起探针,背过身去,望着那些纠缠的树:"最简单的方法——固定锁顺序。所有线程都按 A 然后 B 的顺序获取锁,就不会有 ABBA 死锁。"

"如果必须同时持有多把锁呢?"

"减少嵌套。"lockdep 说,"能不同时持有就不同时持有。如果实在不行,用 trylock——尝试获取锁,失败了就释放已持有的锁,退回来重试。"

他转过身,表情严肃:"还有一种死锁更隐蔽——中断死锁。进程持有锁 A,中断来了,中断处理程序也尝试获取锁 A。进程等中断完成,中断等锁释放,谁都动不了。这种情况用 ,获取锁时关中断。"

"lockdep 不是唯一的森林守卫。"他又指向林外一座闪烁的观察塔,"KCSAN 负责另一类病:data race。它通过编译期插桩和 watchpoint 采样观察并发访问,报告两个线程的读写栈、地址和值变化。它能理解 atomic_* 和部分屏障语义;有些有意为之的竞争,要用 data_race()__data_racy__no_kcsan 标出来。"

"所以 lockdep 看锁顺序,KCSAN 看数据竞争。"

"对。一个证明锁依赖,一个采样共享内存访问。并发问题不止一种,工具也不能混用。"

林小源把这些记在心里。lockdep 的森林看似宁静,但每一棵树背后都藏着陷阱。

破关试炼

竞态之试

内核中基于 watchpoint 采样检测 data race 的工具叫什么?

答对后才能继续滑动和进入下一章。

道藏笔记

内核启示

lockdep 检测潜在死锁。

死锁类型:

  • ABBA — 锁顺序不一致
  • 递归 — 重复获取同一锁
  • 中断 — 中断获取被持有的锁

lockdep 概念:

  • 锁类 — 逻辑上遵循同一锁规则的一组锁
  • 锁实例 — 具体的锁
  • 依赖图 — 锁获取顺序
  • IRQ 使用状态 — hardirq/softirq 上下文与中断开关

检测方法:

  • 记录锁获取顺序
  • 构建依赖图
  • 检测环形依赖
  • _nested() 标注自然层级
  • lockdep_assert_held() 验证应持有的锁

避免死锁:

  • 固定锁顺序
  • 减少锁嵌套
  • 使用 trylock
  • 中断安全

KCSAN 则检测 data race:它依赖编译期插桩和 watchpoint 采样,报告并发读写栈和值变化;有意的竞争要用 data_race() 等方式标注。

lockdep 是克星——检测死锁,防患未然。


破关试炼

死锁检测之试

死锁检测章中,进入自旋锁并保存中断状态的接口是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道