LV089-gdb-多线程调试
多线程程序的编写更容易产生异常或 Bug(例如线程之间因竞争同一资源发生了死锁、多个线程同时对同一资源进行读和写等等)。
一、多线程调试
GDB 调试器不仅仅支持调试单线程程序,还支持调试多线程程序。本质上讲,使用 GDB 调试多线程程序的过程和调试单线程程序类似,不同之处在于,调试多线程程序需要监控多个线程的执行过程,进而找到导致程序出现问题的异常或 Bug,而调试单线程程序只需要监控 1 个线程。
1. 常用命令
| 调试命令 | 功 能 |
|---|---|
| info threads | 查看当前调试环境中包含多少个线程,并打印出各个线程的相关信息,包括线程编号(ID)、线程名称等。 |
| thread id | 将线程编号为 id 的线程设置为当前线程。 |
| thread apply id... command | id... 表示线程的编号;command 代指 GDB 命令,如 next、continue 等。整个命令的功能是将 command 命令作用于指定编号的线程。当然,如果想将 command 命令作用于所有线程,id... 可以用 all 代替。 |
| break location thread id | 在 location 指定的位置建立普通断点,并且该断点仅用于暂停编号为 id 的线程。 |
| set scheduler-locking off|on|step | 默认情况下,当程序中某一线程暂停执行时,所有执行的线程都会暂停;同样,当执行 continue 命令时,默认所有暂停的程序都会继续执行。该命令可以打破此默认设置,即只继续执行当前线程,其它线程仍停止执行。 |
表 1 也仅罗列了 GDB 调试多线程程序的一部分常用命令,有关更多其他命令,可以到 GDB 官网 了解。
2. 测试程序
#include <stdio.h>
#include <pthread.h>
void *thread_job(void *name) {
char *thread_name = (char *)name;
printf("this is %s\n", thread_name);
printf("hello world!\n");
return NULL;
}
int main(int argc, const char *argv[]) {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread_job, "thread1_job");
pthread_create(&tid2, NULL, thread_job, "thread2_job");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("this is main\n");
return 0;
}程序中包含 3 个线程,分别为 main 主线程、tid1 子线程和 tid2 子线程。我们在 ubuntu 中测试,所以直接用下面的命令编译:
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 037-gdb-multi-pthread.c -g -lpthread二、相关命令
1. 查看所有线程
1.1 info threads 命令
info threads 命令的功能有 2 个,既可以查看当前调试环境下存在的线程数以及各线程的具体信息,也可以通过指定线程的编号查看某个线程的具体信息。info threads 命令的完整语法格式如下:
(gdb) info threads [id...]其中,参数 id... 作为可选参数,表示要查看的线程编号,编号个数可以是多个。
1.2 调试示例
✓ sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main ≡ +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) l # <=== 1. 显示源码
1 #include <stdio.h>
2 #include <pthread.h>
3
4 void *thread_job(void *name) {
5 char *thread_name = (char *)name;
6 printf("this is %s\n", thread_name);
7 printf("hello world!\n");
8 return NULL;
9 }
10
(gdb) b 5 # <=== 2. 在第 5 行打上断点
Breakpoint 1 at 0x11d9: file 037-gdb-multi-pthread.c, line 5.
(gdb) r # <=== 3. 运行程序
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7d99700 (LWP 27101)] # <=== 新线程
[New Thread 0x7ffff7598700 (LWP 27102)] # <=== 新线程
[Switching to Thread 0x7ffff7d99700 (LWP 27101)] # <=== 该线程作为当前线程,它最先创建,最先执行,也会最先碰到断点
# <=== 程序暂停
Thread 2 "a.out" hit Breakpoint 1, thread_job (name=0x55555555601d) at 037-gdb-multi-pthread.c:5
5 char *thread_name = (char *)name;
(gdb) info threads # <=== 4. 查看所有线程
Id Target Id Frame
1 Thread 0x7ffff7d9a740 (LWP 27097) "a.out" __pthread_clockjoin_ex (threadid=140737351620352,
thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
* 2 Thread 0x7ffff7d99700 (LWP 27101) "a.out" thread_job (name=0x55555555601d)
at 037-gdb-multi-pthread.c:5
3 Thread 0x7ffff7598700 (LWP 27102) "a.out" thread_job (name=0x555555556029)
at 037-gdb-multi-pthread.c:5要知道,使用 GDB 调试多线程程序时,同一时刻我们调试的焦点都只能是某个线程,被称为 当前线程。整个调试过程中,GDB 调试器总是会从当前线程的角度为我们打印调试信息。如上所示,当执行 r 启动程序后,GDB 编译器自行选择标识号为 LWP 54283(编号为 2)的线程作为当前线程,则随后打印的暂停运行的信息就与该线程有关,而没有打印出编号为 1 和 3 的暂停信息。
GDB 调试器为了方便用户快速识别出当前线程,执行 info thread 命令后,Id 列前标有 * 号的线程即为当前线程。
Tips:我们输入的调试命令并不仅仅作用于当前线程,例如 continue、next 等,默认情况下它们作用于所有线程。
2. 调整当前线程
2.1 thread id 命令
用 GDB 调试多线程程序的过程中,根据需要可以随时对当前线程进行调整,这就需要用到 thead id 命令。
(gdb) thread idthread id 命令用于将编号为 id 的线程设定为当前线程。
2.2 调试示例
(gdb) info threads # <=== 4. 查看所有线程
Id Target Id Frame
1 Thread 0x7ffff7d9a740 (LWP 27097) "a.out" __pthread_clockjoin_ex (threadid=140737351620352,
thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
* 2 Thread 0x7ffff7d99700 (LWP 27101) "a.out" thread_job (name=0x55555555601d)
at 037-gdb-multi-pthread.c:5
3 Thread 0x7ffff7598700 (LWP 27102) "a.out" thread_job (name=0x555555556029)
at 037-gdb-multi-pthread.c:5
(gdb) thread 3 # <=== 5. 将 id 为 3 的线程作为当前线程
[Switching to thread 3 (Thread 0x7ffff7598700 (LWP 27102))]
#0 thread_job (name = 0x555555556029) at 037-gdb-multi-pthread.c: 5
5 char *thread_name = (char *)name;
(gdb) info threads # <=== 6. 查看所有线程信息
Id Target Id Frame
1 Thread 0x7ffff7d9a740 (LWP 27097) "a.out" __pthread_clockjoin_ex (threadid=140737351620352,
thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
2 Thread 0x7ffff7d99700 (LWP 27101) "a.out" thread_job (name=0x55555555601d)
at 037-gdb-multi-pthread.c:5
* 3 Thread 0x7ffff7598700 (LWP 27102) "a.out" thread_job (name=0x555555556029)
at 037-gdb-multi-pthread.c:5
(gdb)可以看到,改变当前线程的同时,GDB 调试器为我们打印出了该线程暂停执行的具体信息。再次执行 info threads 命令可以看到,编号为 3 的线程确实成为了新的当前线程。
3. 执行特定线程
3.1 thread apply id... command 命令
如果想单独控制某一线程进行指定的操作,可以借助 thread apply id... command 命令实现:
(gdb) thread apply id... command参数 id... 表示要控制的目标线程的编号,编号个数可以是多个。如果想控制所有线程,可以用 all 代替书写所有线程的编号;参数 command 表示要目标线程执行的操作,例如 next、continue 等。
3.2 调试示例
(gdb) info threads # <=== 6. 查看所有线程信息
Id Target Id Frame
1 Thread 0x7ffff7d9a740 (LWP 27097) "a.out" __pthread_clockjoin_ex (threadid=140737351620352,
thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
2 Thread 0x7ffff7d99700 (LWP 27101) "a.out" thread_job (name=0x55555555601d)
at 037-gdb-multi-pthread.c:5
* 3 Thread 0x7ffff7598700 (LWP 27102) "a.out" thread_job (name=0x555555556029)
at 037-gdb-multi-pthread.c:5
(gdb) thread apply 2 next # <=== 7. 单步执行 id = 2 的线程
Thread 2 (Thread 0x7ffff7598700 (LWP 27102)):
[Switching to Thread 0x7ffff7598700 (LWP 27102)] # <=== 由于 3 号线程暂停执行,所以这里切换 3 号线程作为当前线程
# <=== 这里运行到了 3 号线程第 5 行停下了
Thread 3 "a.out" hit Breakpoint 1, thread_job (name=0x555555556029) at 037-gdb-multi-pthread.c:5
5 char *thread_name = (char *)name;
(gdb) thread apply 2 next # <=== 8. 再次让 id = 2 的线程单步执行
this is thread2_job # <=== 下面的打印是 id = 3 的线程的???
hello world!
Thread 2 (Thread 0x7ffff7d99700 (LWP 27101)):
[Thread 0x7ffff7598700 (LWP 27102) exited] # <=== 3 号线程退出了???
6 printf("this is %s\n", thread_name); # <===
(gdb)我们调用 thread apply 2 next 命令对 2 号线程进行逐步调试时,3 号线程也会运行,这是为什么呢?这和 GDB 调试器的调试机制有关。默认情况下,无论哪个线程暂停执行,其它线程都会随即暂停;反之,一旦某个线程启动(借助 next、step、continue 命令),其它线程也随即启动。GDB 调试默认的这种调试模式(称为 全停止模式),一定程序上可以帮助我们更好地监控程序中各个线程的执行。
Tips:当对某个线程进行单步调试时,其它线程也会随即执行和停止,但执行的往往不只是一行代码,可能是多行代码。
4. 为特定线程设置断点
4.1 break location thread id
当调试环境中拥有多个线程时,我们可以选择为特定的线程设置断点,该断点仅对指定线程有效。为特定的某个线程设置断点,可以使用如下命令:
(gdb) break location thread id
(gdb) break location thread id if...location 表示设置断点的具体位置;id 表示断点要作用的线程的编号;if... 参数作用指定断点激活的条件,即只有条件符合时,断点才会发挥作用。
Tips:默认情况下,当某个线程执行遇到断点时,GDB 调试器会自动将该线程作为当前线程,并提示用户 "[Switching to Thread n]",其中 n 即为新的当前线程。
4.2 调试示例
(gdb) thread apply 2 next # <=== 8. 再次让 id = 2 的线程单步执行
this is thread2_job
hello world!
Thread 2 (Thread 0x7ffff7d99700 (LWP 27101)):
[Thread 0x7ffff7598700 (LWP 27102) exited]
6 printf("this is %s\n", thread_name);
(gdb) break 7 thread 3 # <=== 9.在 id = 3 的线程的第 7 行打上断点
Breakpoint 2 at 0x5555555551f9: file 037-gdb-multi-pthread.c, line 7.
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551d9 in thread_job at 037-gdb-multi-pthread.c:5
breakpoint already hit 2 times
2 breakpoint keep y 0x00005555555551f9 in thread_job
at 037-gdb-multi-pthread.c:7 thread 3
stop only in thread 3可以看到,我们在第 7 行代码处为 3 号线程单独设置了一个普通断点,该断点仅对 3 号线程有效。
5. 设置线程锁
使用 GDB 调试多线程程序时,默认的调试模式为:一个线程暂停运行,其它线程也随即暂停;一个线程启动运行,其它线程也随即启动。要知道,这种调试机制确实能帮我们更好地监控各个线程的 "一举一动",但并非适用于所有场景。
一些场景中,我们可能只想让某一特定线程运行,其它线程仍维持暂停状态。要想达到这样的效果,就需要借助 set scheduler-locking 命令。 此命令可以帮我们将其它线程都 "锁起来",使后续执行的命令只对当前线程或者指定线程有效,而对其它线程无效。
5.1 set scheduler-locking mode
set scheduler-locking 命令的语法格式如下:
(gdb) set scheduler-locking mode其中,参数 mode 的值有 3 个,分别为 off、on 和 step,它们的含义分别是:
- off:不锁定线程,任何线程都可以随时执行;
- on:锁定线程,只有当前线程或指定线程可以运行;
- step:当单步执行某一线程时,其它线程不会执行,同时保证在调试过程中当前线程不会发生改变。但如果该模式下执行 continue、until、finish 命令,则其它线程也会执行,并且如果某一线程执行过程遇到断点,则 GDB 调试器会将该线程作为当前线程。
同时,我们可以通过执行 show scheduler-locking 命令,查看各个线程锁定的状态:
(gdb) show scheduler-locking5.2 调试示例
这个我们重新运行上面的测试程序:
✓ sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main ≡ +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) l
1 #include <stdio.h>
2 #include <pthread.h>
3
4 void *thread_job(void *name) {
5 char *thread_name = (char *)name;
6 printf("this is %s\n", thread_name);
7 printf("hello world!\n");
8 return NULL;
9 }
10
(gdb) b 5
Breakpoint 1 at 0x11d9: file 037-gdb-multi-pthread.c, line 5.
(gdb) r
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7d99700 (LWP 27738)]
[New Thread 0x7ffff7598700 (LWP 27739)]
[Switching to Thread 0x7ffff7d99700 (LWP 27738)]
Thread 2 "a.out" hit Breakpoint 1, thread_job (name=0x55555555601d) at 037-gdb-multi-pthread.c:5
5 char *thread_name = (char *)name;
(gdb) info threads
Id Target Id Frame
1 Thread 0x7ffff7d9a740 (LWP 27734) "a.out" __pthread_clockjoin_ex (threadid=140737351620352,
thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
* 2 Thread 0x7ffff7d99700 (LWP 27738) "a.out" thread_job (name=0x55555555601d)
at 037-gdb-multi-pthread.c:5
3 Thread 0x7ffff7598700 (LWP 27739) "a.out" thread_job (name=0x555555556029)
at 037-gdb-multi-pthread.c:5
(gdb) set scheduler-locking on
(gdb) show scheduler-locking
Mode for locking scheduler during execution is "on".
(gdb) n
6 printf("this is %s\n", thread_name);
(gdb) info threads
Id Target Id Frame
1 Thread 0x7ffff7d9a740 (LWP 27734) "a.out" __pthread_clockjoin_ex (threadid=140737351620352,
thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
* 2 Thread 0x7ffff7d99700 (LWP 27738) "a.out" thread_job (name=0x55555555601d)
at 037-gdb-multi-pthread.c:6
3 Thread 0x7ffff7598700 (LWP 27739) "a.out" thread_job (name=0x555555556029)
at 037-gdb-multi-pthread.c:5
(gdb)可以看到,通过执行 set scheduler-locking on 命令,接下来的 next 命令只对当前线程(2 号线程)有效,其它线程仍保持原有的暂停状态。
三、non-stop 模式
1. non-stop 简介
对于调试多线程程序,GDB 默认采用的是 all-stop 模式,即只要有一个线程暂停执行,所有线程都随即暂停。这种调试模式可以适用于大部分场景的需要,借助适当数量的断点,我们可以清楚地监控到各个线程的具体执行过程。
但在某些场景中,我们可能需要调试个别的线程,并且不想在调试过程中,影响其它线程的运行。这种情况下,可以将 GDB 的调试模式由 all-stop 模式更改为 non-stop 模式,该模式下调试多线程程序,当某一线程暂停运行时,其它线程仍可以继续执行。
Tips:注意,只有 7.0 版本以上的 GDB 调试器,才支持 non-stop 模式。
也就是说,non-stop 模式下可以进行 all-stop 模式无法做到的调试工作,例如:
- 保持其它线程继续执行的状态下,单独调试某个线程;
- 在所有线程都暂停执行的状态下,单步调试某个线程;
- 单独执行多个线程等等。
另外还有一点和 all-stop 模式不同的是,在 all-stop 模式下,continue、next、step 命令的作用对象并不是当前线程,而是所有的线程;但在 non-stop 模式下,continue、next、step 命令只作用于当前线程。
Tips:在 non-stop 模式下,如果想要 continue 命令作用于所有线程,可以为 continue 命令添加一个 -a 选项,即执行 continue -a 或者 c -a 命令,即可实现令所有线程继续执行的目的。
2. 切换模式
那么,GDB 调试多线程程序时,怎样才能由 all-stop 模式转换到 non-stop 模式呢?未启动程序前执行如下命令即可:
(gdb) set non-stop mode其中,mode 参数的值有 2 种,分别是 on 和 off,on 表示启用 non-stop 模式;off 表示禁用 non-stop 模式。
3. 使用示例
3.1 测试程序
#include <stdio.h>
#include <pthread.h>
static void *thread1_job() {
printf("this is 1\n");
return NULL;
}
static void *thread2_job() {
printf("this is 2\n");
return NULL;
}
int main(int argc, const char *argv[]) {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread1_job, NULL);
pthread_create(&tid2, NULL, thread2_job, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("this is main\n");
return 0;
return 0;
}程序中包含 3 个线程,分别为 main 主线程、tid1 子线程和 tid2 子线程。我们在 ubuntu 中测试,所以直接用下面的命令编译:
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 038-gdb-multi-pthread-non-stop.c -g -lpthread3.2 调试示例
✓ sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main ≡ +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) set non-stop on # <=== 1. 开启non-stop模式
(gdb) b 5 # <=== 2. 在第5行打断点,第5行时线程1的线程执行函数
Breakpoint 1 at 0x11b1: file 038-gdb-multi-pthread-non-stop.c, line 5.
(gdb) r # <=== 3. 运行程序
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7d99700 (LWP 29488)] # <=== 新线程
[New Thread 0x7ffff7598700 (LWP 29489)] # <=== 新线程
this is 2 # <=== 线程执行函数thread2_job的打印信息
# <=== 线程执行函数thread1_job中暂停
Thread 2 "a.out" hit Breakpoint 1, thread1_job () at 038-gdb-multi-pthread-non-stop.c:5
5 printf("this is 1\n");
(gdb) [Thread 0x7ffff7598700 (LWP 29489) exited] # <=== 执行函数为thread2_job的这个线程退出了
(gdb) info threads # <=== 4. 查看所有线程,只有主进程和暂停的线程了
Id Target Id Frame
* 1 Thread 0x7ffff7d9a740 (LWP 29484) "a.out" (running)
2 Thread 0x7ffff7d99700 (LWP 29488) "a.out" thread1_job ()
at 038-gdb-multi-pthread-non-stop.c:5
(gdb)