利用 Lockdep 检测内核死锁问题
1. Lockdep 基础
Lockdep 基于锁类(lock class)工作,锁类是一种“逻辑”锁,与锁的“物理”实例相对。例如,内核的打开文件数据结构 struct file 有两个锁(一个互斥锁和一个自旋锁),Lockdep 将它们都视为锁类。即使运行时内存中存在数千个 struct file 实例,Lockdep 也只会将其作为一个类进行跟踪。更多关于 Lockdep 内部设计的详细信息,可参考 官方内核文档 。
2. 验证 Lockdep 是否启用
在使用 Lockdep 之前,需要确保其已在调试内核中启用。可以通过以下命令进行验证:
$ uname -r
5.4.0-llkd-dbg
$ grep PROVE_LOCKING /boot/config-5.4.0-llkd-dbg
CONFIG_PROVE_LOCKING=y
3. 示例 1:使用 Lockdep 捕获自死锁 bug
以下是一个示例,展示了如何使用 Lockdep 捕获自死锁问题。
3.1 原始代码
最初的代码通过直接访问 t->comm 来获取线程名称:
// ch6/foreach/thrd_showall/thrd_showall.c
static int showthrds(void)
{
struct task_struct *g = NULL, *t = NULL; /* 'g' : process ptr; 't': thread ptr */
[ ... ]
do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */
task_lock(t);
[ ... ]
if (!g->mm) { // kernel thread
snprintf(tmp, TMPMAX-1, " [%16s]", t->comm);
} else {
snprintf(tmp, TMPMAX-1, " %16s ", t->comm);
}
snprintf(buf, BUFMAX-1, "%s%s", buf, tmp);
[ ... ]
3.2 修改后的代码
为了改进代码,使用了 get_task_comm() 辅助宏:
// ch13/3_lockdep/buggy_lockdep/thrd_showall_buggy.c
static int showthrds_buggy(void)
{
struct task_struct *g, *t; /* 'g' : process ptr; 't': thread ptr */
[ ... ]
char buf[BUFMAX], tmp[TMPMAX], tasknm[TASK_COMM_LEN];
[ ... ]
do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */
task_lock(t);
[ ... ]
get_task_comm(tasknm, t);
if (!g->mm) // kernel thread
snprintf(tmp, sizeof(tasknm)+3, " [%16s]", tasknm);
else
snprintf(tmp, sizeof(tasknm)+3, " %16s ", tasknm);
[ ... ]
当编译并将此模块插入内核时,系统可能会出现异常甚至挂起。
3.3 问题分析
通过查看内核日志,发现 Lockdep 捕获到了自死锁问题。 get_task_comm() 宏内部调用了 __get_task_comm() 函数,其代码如下:
// fs/exec.c
char *__get_task_comm(char *buf, size_t buf_size, struct task_struct *tsk)
{
task_lock(tsk);
strncpy(buf, tsk->comm, buf_size);
task_unlock(tsk);
return buf;
}
EXPORT_SYMBOL_GPL(__get_task_comm);
在调用 get_task_comm() 之前已经调用了 task_lock(t) ,而 __get_task_comm() 函数内部又尝试重新获取同一个锁,导致自死锁。 task_lock() 函数的代码如下:
// include/linux/sched/task.h */
static inline void task_lock(struct task_struct *p)
{
spin_lock(&p->alloc_lock);
}
可以看出,这是任务结构中名为 alloc_lock 的自旋锁。
3.4 Lockdep 报告解读
Lockdep 的报告中有一些令人困惑的符号,例如:
[ 1021.449384] insmod/2367 is trying to acquire lock:
[ 1021.451361] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: __get_task_comm+0x28/0x50
[ 1021.453676]
but task is already holding lock:
[ 1021.457365] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: showthrds_buggy+0x13e/0x6d1
忽略时间戳,第二行最左边列的数字是用于标识特定锁序列的 64 位轻量级哈希值。 {+.+.} 是 Lockdep 用于表示锁获取状态的符号,具体含义可参考 内核文档 。
3.5 修复方法
修复方法很简单,在调用 get_task_comm() 之前解锁,执行完后再重新加锁。
4. 示例 2:使用 Lockdep 捕获 AB - BA 死锁
以下是一个故意创建循环依赖以导致死锁的示例。
4.1 代码逻辑
创建两个内核线程,分别运行在不同的 CPU 核心上,并使用两个自旋锁 lockA 和 lockB 。规定的锁顺序是先获取 lockA ,再获取 lockB 。但如果违反这个顺序,就会导致经典的 AB - BA 死锁:
kthread 0 on CPU #0 kthread 1 on CPU #1
Take lockA Take lockB
<perform work> <perform work>
(Try and) take lockA
< ... spins forever :
DEADLOCK ... >
(Try and) take lockB
< ... spins forever :
DEADLOCK ... >
4.2 代码示例
// ch13/3_lockdep/deadlock_eg_AB-BA/deadlock_eg_AB-BA.c
[ ... ]
/* Our kernel thread worker routine */
static int thrd_work(void *arg)
{
[ ... ]
if (thrd == 0) { /* our kthread #0 runs on CPU 0 */
pr_info(" Thread #%ld: locking: we do:"
" lockA --> lockB\n", thrd);
for (i = 0; i < THRD0_ITERS; i ++) {
/* In this thread, perform the locking per the lock ordering 'rule';
* first take lockA, then lockB */
pr_info(" iteration #%d on cpu #%ld\n", i, thrd);
spin_lock(&lockA);
DELAY_LOOP('A', 3);
spin_lock(&lockB);
DELAY_LOOP('B', 2);
spin_unlock(&lockB);
spin_unlock(&lockA);
}
} else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */
for (i = 0; i < THRD1_ITERS; i ++) {
/* In this thread, if the parameter lock_ooo is 1, *violate* the
* lock ordering 'rule'; first (attempt to) take lockB, then lockA */
pr_info(" iteration #%d on cpu #%ld\n", i, thrd);
if (lock_ooo == 1) { // violate the rule, naughty boy!
pr_info(" Thread #%ld: locking: we do: lockB --> lockA\n",thrd);
spin_lock(&lockB);
DELAY_LOOP('B', 2);
spin_lock(&lockA);
DELAY_LOOP('A', 3);
spin_unlock(&lockA);
spin_unlock(&lockB);
} else if (lock_ooo == 0) { // follow the rule, good boy!
pr_info(" Thread #%ld: locking: we do: lockA --> lockB\n",thrd);
spin_lock(&lockA);
DELAY_LOOP('B', 2);
spin_lock(&lockB);
DELAY_LOOP('A', 3);
spin_unlock(&lockB);
spin_unlock(&lockA);
}
[ ... ]
4.3 运行结果
当 lock_ooo 参数设置为 0 时,程序正常运行;当设置为 1 时,系统会出现死锁。通过 journalctl --since="10 min ago" 可以获取 Lockdep 的报告:
======================================================
WARNING: possible circular locking dependency detected
5.4.0-llkd-dbg #2 Tainted: G OE
------------------------------------------------------
thrd_0/0/6734 is trying to acquire lock:
ffffffffc0fb2518 (lockB){+.+.}, at: thrd_work.cold+0x188/0x24c [deadlock_eg_AB_BA]
but task is already holding lock:
ffffffffc0fb2598 (lockA){+.+.}, at: thrd_work.cold+0x149/0x24c [deadlock_eg_AB_BA]
which lock already depends on the new lock.
[ ... ]
other info that might help us debug this:
Possible unsafe locking scenario:
CPU0 CPU1
---- ----
lock(lockA);
lock(lockB);
lock(lockA);
lock(lockB);
*** DEADLOCK ***
[ ... lots more output follows ... ]
5. Lockdep 注解和问题
5.1 Lockdep 注解
Lockdep 允许内核开发者使用 lockdep_assert_held() 宏来断言在特定点持有锁,这被称为 Lockdep 注解:
// include/linux/lockdep.h
#define lockdep_assert_held(l) do { \
WARN_ON(debug_locks && !lockdep_is_held(l)); \
} while (0)
当断言失败时,会通过 WARN_ON() 发出警告。
5.2 Lockdep 问题
使用 Lockdep 时可能会出现以下问题:
- 重复加载和卸载模块可能会导致 Lockdep 的内部锁类限制被超出。解决方法是避免重复加载/卸载模块或重置系统。
- 当数据结构中有大量锁时,未能正确初始化每个锁可能会导致 Lockdep 锁类溢出。
- 当调试锁被禁用时, debug_locks 整数会被设置为 0,可能会出现 *WARNING* lock debugging disabled!! 消息。此时需要重启系统并重试。
6. 总结
通过以上示例可以看出,Lockdep 是一个强大的工具,可以帮助开发者捕获内核中的死锁问题。同时,了解 Lockdep 的工作原理和可能出现的问题,有助于更有效地使用它进行内核调试。
以下是一个简单的流程图,展示了使用 Lockdep 检测死锁的基本流程:
graph TD;
A[验证 Lockdep 是否启用] --> B[编写可能存在死锁的代码];
B --> C[编译并插入内核模块];
C --> D{系统是否异常};
D -- 是 --> E[获取内核日志];
D -- 否 --> F[继续测试其他情况];
E --> G[分析 Lockdep 报告];
G --> H[修复死锁问题];
H --> C;
表格总结 Lockdep 可能出现的问题及解决方法:
| 问题 | 原因 | 解决方法 |
| ---- | ---- | ---- |
| 锁类限制超出 | 重复加载和卸载模块 | 避免重复加载/卸载模块或重置系统 |
| 锁类溢出 | 数据结构中有大量锁且未正确初始化 | 确保正确初始化所有锁 |
| 调试锁禁用 | 出现 Lockdep 警告或其他错误 | 重启系统并重试 |
利用 Lockdep 检测内核死锁问题
7. 操作步骤总结
为了更清晰地展示使用 Lockdep 检测内核死锁的操作流程,以下是详细的步骤列表:
1. 启用并验证 Lockdep
- 确保在调试内核中启用 Lockdep。
- 使用以下命令验证:
$ uname -r
5.4.0-llkd-dbg
$ grep PROVE_LOCKING /boot/config-5.4.0-llkd-dbg
CONFIG_PROVE_LOCKING=y
- 编写测试代码
- 编写可能存在死锁问题的内核模块代码,如自死锁或 AB - BA 死锁的示例代码。
- 编译并插入内核模块
- 编译内核模块代码。
- 使用
insmod命令将模块插入内核。
- 观察系统状态
- 检查系统是否出现异常,如挂起等。
- 获取内核日志
- 如果系统异常,可尝试使用
dmesg或journalctl命令获取内核日志。 - 例如:
journalctl --since="10 min ago"
- 如果系统异常,可尝试使用
- 分析 Lockdep 报告
- 查看 Lockdep 报告,找出死锁的原因和相关信息。
- 修复死锁问题
- 根据报告分析结果,修改代码以解决死锁问题。
- 重复测试
- 再次编译并插入修改后的内核模块,重复上述步骤进行测试,直到问题解决。
8. 常见问题解答
以下是关于 Lockdep 使用过程中常见问题的解答:
| 问题 | 解答 |
| ---- | ---- |
| 如何解读 Lockdep 报告中的锁状态符号,如 {+.+.} ? | {+.+.} 是 Lockdep 用于表示锁获取状态的符号, + 表示锁在 IRQ 启用时获取, . 表示锁在 IRQ 禁用且不在 IRQ 上下文时获取。具体含义可参考 内核文档 。 |
| 为什么重复加载和卸载模块会导致 Lockdep 锁类限制超出? | 加载内核模块会为其所有锁创建新的锁类,而卸载模块时这些锁类不会被移除,而是被重用。因此,重复加载和卸载会使锁类数量不断增加,最终超出限制。 |
| 当系统出现 *WARNING* lock debugging disabled!! 消息时该怎么办? | 这可能是由于 Lockdep 发出警告或其他内核锁定基础设施出现错误导致调试锁被禁用。此时需要重启系统并重试。 |
9. 深入理解 Lockdep
为了更好地使用 Lockdep,我们可以进一步深入理解其工作原理和内部机制。Lockdep 通过跟踪锁的获取和释放操作,构建锁的依赖关系图。当检测到锁的获取顺序违反了已建立的规则时,就会发出警告,提示可能存在死锁问题。
以下是一个简单的示意图,展示了 Lockdep 跟踪锁操作的基本原理:
graph LR;
A[线程 1 获取锁 A] --> B[线程 1 获取锁 B];
C[线程 2 获取锁 B] --> D[线程 2 尝试获取锁 A];
B & D --> E{检测到死锁风险};
E --> F[Lockdep 发出警告];
10. 拓展应用
除了上述示例中的自死锁和 AB - BA 死锁,Lockdep 还可以用于检测其他复杂的死锁场景。例如,多个线程之间的循环依赖死锁、递归锁导致的死锁等。在实际开发中,开发者可以根据具体需求编写不同的测试代码,利用 Lockdep 进行全面的死锁检测。
同时,结合其他内核调试工具,如 kdump 和 crash ,可以在系统崩溃后进行事后分析,进一步定位和解决死锁问题。具体操作步骤如下:
1. 启用 kdump :在系统中启用 kdump 功能,确保在系统崩溃时能够生成内核转储文件。
2. 系统崩溃后 :使用 crash 工具对内核转储文件进行分析,获取更多关于死锁的详细信息。
11. 总结与展望
Lockdep 作为一个强大的内核调试工具,为开发者提供了有效的手段来检测和解决内核中的死锁问题。通过深入理解其工作原理、掌握使用方法以及注意可能出现的问题,开发者可以更高效地进行内核开发和调试。
随着内核技术的不断发展,未来可能会有更多的工具和技术出现,进一步提升内核调试的效率和准确性。例如,Kernel Concurrency Sanitizer (KCSAN) 是一个数据竞争检测器,通过编译时插桩技术来检测 Linux 内核中的数据竞争问题。开发者可以关注这些新技术的发展,并结合使用,以提高内核代码的质量和稳定性。
总之,掌握 Lockdep 等调试工具的使用,对于内核开发者来说是非常重要的。希望本文能够帮助读者更好地理解和应用 Lockdep,解决内核开发中的死锁难题。
超级会员免费看
45

被折叠的 条评论
为什么被折叠?



