GDB 调试指南

本文用一个「双线程、双互斥锁、加锁顺序相反」的最小示例,演示如何用 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_1child2 拿到 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 回溯现场,顺着栈帧找到卡住的位置和等待的资源,问题往往就迎刃而解。没有回溯现场就谈问题,都是耍流氓。


   转载规则


《GDB 调试指南》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Learning the Shell Learning the Shell
Why Bother?Why do you need to learn the command line anyway? Well, let me tell you a story. A few years ago we had a pro
2020-09-20
下一篇 
What is the Shell? What is the Shell?
What Is “The Shell”?Simply put, the shell is a program that takes commands from the keyboard and gives them to the opera
2020-09-13
  目录