本文用一个「双线程、双互斥锁、加锁顺序相反」的最小示例,演示如何用 gdb attach 定位程序死锁。死锁是并发编程里最棘手的问题之一,通过这个例子可以掌握一套通用的分析思路:先看线程与栈回溯(backtrace),再顺着栈找到等待的锁,最后确认锁的持有者(owner)。
示例程序
下面的程序启动了两个线程,每个线程都要依次获取两把互斥锁,但获取顺序恰好相反——这正是构成死锁的经典条件:
pthread_mutex_t mutex_1;
pthread_mutex_t mutex_2;
void *child1(void *arg)
{
while (1) {
pthread_mutex_lock(&mutex_1);
sleep(3);
pthread_mutex_lock(&mutex_2);
printf("thread 1 get running\n");
pthread_mutex_unlock(&mutex_2);
pthread_mutex_unlock(&mutex_1);
sleep(5);
}
}
void *child2(void *arg)
{
while (1) {
pthread_mutex_lock(&mutex_2);
pthread_mutex_lock(&mutex_1);
printf("thread 2 get running\n");
pthread_mutex_unlock(&mutex_1);
pthread_mutex_unlock(&mutex_2);
sleep(5);
}
}
int main(int argc, char *argv[])
{
int tid1, tid2;
pthread_mutex_init(&mutex_1, NULL);
pthread_mutex_init(&mutex_2, NULL);
pthread_create(&tid1, NULL, child1, NULL);
pthread_create(&tid2, NULL, child2, NULL);
...
}
child1 先锁 mutex_1 再锁 mutex_2,而 child2 顺序相反。当 child1 拿到 mutex_1、child2 拿到 mutex_2 之后,两者都会卡在获取第二把锁上,程序随即死锁。
用 GDB 定位死锁
程序跑起来后必然死锁,我们直接 gdb attach 它的 PID 进行分析。先看一共有多少个线程:
(gdb) i threads
Id Target Id Frame
3 Thread 0xb75e9b40 (LWP 15809) "a.out" ...
2 Thread 0xb7deab40 (LWP 15808) "a.out" ...
* 1 Thread 0xb7deb700 (LWP 15804) "a.out" ...
一共 3 个线程。切到线程 2,看一下它的 backtrace:
(gdb) thread 2
[Switching to thread 2 (Thread 0xb7deab40 (LWP 15808))]
(gdb) bt
...
#4 0x08048627 in child1 (arg=0x0) at deadlock.c:12
...
可以看到 PID 为 15808 的线程停在 child1(),也就是代码的第 12 行,在等一把 mutex。那它等的是哪一把呢?
(gdb) l child1
...
12 pthread_mutex_lock(&mutex_2);
...
第 12 行在等 mutex_2。接着看 mutex_2 的 owner 是谁:
(gdb) p mutex_2
$1 = {__data = {__lock = 2, __count = 0, __owner = 15809, __kind = 0, ...
mutex_2 的持有者是 15809。也就是说,15808 在等 mutex_2,而 mutex_2 正被 15809 拿着。
再切到线程 3,同样看 backtrace:
(gdb) thread 3
[Switching to thread 3 (Thread 0xb75e9b40 (LWP 15809))]
(gdb) bt
...
#4 0x08048677 in child2 (arg=0x0) at deadlock.c:24
...
15809 停在 child2(),即第 24 行,也在等一把 mutex:
(gdb) l child2
...
24 pthread_mutex_lock(&mutex_1);
...
第 24 行在等 mutex_1,再看它的 owner:
(gdb) p mutex_1
$2 = {__data = {__lock = 2, __count = 0, __owner = 15808, __kind = 0...
mutex_1 的持有者是 15808。
结论
把两个线程的等待关系放在一起,就形成了一个环:
| 线程 | 函数 | 已持有 | 正在等待 |
|---|---|---|---|
| 15808 | child1 |
mutex_1 |
mutex_2 |
| 15809 | child2 |
mutex_2 |
mutex_1 |
15808 等 mutex_2,而 mutex_2 被 15809 拿着;15809 等 mutex_1,而 mutex_1 被 15808 拿着。双方互相等待、谁也释放不了——典型的死锁。
这个例子虽然简单,但已经完整展示了定位问题的思路:遇到程序 hang 住或崩溃(kernel 的 lockup、OOPS,应用层的卡死)时,第一时间用 backtrace 回溯现场,顺着栈帧找到卡住的位置和等待的资源,问题往往就迎刃而解。没有回溯现场就谈问题,都是耍流氓。