GDB-深化讨论经常使用-调试多线程死锁之谜 (深化什么)
多线程编程是现代软件开发中的一项关键技术,但随之而来的应战之一是多线程死锁。多线程死锁是程序中的一种经常出现疑问,它会造成线程相互期待,堕入不可继续口头的形态。这里,咱们将讨论多线程死锁的概念、原理,同时咱们经过一个例子来引见如何经常使用GDB(GNUDebugger)这一工具来排查和处置多线程死锁疑问。
多线程死锁的概念
多线程死锁是多线程编程中的一种关键疑问。它出当初多个线程试图失掉一组资源(理论是锁或资源对象)时,造成彼此相互期待的状况。详细来说,当线程1持有资源A并期待资源B,而线程2持有资源B并期待资源A时,就或者出现死锁。
多线程死锁原理
为了更好地理解多线程死锁的原理,让咱们思考一个便捷的示例。假定有两个资源A和B,以及两个线程(Thread1和Thread2)。线程1须要失掉资源A和B,线程2须要失掉资源B和A。假设线程1失掉了资源A,而线程2失掉了资源B,它们都不可继续,由于它们都须要对方持有的资源才干继续。这就是典型的死锁状况。
多线程死锁理论出当初以下状况下:
多线程死锁之所以会出现,是由于线程之间的相互依赖和期待。当多个线程须要共享资源时,它们或者会按不同的顺序失掉这些资源,造成资源互斥疑问,最终引发死锁。
排查多线程死锁
GDB是一个弱小的调试工具,可以用来排查多线程死锁疑问。上方经过一个例子来说下如何经常使用gdb调试死锁疑问,这也是前段期间我碰锁疑问新学到的技艺。
便捷的代码如下:
#include<stdio.h>#include<pthread.h>#include<stdlib.h>#include<unistd.h>#include<time.h>pthread_mutex_tmutex1=PTHREAD_MUTEX_INITIALIZER;pthread_mutex_tmutex2=PTHREAD_MUTEX_INITIALIZER;pthread_cond_texit_condition=PTHREAD_COND_INITIALIZER;intshould_exit=0;void*thread1_function(void*arg){while(1){printf("Thread1:Attemptingtoacquiremutex1...n");pthread_mutex_lock(&mutex1);printf("Thread1:Acquiredmutex1.n");printf("Thread1:Attemptingtoacquiremutex2...n");pthread_mutex_lock(&mutex2);printf("Thread1:Acquiredmutex2.n");//在此处审核能否应该分开if(should_exit){pthread_mutex_unlock(&mutex2);pthread_mutex_unlock(&mutex1);break;}pthread_mutex_unlock(&mutex1);pthread_mutex_unlock(&mutex2);}printf("Thread1exitdone!n");pthread_exit(NULL);}void*thread2_function(void*arg){sleep(5);//让线程2休眠10秒钟printf("Thread2:Attemptingtoacquiremutex2...n");pthread_mutex_lock(&mutex2);printf("Thread2:Acquiredmutex2.n");printf("Thread2:NotifyingThread1toexit...n");should_exit=1;pthread_cond_signal(&exit_condition);//经过不监禁该锁制作死锁pthread_mutex_unlock(&mutex2);printf("Thread2exitdone!n");//exit口头后不会再口头该函数前面局部pthread_exit(NULL);}intmn(){pthread_tthread1,thread2;pthread_create(&thread1,NULL,thread1_function,NULL);pthread_create(&thread2,NULL,thread2_function,NULL);pthread_join(thread1,NULL);pthread_join(thread2,NULL);return0;}
代码很便捷,经过创立两个线程,线程1睡眠5s为mutex2加锁并通知线程1启动分开,之后线程2分开,线程1是个while循环,不停的对mutex1启动加解锁,并加锁后检测能否分开,分开则对mutex2启动加锁打印,而后监禁mutex1、mutex2启动分开。
经常使用:gccthread.c-g-lpthread-othread编译,由于要gdb调试所以须要带上-g参数,反常现象会口头完结打印如下:
如今咱们屏蔽掉线程2监禁mutex2启动死锁调试:
void*thread2_function(void*arg){sleep(5);//让线程2休眠10秒钟printf("Thread2:Attemptingtoacquiremutex2...n");pthread_mutex_lock(&mutex2);printf("Thread2:Acquiredmutex2.n");printf("Thread2:NotifyingThread1toexit...n");should_exit=1;pthread_cond_signal(&exit_condition);//经过不监禁该锁制作死锁//pthread_mutex_unlock(&mutex2);printf("Thread2exitdone!n");//exit口头后不会再口头该函数前面局部pthread_exit(NULL);}
实践环境中咱们并不知道死锁出现,所以咱们经过gdb先运转一次性直到程序不可反常分开时,口头bt检查堆栈:
这里由于加了打印所以很快可以看到mutex2上锁那里卡住,实践环境会有很多线程运转,咱们并不直到哪里会有疑问,此时只能经过bt检查堆栈咱们发现卡在函数__futex_abstimed_wait_common64,运转到./nptl/futex-internal.c文件第57行。
这里咱们只有要知道该函数__futex_abstimed_wait_common64是内核中用于处置互斥锁期待超时的一个外部函数即可。
此时可以判定代码存在死锁疑问了,咱们继续排查。
咱们继续看bt信息,发现该期待是从#40x00005555555553c8inmain()atthread.c:59调入的,由于前面是#4,所以经常使用f4进入该函数。
咱们发现是main里调入,同时在口头thread1的pthread_join,所以前面的__futex_abstimed_wait_common64并不是咱们真正要找的疑问,其实thread1曾经到来了join的位置,期待完结了。咱们继续口头threadlyallbt把一切线程堆栈打进去看下:
依据前面剖析thread1曾经反常分开了,咱们这里看到thread2卡在futex_wait,依据高低文十分显著是在期待futexlock,再往下看咱们发现锁mutex2,这里就是thread2在期待mutex2,那么mutex2被谁lock住没监禁呢?咱们经过pmutex2来检查owner即可知道该锁被谁领有。
这里有个疑问,是由于该代码凑巧thread1分开期待join了,所以这里的23890是个内核线程,在持有着mutex2,实践环境中咱们会看到owner大略会是infothreads中的LWP,于是就可以定位到该锁被谁持有没有监禁了,再剖析代码即可。
我把thread1再改下,不间接分开而是不时while(1)的外形来测试,此时再经过上述来查找mutex2被谁持有即可直观看到:
在Linux下综合使用vi、gcc编译器和gdb调试器开发汉诺塔游戏程序
结果是游戏可以正常运行。
Linux,全称GNU/Linux,是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯·本纳第克特·托瓦兹于1991年10月5日首次发布,它主要受到Minix和Unix思想的启发,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。
它能运行主要的Unix工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。Linux有上百种不同的发行版,如基于社区开发的debian、archlinux,和基于商业开发的Red Hat Enterprise Linux、SUSE、Oracle Linux等。
2021年6月,根据Linux 5.14刚刚进入合并队列的char-misc-next提交,Linux 5.14正式移除了RAW驱动。
Linux操作系统的诞生、发展和成长过程始终依赖着五个重要支柱:Unix操作系统、MINIX操作系统、GNU计划、POSIX标准和Internet网络。
20世纪80年代,计算机硬件的性能不断提高,PC的市场不断扩大,当时可供计算机选用的操作系统主要有Unix、DOS和macOS这几种。Unix价格昂贵,不能运行于PC;DOS显得简陋,且源代码被软件厂商严格保密;MacOS是一种专门用于苹果计算机的操作系统。
linux多线程程序怎么调试
多线程程序可能存在很多潜在的bug,如data race,dead lock,信号bug等,而这些bug一向很难调试,现在有很多论文都是基于多线程程序的调试技术的,比如model check,死锁检测,replay技术等,也有很多对应的工具,如intel的pinplay,微软的Zing等。 关于这些技术和工具,如果感兴趣可以 google相应的论文进一步了解。 这里我主要讲述的是我在对二进制翻译下多线程程序调试中经常使用的一些方法以及一些调试经验,虽然我的调试的是二进制翻译器,但是这些方法也同样适用于大多数多线程程序。 1、最直接的方法就是在源程序插入printf语句来打印出一些有用的变量。 这种方法的优点是不用借助其他工具就可以对程序的运行进行观察,缺点是插入语句的位置、粒度等都需要调试者自己去权衡,如果插入过多的打印语句,则频繁的IO操作会使程序运行变慢,线程行为改变,有些bug甚至不会再出现。 至于需要在什么地方插入语句,首先,只打印有必要的变量,一个语句可以打印多个变量;其次,在循环中,我们可以通过设置一些条件来降低打印的粒度,比如下面这段代码:1 2 3 4 5 6 7 8while(flag){ pc = getpc(); printf(“pc is:0x%x\n”, pc);//我们插入的打印语句 ...... ...... //do somthing using pc; }假设我们对pc的取值很感兴趣,需要打印出所有pc取到过的值,但是大多数情况下,getpc()的返回值都同上一次的返回值相同,这样我们printf出来的就会有很多重复值。 这种情况下我们可以用下面这种插桩方式来去处重复值:1 2 3 4 5 6 7 8 9 10 11int lastpc; //定义为全局变量或局部静态变量 while(flag){ pc = getpc(); if(pc !=lastpc){ lastpc = pc; printf(“pc is:0x%x\n”, pc); } ...... ......//do somthing using pc }这样通过一个简单的判断就可以省掉很多没有必要的输出。 很多别的情形,比如我们只关心某一变量等于特定值(比如0)时其他变量的状态,我们就没有必要把改变量不等于0时的状态打印出来。 总之,能省则省,只打印我们需要的。 2、利用gdb的attach功能和sleep()函数。 gdb是由gnu维护的功能强大的调试工具,并且支持多线程程序的调试,可以在gdb下直接运行一个多线程程序,通过thread等命令进行调试。 但是很多多线程程序在其他工具(gdb,pin,strace等)监管下,原有的bug就不会出现。 这的确是很让人头疼的事情,也是我十分不喜欢这个方法的原因,想象一下,一个程序直接跑就出错,但是放到gdb下就能得到正确的结果,好像故意在耍我们一样。 我更喜欢使用gdb的attach功能,我们可以通过下面的命令来让gdb接管一个运行的线程:1gdb attach <pid>这种方法的好处是能够使gdb对程序执行的影响最小,而且可以只接管程序中某一条我们所关心的线程,而其他线程不受影响。 这时有人会问,如果线程执行过快,我们还没来得及attach线程就已经执行完或者dump掉了,这种情况该怎么办?解决方法很简单,既然线程执行过快,我们就让它等一等,可以在源代码中让我们关心这个线程sleep()一小会儿,这样我们就有足够的时间来attach它,并且attach的位置我们也可以进行控制,想在哪里attach,就在哪里sleep。 3、第三种方法是利用信号处理函数来获取一些信息。 在多线程程序的压力测试中,很多错误要每隔几百几千次运行才能出现一次,而这种错误的replay是很困难的,因此捕捉到这种错误的现场很重要。 这里我习惯利用信号处理程序来保存这样的现场,这样你可以晚上写个脚本让程序无限跑,早上起来你会发现程序停在出错的地方,这是很惬意的事情。 多数多线程程序出错,都是访问非法内存,也就是我们常说的“段错误”(segmentation fault),程序发生非法内存的访问,系统会发给线程一个SIGSEGV信号,这个信号默认处理为core掉该线程。 我们可以对这个信号进行利用,为其注册一个信号处理函数:1 2 3 4struct sigaction act; _flags = SA_SIGINFO; _sigaction = signal_handler; sigaction(SIGSEGV, &act, NULL); //SIGSEGV表示该信号的值信号处理函数如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14void signal_handler(int host_signum, siginfo_t *info, void *puc){ structucontext *uc = (struct ucontext *)puc; int loopflag = 1; while(loopflag)//可以在gdb中手动更改loopflag的值跳出循环 sleep(1); ...... //这里可以打印一些感兴趣的变量 }函数参数中,puc是一个体系结构相关的指针,不同的体系结构,指针指向的结构不一样,里面存放了发生信号时线程的寄存器的值,程序地址等信息,函数内第一句话的目的就是把void类型转换成ucontext结构类型,这样在gdb中可以直接print出该结构的成员。 函数中sleep的作用是让程序停在信号处理程序中,以给我们足够的时间进行attach。 如果想让程序继续运行下去,手动把loopflag修改为1即可。 用while循环的目的是我们可以在运行时手动控制sleep的时间。 这种方法同样适用于其他信号带来的bug,比如SIGBUS等。 在二进制翻译下,还可以使用这种方法对二进制翻译器信号处理进行跟踪和调试,具体使用读者可以自己去发掘。 4、利用strace得到我们关心的信息。 大多数情况下我们用strace的目的是跟踪系统调用,但其实strace对多线程程序的调试有很大的帮助,使用strace打印多线程程序信息的命令如下:1strace -F ./test如果我们对某些系统调用,如gettimeofday,ioctl不感兴趣,可以屏蔽掉1strace -F -etrace=\!gettimeofday,ioctl ./test通过strace打印出的信息,我们可以对什么时候产生了一个子线程,那个线程在等待,哪个线程被唤醒,哪个线程收到信号,哪个线程core掉有一个综合的了解,这些信息对多线程调试会起到很大的作用。 还有很多方法比如利用core文件等,很多地方可以查到,我不做累赘的介绍。 总之技术是死的,但是方法是灵活的,当传统方法解决不了一个问题的时候,可以放开思路尝试其他的方法。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。