第一百九十四章:死锁检测
大乘期涉及内核源码:
一
林小源离开调试工坊,来到一片寂静的森林。森林里的每一棵树都是一把锁——有的粗壮如 mutex,有的纤细如 spinlock,有的枝叶繁茂如 rwlock。
但森林并不安宁。林小源看到两棵树的根纠缠在一起,互相拉扯,谁也无法移动。两只鸟分别停在两棵树的树梢上,各自叼着对方的树枝,僵持不下。
"ABBA 死锁。"一个冷静的声音说。
林小源循声望去,只见一个中年人站在两棵树之间,身穿一袭灰色长袍,手持一根银色的探针。他的表情严肃,目光在两棵树之间来回扫视。
"你是 lockdep?"
"我是锁依赖的分析者。"中年人说,"线程 A 先拿锁 A 再拿锁 B,线程 B 先拿锁 B 再拿锁 A——这就是 ABBA 死锁。两条路径形成了环,谁也走不动。"
lockdep 用探针在空中画出一个圆:"我记录每一次锁的获取顺序,构建依赖图。如果图中有环——"他指向那两棵纠缠的树,"就是潜在的死锁。"
"更准确地说,我记录的是 lock-class。"lockdep 把探针插进树根,根须上浮现出一串类名,"同一种逻辑规则下的锁是一类,即使它有成千上万个实例。比如每个 inode 都有自己的锁实例,但它们可以属于同一个锁类。依赖图上的边是类与类之间的顺序:L1 -> L2 表示某条路径曾经持有 L1 时尝试获取 L2。"
"所以你不等死锁真的发生?"
"不等。只要简单锁链出现过一次,我就能把它们组合起来证明某类死锁是否可能。"
锁类初试
lockdep 依赖图的基本对象是具体锁实例,还是逻辑上的 lock-class?
/*
* 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");#include <stdio.h>
/*
* 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");
}
int main() {
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");
return 0;
}二
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 是克星——检测死锁,防患未然。
死锁检测之试
死锁检测章中,进入自旋锁并保存中断状态的接口是什么?