使用gdb调试多线程的方法

  • Post author:
  • Post category:其他




1. 多线程死锁的调试方法:


  1. 方法一:

    kill -11 得到 coredump 然后分析:

    在出现死锁时,kill -11 + 进程ID,得到 coredump 然后分析;

  2. 方法二:

    打印日志 :

    把日志写的详细一些,可以反映出程序运行时的真实情况;

  3. 方法三:

    gdb单步调试:

    gdb法有个致命的缺陷,就是它打乱了多线程的真实调度顺序。

    多线程特定的错误往往是由于缺乏对共享数据的保护而导致的竞争状态的出现,而竞争状态的出现是具有偶然性的,

    取决于特定的调度次序

    ;而当设置断点时,实际是将调度器的权限由系统交给调试者。这样在调试环境重现的,可能并不是bug时的真实情况。



方法1: kill -11 调试coredump文件:


操作步骤:

kill -11 (pid)		//kill终止死锁进程
gdb + 可执行程序 + coredump文件	//gdb开始查看coredump文件
thread apply all bt		//查看所有线程的堆栈信息


使用举例:

一个死锁程序:test.c

#include <stdio.h>
#include <unistd.h> //sleep
#include <pthread.h>

pthread_mutex_t mtx_1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mtx_2 = PTHREAD_MUTEX_INITIALIZER;

void *func_1(void *arg) {
    printf("i am func_1\n");
    pthread_mutex_lock(&mtx_1);
    sleep(1);
    pthread_mutex_lock(&mtx_2);

    return NULL;
}

void *func_2(void *arg) {
    printf("i am func_2\n");
    pthread_mutex_lock(&mtx_2);
    sleep(1);
    pthread_mutex_lock(&mtx_1);
    
    return NULL;
}

int main() {
    pthread_t tid_1;
    pthread_t tid_2;

    pthread_create(&tid_1, NULL, func_1, NULL);
    pthread_create(&tid_2, NULL, func_2, NULL);

    pthread_join(tid_1, NULL);
    pthread_join(tid_2, NULL);

    return 0;
}

上述示例程序是一个会产生死锁的错误程序:

线程1拿到互斥锁A等待互斥锁B,线程2拿到互斥锁B等待互斥锁A。


调试过程:

ps -aux | grep test		//查询test程序的进程号

kill -11 11440			//强制test进程产生coredump文件

gdb ./test core.test.11440	//开始调试coredump文件


(gdb) thread apply all bt

Thread 3 (Thread 0x7f5739841700 (LWP 11441)):
#0  0x00007f5739c1837d in __lll_lock_wait () from /lib64/libpthread.so.0
#1  0x00007f5739c11e5d in pthread_mutex_lock () from /lib64/libpthread.so.0     <===========
#2  0x000000000040073f in func_1 (arg=0x0) at test.c:8                          <===========
#3  0x00007f5739c0f73a in start_thread () from /lib64/libpthread.so.0
#4  0x00007f5739949e0f in clone () from /lib64/libc.so.6

Thread 2 (Thread 0x7f5739040700 (LWP 11442)):
#0  0x00007f5739c1837d in __lll_lock_wait () from /lib64/libpthread.so.0
#1  0x00007f5739c11e5d in pthread_mutex_lock () from /lib64/libpthread.so.0     <===========
#2  0x000000000040077b in func_2 (arg=0x0) at test.c:17                         <===========
#3  0x00007f5739c0f73a in start_thread () from /lib64/libpthread.so.0
#4  0x00007f5739949e0f in clone () from /lib64/libc.so.6

Thread 1 (Thread 0x7f573a020700 (LWP 11440)):
#0  0x00007f5739c109bd in pthread_join () from /lib64/libpthread.so.0
#1  0x00000000004007cd in main () at test.c:26

通过查看所有线程的bt堆栈信息,可以看到子线程 11441 和 11442 在出现死锁时正在执行的函数分别是:

test.c 文件的第 8 行 和 第 17 行,执行的函数都是 pthread_mutex_lock() 。

这样就可以定位到引入死锁的位置。

(通过 coredump 文件只能定位到出现死锁时各线程的堆栈情况、正在执行的函数,至于出现死锁的逻辑原因,还要我们去亲自分析代码。 coredump 只是一个辅助定位手段。)



方法2:打印日志

一个打印技巧:

printf("%-20s, %3d, %6d, content = %d\n", 
		__func__, __LINE__, gettid(), content);

在调用printf打印时,可以使用C语言中的“预定义标识符”,包括:

__LINE__	:	当前程序的行号,十进制整型,%d
__FILE__	:	当前源文件名,字符串,%s
__DATE__	:	日期,字符串,%s
__TIME__	:	时间,字符串,%s

另外:

__func__	:	指示所在的函数,字符串,%s
				这个是由gcc支持的



方法3:gdb单步调试:

gbb ./active_threadpool		//直接开始执行多线程程序

start
r					//run

info inferiors		//查看进程信息
info threads		//查看线程信息

thread (n)			//切换到某个线程上去
					//注意参数 n 是gdb中的序号(1, 2, 3...),而不是 LWP 的tid (16088)

bt					//查看程序的调用栈信息,显示的是调用栈中的函数

b (n)				//breakpoint,打断点,参数n可以是行号或者【函数名】,如 `b 15` 或者 `b main`
info b

n					//next
s					//step

c					//continue,使程序“恢复执行”,继续执行到下一个断点处,如果没有下一个断点则一直执行到程序的最后
fin					//finish,当不小心单步进入了原本希望单步越过的函数时,使用fin返回

set (var)			//修改变量的值

thread apply all bt				
thread apply [thread-id-list]/[all] (args)		//在一系列线程上执行命令

print (var)				
print (struct)			//如果打印结构体,直接会{ }打印出结构体所有成员的值
print (p)				//如果打印指针,只会打印指针本身的值
print (*p)				//比如: print *pool;  会把指针所指向的变量的内容打印出来

set print pretty on		//美化打印,换行打印输出

set scheduler-locking off|on|step	//是否让所有的线程在gdb调试时都执行
									//off :(缺省)不锁定任何线程,也就是所有线程都执行
									//on :只有当前被调试的线程能够执行
									//step :阻止其他线程在当前线程【单步调试】的时候(即step时),抢占当前线程。只有当next、continue、util、finish的时候,其他线程才能重新运行。

set follow-fork-mode parent|child	//用于调试多进程:fork之后是要调试父进程还是调试子进程

attach (thread-ID)			//用于调试正在运行的进程,它允许开发人员中断程序,并查看其状态,之后还能让程序正常的执行



① start 和 run 指令的比较:

run和start指令都可以用来在GDB调试器中启动程序,二者的区别在于:

run指令会一直执行程序,直到执行结束或遇到第一个断点处;

start执行会执行至main函数的其实位置处停下来。

可以这样理解:


使用start指令启动程序,完全等价于先在main()主函数起始位置设置一个断点,然后再使用run指令启动程序。


另外,程序执行过程中使用run或者start指令,表示的是重新启动程序。



② next 与 step 指令的比较:

如果下一条要执行的语句是普通语句,那么step和next的行为是一模一样的:执行完那条普通语句,再次暂停住;

如果下一条要执行的语句是一个函数调用语句,那么step就会进入到这个函数之中,next就会直接越过这个函数(gdb在本地里悄悄的把这个函数执行完)。



③ continue 和 finish 指令的比较:

continue:使程序“恢复执行”,继续执行到下一个断点处,如果没有下一个断点则一直执行到程序的最后;

finish:当不小心单步进入了原本希望单步越过的函数时,使用fin返回。



2. 背景知识:



2.1 什么是“主线程”:

主线程是当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行。

因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。每个进程至少都有一个主线程。

主线程的重要性体现在两个方面:

(1)其他线程必须由主线程来创建;

(2)主线程应该是最后一个退出,否则如果主线程提前退出,其他子线程也会跟着退出,可能无法执行完成任务。

对于如何实现让主线程等待子线程全部执行完了再退出,目前掌握两种方法:

(1)主线程中调用 pthread_join() 等待所有线程执行完毕,但这种情况下子线程不能detach,子线程退出后的资源要考主线程来回收;

(2)实现一个“wait方法”(参考线程池的ntyThreadPoolWait),在主线程将任务全部传递给线程池后,调用 pthread_cond_wait() 等待在一个 wait_cv 条件变量上,当线程池处理完所有的任务后,signal 唤醒主线程继续向下执行,这种情况下子线程是可以 detach 的。



2.2 Linux下的 ps 命令:


ps = process status

(进程状态),用于显示当前进程的状态,

类似于Windows下的任务管理器

ps命令的参数众多,几个常用的:

ps -a			//列出所有进程
ps -au			//显示更多的信息
ps -aux			//显示所有包含其他使用者的进程


ps -aux

查看到的Linux进程的几种状态:

R	:	running,正在运行 或 在运行队列中等待
S	:	sleeping,正在休眠(例如阻塞挂起)
T	:	停止或被追踪(停止:进程收到SIGSTOP、SIGSTP、SIGINT、SIGOUT信号后停止运行)
Z	:	僵尸进程(进程已终止,但进程描述符存在,直到父进程调用wait4()系统调用后释放)
D	:	不可中断(收到信号也不会唤醒或运行,进程必须等待中断发生)



2.3 线程的查看:

ps -aux  | grep a.out		//查看进程
ps -aL	| geep a.out		//查看线程
pstree -p (主线程ID)			//查看线程关系,树状图



2.4 gettid() 和 pthread_self()的区别:

gettid() 获取的是内核中真实线程ID,pthread_self() 获取的是相对于子进程的线程控制块的首地址。

如何使用 gettid():

#include <sys/syscall.h>
#include <unistd.h> //pid_t

pid_t gettid()
{
	return syscall(SYS_gettid);
}



2.5 kill 命令:

为什么一定要用

kill -11

才能产生coredump文件?而

kill -9

却不会产生coredump文件?

kill命令是向进程发送信号:

kill + (signal) + (pid)

“kill -11”、“kill -9”这些数字实际上代表的是信号的值:

#define SIGKILL		9
#define SIGSEGV		11

“9” 表示的是强制终止进程,SIGKILL信号不能被屏蔽,不能被忽略;

“11” 表示的是强制生成coredump文件,相当于是向未定义的内存去写入数据。

“CTRL + C” 相当于是 发送 SIGINT(2) 。



版权声明:本文为ArtAndLife原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。