C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...) https://blog.csdn.net/chenlycly/article/details/125529931 使用Windbg可以辅助排查多个线程之间的死锁问题,本文以一个问题实例讲解一下使用Windbg辅助排查死锁问题的相关细节,以供参考。
1、问题说明
有同事反馈,笔记本从待机状态恢复后,软件会弹出连不上会议服务器的提示:
而且长时间处于连不上的状态,始终无法恢复,导致没法进行会议操作。必须重启软件后才能正常进性会议操作,这个问题已经出现过好几次了,之前排查时始终没找出问题,这次邀请我来协助排查一下。
同事发现问题的场景是这样子的,笔记本待机前会议软件就处于启动状态,软件主界面处于最小化状态。笔记本待机恢复后,同事没有关注软件的连接状态,因为软件的主窗口处于掩藏状态,同事早晨要参加一个视频会议的,结果一直没有收到会议呼叫邀请,通过和参会人聊天得知,会议之前就开始了。于是同事点开会议软件,结果才发现软件一直连不上会议服务器,所以收不到会议呼叫邀请,所以一直没有入会。
2、初步怀疑底层的库发生堵死了
查看打印得知,笔记本从待机中恢复后,软件业务层有发起对服务器的登录,但底层的协议层始终没有回应,既没有登录成功的通知,也没有登录失败的通知。并且从日志文件中看,一开始是有协议模块的打印的,但在笔记本从待机中恢复后发起服务器注册之后,一直看不到协议层的打印日志(协议层是以回调的方式将日志回调给业务层,由业务层去写日志到文件中的),所以怀疑协议层的相关模块可能出现线程堵死的情况了,导致上层发来的后续事务处理请求都没有正常的响应。
因为协议模块不再有打印日志输出了,所以协议组的开发人员没法再通过打印日志去排查问题了。有时即便有打印日志,通过打印日志去猜可能是哪里出问题了,也是比较费劲的。
3、到出问题的笔记本上将Windbg附加到目标进程上
因为问题的现象不是必现的,此刻出问题的笔记本还保留着现场,需要相关开发人员到现场去分析一下。
协议组的同事找到我,希望我协助他们一下,将windbg附加到出问题的进程上,看看能不能看到他们协议模块线程的函数调用堆栈。他们想通过查看这些线程的函数调用堆栈,看看能不能找到线程堵塞的线索。
赶到现场后,先是在笔记本上安装了10.0版本的windbg,然后启动Windbg,将Windbg附加到目标进程上,输入~*kn命令将目标进程当前的所有线程的函数调用堆栈都打印出来。想通过函数调用堆栈中调用的函数确定哪些线程是协议模块的。但是输入~*kn命令后,线程的函数调用堆栈打印非常慢,同事的笔记本是工作用的,我不能在这个笔记本上操作太久。
于是想将Windbg从目标进程上Detach掉,然后从任务管理器中导出包含当前进程信息的转储文件(即全dump文件),然后将转储文件发到我们的机器上,我们回头慢慢分析,同事就可以使用笔记本了。
点击Windbg菜单栏中的Debug -> Detach Debugee,将Windbg从目标进程上分离出来,点击Detach后Windbg就卡死了,长时间没有反应。注意此时不能通过任务管理器将Windbg进程杀掉,如果强杀,因为Windbg已经附加在目标进程上融为一体,目标进程就会一起被处理掉,这样就没有现象了,显然是不能强杀Windbg的。
于是直接到任务管理器中找到目标进程,点击右键,在弹出右键菜单中点击“创建转储文件”菜单项:(截图以导出chrome.exe进程为例)
导出成功后会弹出如下的提示框:
点击“打开文件位置”按钮,在打开的文件夹中找到导出的dump文件,然后将dump文件发给我们,我们到我们工作机器上去分析。
4、任务管理器中导出的dump文件是64位的,需要使用命令切换到32位上下文
我们接收到dump文件,使用Windbg打开,输入~*kn命令将所有线程的堆栈打印出来。此时该命令执行很快,结果打印出来的函数调用堆栈很奇怪,根本看不到我们业务层的函数接口,打印的是一堆wow64.dll和wow64cpu.dll模块的内容,如下:
奥,对了,出问题的笔记本上的Windows系统是64位的,而我们的应用程序是32位的,通过任务管理器导出的dump文件,默认显示的是64位下的函数调用堆栈,需要使用**.effmach X86命令,将64位模式切换到32模式下,才能看到有效的函数调用堆栈。**
** **关于.effmach命令的说明,可以到windbg的帮助文档中去查看。点击windbg菜单栏中的Help -> Index,打开帮助.chm文档,在搜索框中输入.effmach,即可找到.effmach命令的条目,如下:
双击查看即可。也可以在windbg中输入.hh命令打开.chm帮助文档,不过通过此方式打开的帮助文档,左边默认打开的是目录标签页,需要手动切换到索引标签页中,手动输入命令,支持模糊匹配。
5、为什么我们的软件还使用32位版本,为啥编译64位版本呢?
32位程序虚拟内存不大,系统默认给32位程序分配4GB的虚拟内存,一般用户态和内核态各占2GB。
exe程序启动时,系统会先将exe依赖的所有dll加载到进程空间中,等加载完成后才会最终将exe启动起来。如果在系统中找不到依赖的dll库,则会报错;如果从依赖的dll库中找不到接口,也会报错。exe二进制文件和其依赖的dll二进制文件加载到进程空间中占用的是代码段的内存,这些内存也是归属在用户态的,如果exe依赖的dll文件比较多比较大,会占去不小的用户态虚拟内存空间,那留给代码的内存就变小了,然后各个模块在运行时都会占用内存,有一上来就申请的全局内存,也有使用过程中使用new或malloc动态申请的堆内存,很可能会出现总的用户态内存超过2GB的情况,就会出现Out of Memory内存用尽的崩溃。
如果编译成64位程序,则系统会给进程分配2的64次方的虚拟内存,这样虚拟内存要大的多,就不会有使用超限的问题了。那为啥还继续使用32位版本呢?虽然新出的Windows系统都是64位的,但还有部分Win7系统还是32位,为了保持对所有系统的兼容,所以使用32位版本。64位程序无法在32位系统上运行的,启动时就会报错的。
6、找来pdb文件,查看详细的函数调用堆栈,结合源代码,判断线程间发生了死锁
使用.effmach X86命令将上下文切换到32位后,命令行标识符也从“0:000>”变成了“0:000:x86>”,即多了个x86字样。X86对应的就是32位,X64对应的就是64位。然后在输入~*kn命令就能看到有效的32位函数调用堆栈了。但此时输出的函数调用堆栈很慢,命令行标识符位置会一直显示“*BUSY*”状态:
先放在一边让Windbg慢慢输出,然后去干别的事情。等过若干分钟后,看到了所有线程的函数调用堆栈:
等~*kn命令执行完成后,就会退出BUSY状态,此时从0号线程开始查看,查看所有线程的函数调用堆栈,根据函数调用堆栈中调用的函数确定是否是协议模块的线程,然后我们找到了77号线程,就是协议模块的线程,当前线程的函数调用堆栈最后一帧是WaitForSingleObject函数中,看到这可能是死锁了。
在77号线程的函数调用堆栈中,看到了业务dll模块的接口调用,但因为没有加载pdb文件,导致看不到具体的函数及代码的行号。
于是使用lm命令查看dll文件的时间戳(编译生成的时间),命令格式为:lm vm ntdll.dll,查看到的时间如下所示:
看到这个库的生成时间为2022年6月6日17点05分30秒。对于exe主程序的产品流,协议的dll库是发布过来的,于是到exe产品流上查看协议dll库的svn记录,看看2022年6月6日17点05分30秒生成的dll文件是什么时候发布过来的。找到了如下的发布记录:
在发布文字描述中会给出dll库在文件服务器上的路径,所有dll文件都会把pdb文件保存到文件版本服务器上的,于是到这个目录下就可以找到dll库对应的pdb文件了:
然后将pdb文件拷贝到桌面上,将pdb的路径设置到windbg中,然后windbg回去自动加载pdb,然后输入~77s命令,切换到77号线程中,然后再输入kn命令就可以看到详细的函数调用堆栈了,看到了具体的函数名,看到了具体的代码行号了,如下所示:
然后协议模块的开发人员,对照着源码,最终确定是线程间发生死锁,导致不能正常提供服务了。
这个地方主要使用Windbg查看发生死锁的线程的函数调用堆栈,最终是要结合源码分析为什么会发生死锁。如何锁使用临界区实现的,那这个锁是运行在用户态的,Windbg可以直接分析出哪几个线程之间发生了死锁(回头可以写一篇分析临界区锁引发的多线程间的死锁问题)。但如果锁是用互斥量或者信号量实现的,则使用的是内核态的对象,需要进行内核态调试分析的,而内核态的调试会复杂很多。
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。