GDB-2——GDB调试多线程

 

一、简介

前一博文实际上已经介绍了多线程的调试方法,这节专门进行一下总结。

 

二、调试多线程

1. 使用 gdb 将程序跑起来,然后按 Ctrl + C 将程序中断下来,使用 info threads 命令查看当前进程有多少线程。

2. 使用 thread <线程编号> 可以切换到对应的线程,然后使用 bt 命令可以查看对应线程从顶到底层的函数调用,以及上层调用下层对应的源码中的位置;可以使用 frame <栈函数编号> 切换到当前函数调用堆栈的任何一层函数调用中去,然后分析该函数执行逻辑;使用 print 等命令输出各种变量和表达式值。

标号为1的线程是主线程,可以切到标号为1的线程上,然后 bt 打印出的调用栈就可以看到main()函数。对每个线程都进行这样的分析之后,基本上就可以搞清楚整个程序运行中的执行逻辑了。对不熟悉的C/C++项目搞一轮比较有帮助。

3. 调试时控制线程切换

在调试多线程程序时,有时候我们希望执行流一直在某个线程执行,而不是切换到其他线程。gdb 提供了一个在调试时将程序执行流锁定在当前调试线程的命令选项 scheduler-locking 选项,它有三个值,分别是 on、step 和 off,使用方法是 set scheduler-locking on/step/off。

(1) set scheduler-locking on 可以用来锁定当前线程,只观察这个线程的运行情况。当锁定这个线程时,其他线程就处于了暂停状态,也就是说你在当前线程执行 next、step、until、finish、return 命令时,其他线程是不会运行的。需要注意的是,你在使用 set scheduler-locking on/step 选项时要确认下当前线程是否是你期望锁定的线程,如果不是,可以使用 thread <线程编号> 切换到你需要的线程,然后再进行锁定。

(2) set scheduler-locking step 也是用来锁定当前线程,当且仅当使用 next 或 step 命令做单步调试时会锁定当前线程,如果使用 until、finish、return 等线程内的非单步调试命令,其他线程还是有机会运行的。相比较 on 选项值,step 选项值给为单步调试提供了更加精细化的控制,因为通常我们只希望在单步调试时,不希望其他线程对当前调试的各个变量值造成影响。

(3) set scheduler-locking off 用于关闭锁定当前线程。

4. 断点是全局的,无论哪个线程执行到断点位置,所有线程都会停住。

5. 实验

(1) 实验程序

 1 #include <stdio.h>
 2 #include <pthread.h>
 3 #include <unistd.h>
 4 
 5 int g = 0;
 6 
 7 void* worker_thread_1(void* p)
 8 {
 9     while (true) {
10         g = 100;
11         printf("worker_thread_1\n");
12         usleep(300000);
13     }
14     return NULL;
15 }
16 
17 void* worker_thread_2(void* p)
18 {
19     while (true)
20     {
21         g = -100;
22         printf("worker_thread_2\n");
23         usleep(500000);
24     }
25     return NULL;
26 }
27 
28 int main()
29 {
30     pthread_t thread_id_1;
31     pthread_create(&thread_id_1, NULL, worker_thread_1, NULL);
32     pthread_t thread_id_2;
33     pthread_create(&thread_id_2, NULL, worker_thread_2, NULL);
34     while (true) {
35         g = -1;
36         printf("g=%d\n", g);
37         g = -2;
38         printf("g=%d\n", g);
39         g = -3;
40         printf("g=%d\n", g);
41         g = -4;
42         printf("g=%d\n", g);
43         usleep(1000000);
44     }
45     return 0;
46 }

(2) 实验示范

lvm:~/origin_tmp/3.gdb$ g++ -g -o thread thread.cpp -lpthread
lvm:~/origin_tmp/3.gdb$ gdb thread
...
Reading symbols from thread...done.
(gdb) b 10
Breakpoint 1 at 0x4006d2: file thread.cpp, line 10.
(gdb) b 35
Breakpoint 2 at 0x40076b: file thread.cpp, line 35.
(gdb) r
Starting program: /origin_tmp/3.gdb/thread 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff77ef700 (LWP 219338)]
[New Thread 0x7ffff6fee700 (LWP 219339)]
worker_thread_2
[Switching to Thread 0x7ffff77ef700 (LWP 219338)]

Thread 2 "thread" hit Breakpoint 1, worker_thread_1 (p=0x0) at thread.cpp:10
10                      g = 100;
(gdb) until 43
worker_thread_2 //线程2在运行
[Switching to Thread 0x7ffff7fd9700 (LWP 219334)]

Thread 1 "thread" hit Breakpoint 2, main () at thread.cpp:35
35                      g = -1;
(gdb) until 43
worker_thread_2  //线程2在运行
worker_thread_1  //线程1在运行
g=-1             //主线程在运行
g=-2
g=-3
g=-4
main () at thread.cpp:43
43                      usleep(1000000);
(gdb) set scheduler-locking on  //设置 locking on
(gdb) until 43

Thread 1 "thread" hit Breakpoint 2, main () at thread.cpp:35
35                      g = -1;
(gdb) until 43  //可以看到locking on后只有主线程运行
g=-1
g=-2
g=-3
g=-4
main () at thread.cpp:43
43                      usleep(1000000);
(gdb) until 43  //可以看到locking on后只有主线程运行

Thread 1 "thread" hit Breakpoint 2, main () at thread.cpp:35
35                      g = -1;
(gdb) set scheduler-locking step  //设置locking step
(gdb) until 43
worker_thread_2
[Switching to Thread 0x7ffff77ef700 (LWP 219338)]

Thread 2 "thread" hit Breakpoint 1, worker_thread_1 (p=0x0) at thread.cpp:10
10                      g = 100;
(gdb) until 43  //locking step下,until还是多线程可以运行
g=100
g=-2
g=-3
g=-4
worker_thread_2
worker_thread_1

Thread 2 "thread" hit Breakpoint 1, worker_thread_1 (p=0x0) at thread.cpp:10
10                      g = 100;
(gdb) info threads  //切到主线程
  Id   Target Id         Frame 
  1    Thread 0x7ffff7fd9700 (LWP 219334) "thread" 0x00007ffff78bc38d in nanosleep () at ../sysdeps/unix/syscall-template.S:84
* 2    Thread 0x7ffff77ef700 (LWP 219338) "thread" worker_thread_1 (p=0x0) at thread.cpp:10
  3    Thread 0x7ffff6fee700 (LWP 219339) "thread" 0x00007ffff78bc38d in nanosleep () at ../sysdeps/unix/syscall-template.S:84
(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fd9700 (LWP 219334))]
#0  0x00007ffff78bc38d in nanosleep () at ../sysdeps/unix/syscall-template.S:84
84      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) n  //在locking step下单步运行情况下只有主线运行
main () at thread.cpp:34
34              while (true) {
(gdb) n

Thread 1 "thread" hit Breakpoint 2, main () at thread.cpp:35
35                      g = -1;
(gdb) n
36                      printf("g=%d\n", g);
(gdb) n
g=-1
37                      g = -2;
(gdb) n
38                      printf("g=%d\n", g);
(gdb) n
g=-2
39                      g = -3;
(gdb) n
40                      printf("g=%d\n", g);
(gdb) n
g=-3
41                      g = -4;
(gdb) n
42                      printf("g=%d\n", g);
(gdb) n
g=-4
43                      usleep(1000000);
(gdb) n
n
34              while (true) {

 

三、调试多进程

在实际的应用中,有一类应用会通过 Linux 函数 fork 出新的子进程。例如网络传输中的创建一个新的进程来处理该连接上的客户端,新产生的进程与原进程互为父子关系。那么如何用 gdb 调试这样父子进程呢,有两种方法:

(1) 方法1: 开两个 shell 终端分别对父子进程进行调试。用 gdb 先调试父进程,等子进程被 fork 出来后,重新开启一个 Shell 窗口,使用 gdb attach 到子进程上去。

(2) set follow-fork <parent/child> 来设置是当一个进程 fork 出新的子进程时,gdb 是继续调试父进程还是子进程,默认是父进程。使用 how follow-fork mode 来查看当前的调试模式。
默认是fork之后attach到父进程的,但是如果想在 fork 之后 gdb 去 attach 子进程,我们可以在程序 run 之前在 gdb 中设置 set follow-fork child 来实现。可以通过 Ctrl +C 将程序中断下来,然后使用 bt 命令查看当前线程调用堆栈来确定是主进程还是子进程。

 

posted on 2022-11-08 11:46  Hello-World3  阅读(3761)  评论(0编辑  收藏  举报

导航