Linux 应用程序调试技术/死锁
外观
寻找死锁意味着重建线程和资源(互斥量、信号量、条件变量等)之间的依赖关系图——谁拥有什么,谁想要获取什么。典型的死锁看起来像该图中的循环。这项任务很繁琐,因为我们正在寻找的一些参数已经被编译器优化到寄存器中。
以下是对 x86_64 死锁的分析。在这个平台上,寄存器 r8 包含第一个参数:互斥量的地址
(gdb) thread apply all bt
...
Thread 4 (Thread 0x419bc940 (LWP 12275)):
#0 0x0000003684c0d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003684c08e1a in _L_lock_1034 () from /lib64/libpthread.so.0
#2 0x0000003684c08cdc in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400a50 in thread1 (threadid=0x1) at deadlock.c:66
#4 0x0000003684c0673d in start_thread () from /lib64/libpthread.so.0
#5 0x00000036840d3d1d in clone () from /lib64/libc.so.6
Thread 3 (Thread 0x421bd940 (LWP 12276)):
#0 0x0000003684c0d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003684c08e1a in _L_lock_1034 () from /lib64/libpthread.so.0
#2 0x0000003684c08cdc in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400c07 in thread2 (threadid=0x2) at deadlock.c:111
#4 0x0000003684c0673d in start_thread () from /lib64/libpthread.so.0
#5 0x00000036840d3d1d in clone () from /lib64/libc.so.6
...
(gdb) thread 4
[Switching to thread 4 (Thread 0x419bc940 (LWP 12275))]#2 0x0000003684c08cdc in pthread_mutex_lock ()
from /lib64/libpthread.so.0
(gdb) frame 2
#2 0x0000003684c08cdc in pthread_mutex_lock () from /lib64/libpthread.so.0
(gdb) info reg
...
r8 0x6015a0 6296992
...
(gdb) p *(pthread_mutex_t*)0x6015a0
$3 = {
__data = {
__lock = 2,
__count = 0,
__owner = 12276, <== LWP 12276 is Thread 3
__nusers = 1,
__kind = 0, <== non-recursive
__spins = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = "\002\000\000\000\000\000\000\000\364/\000\000\001", '\000' <repeats 26 times>,
__align = 2
}
(gdb) thread 3
[Switching to thread 3 (Thread 0x421bd940 (LWP 12276))]#0 0x0000003684c0d4c4 in __lll_lock_wait ()
from /lib64/libpthread.so.0
(gdb) bt
#0 0x0000003684c0d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003684c08e1a in _L_lock_1034 () from /lib64/libpthread.so.0
#2 0x0000003684c08cdc in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400c07 in thread2 (threadid=0x2) at deadlock.c:111
#4 0x0000003684c0673d in start_thread () from /lib64/libpthread.so.0
#5 0x00000036840d3d1d in clone () from /lib64/libc.so.6
(gdb) info reg
...
r8 0x6015e0 6297056
...
(gdb) p *(pthread_mutex_t*)0x6015e0
$4 = {
__data = {
__lock = 2,
__count = 0,
__owner = 12275, <=== Thread 4
__nusers = 1,
__kind = 0,
__spins = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = "\002\000\000\000\000\000\000\000\363/\000\000\001", '\000' <repeats 26 times>,
__align = 2
}
线程 3 和 4 正在争夺两个互斥量。
注意:如果 gdb 无法找到符号 pthread_mutex_t,因为它没有加载 pthreadtypes.h 的符号表,你仍然可以按如下方式打印结构的各个成员
(gdb) print *((int*)(0x6015e0))
$4 = 2
(gdb) print *((int*)(0x6015e0)+1)
$5 = 0
(gdb) print *((int*)(0x6015e0)+2)
$6 = 12275
一个 std::mutex 有类似的结构,其中 __owner 是 LWP
(gdb) p mtx
$1 = (std::mutex &) @0x7fffffffe5b0: {<std::__mutex_base> = {_M_mutex = {__data = {__lock = 1, __count = 0, __owner = 256829, __nusers = 1, __kind = 0, __spins = 0,
__elision = 0, __list = {__prev = 0x0, __next = 0x0}}, __size = "\001\000\000\000\000\000\000\000=\353\003\000\001", '\000' <repeats 26 times>,
__align = 1}}, <No data fields>}
(gdb) info threads
Id Target Id Frame
...
* 3 Thread 0x7ffff77ff640 (LWP 256829) "a.out" operator() (nLoops=1000000, __closure=0x55555556f770) at lockfree1.cpp:64
可以构建一个插入库来 自动化死锁分析。需要插入大量 API,即使那样,也有一些情况库无法注意到。例如,一个不涉及任何用户空间锁定机制的死锁两个线程的创造性方法是让每个线程加入另一个线程。因此,插入工具的诊断功能有限。
还有许多其他工具可用
- gdb-automatic-deadlock-detector - 脚本向 GDB 添加了新的命令“blocked”。此命令分析所有线程并显示哪些线程正在等待其他线程。它还显示了线程之间的死锁。
- 用户空间 lockdep。正在进行的工作。
- Locksmith。基本。
- Valgrind Helgrind。没有像其他 Valgrind 工具那样遭受严重的减速(特别是内存分析工具),但在 amd64 平台上无法长期运行。
pthreads对某些同步机制有一些内置支持(例如PTHREAD_MUTEX_ERRORCHECK互斥量)。
此外,还有一个用于健壮互斥量的 POSIX 互斥量构造属性(一个可恢复的互斥量,由其进程已死亡的线程持有):错误返回代码表示先前的所有者已死亡;获取锁意味着承担处理任何清理的责任。
在启用了 lockdep 的内核上,按 Alt+SysRq+d 会转储有关内核已知的所有锁的信息。