在上一篇文章《使用 gdb 调试多进程程序 —— 以调试 nginx 为例》我们介绍了如何使用 gdb 调试多进程程序,这篇文章我们来介绍下如何使用 gdb 调试多线程程序,同时这个方法也是我阅读和分析一个新的 C/C++ 项目常用的方法。
当然,多线程调试的前提是你需要熟悉多线程的基础知识,包括线程的创建和退出、线程之间的各种同步原语等。如果您还不熟悉多线程编程的内容,可以参考这个专栏《C++ 多线程编程专栏》,如果您不熟悉 gdb 调试可以参考这个专栏《Linux GDB 调试教程》。
一、调试多线程的方法
使用 gdb 将程序跑起来,然后按 Ctrl + C 将程序中断下来,使用 info threads
命令查看当前进程有多少线程。
info threads
命令查看 redis-server 有多少线程,每个线程正在执行哪里的代码。
使用 thread 线程编号
可以切换到对应的线程去,然后使用 bt
命令可以查看对应线程从顶到底层的函数调用,以及上层调用下层对应的源码中的位置;当然,你也可以使用 frame 栈函数编号
(栈函数编号即下图中的 #0 ~ #4,使用 frame 命令时不需要加 #)切换到当前函数调用堆栈的任何一层函数调用中去,然后分析该函数执行逻辑,使用 print
等命令输出各种变量和表达式值,或者进行单步调试。
接着我们分别通过得到的各个线程的线程函数名去源码中搜索,找到创建这些线程的函数(下文为了叙述方便,以 f 代称这个函数),再接着通过搜索 f 或者给 f 加断点重启程序看函数 f 是如何被调用的,这些操作一般在程序初始化阶段。
redis-server 1 号线线程是在 main 函数中创建的,我们再看下 2 号线程的创建,使用 thread 2
切换到 2号线程,然后使用 bt 命令查看 2 号线程的调用堆栈,得到 2 号线程的线程函数为 bioProcessBackgroundJobs
,注意在顶层的 clone
和 start_thread
是系统函数,我们找的线程函数应该是项目中的自定义线程函数。
bioProcessBackgroundJobs
函数,我们发现
bioProcessBackgroundJobs
函数在
bioInit
中被调用,而且确实是在
bioInit
函数中创建了线程 2,因此我们看到了
pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0)
这样的调用。
1//bio.c 96行
2void bioInit(void) {
3 //...省略部分代码...
4
5 for (j = 0; j 6 void *arg = (void*)(unsigned long) j;
7 //在这里创建了线程 bioProcessBackgroundJobs
8 if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
9 serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
10 exit(1);
11 }
12 bio_threads[j] = thread;
13 }
14}
此时,我们可以继续在项目中查找 bioInit
函数,看看它在哪里被调用的,或者直接给 bioInit
函数加上断点,然后重启 redis-server,等断点触发,使用 bt
命令查看此时的调用堆栈就知道 bioInit
函数在何处调用的了。
1(gdb) b bioInit
2Breakpoint 1 at 0x498e5e: file bio.c, line 103.
3(gdb) r
4The program being debugged has been started already.
5Start it from the beginning? (y or n) y
6Starting program: /root/redis-6.0.3/src/redis-server
7[Thread debugging using libthread_db enabled]
8//...省略部分无关输出...
9Breakpoint 1, bioInit () at bio.c:103
10103 for (j = 0; j 11(gdb) bt
12#0 bioInit () at bio.c:103
13#1 0x0000000000431b5d i