介绍
多线程编程中,对于资源同步导致的死锁问题,应该是开发过程中经常碰到的,同时在问题定位过程中也会花费大量的时间,这里就介绍下如何通过windbg来分析死锁问题。
资源同步
windows下资源同步的方法主要是
- 原子访问
- 临界区
- 读写锁
- 旋转锁
- 等待函数
- 事件
- 信号量
- 互斥量
其中最常用的应该是临界区了(性能开销小),所以我也是拿临界区作为分析对象,其他资源同步不在此讨论。
临界区
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操 作共享资源的目的。
临界区包含两个操作原语:
EnterCriticalSection() 进入临界区
LeaveCriticalSection() 离开临界区
EnterCriticalSection() 语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的 LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本 进程内的线程,而不可用来同步多个进程中的线程。
结构体定义
1 | typedef struct _RTL_CRITICAL_SECTION { |
各个参数的解释如下:
第一个参数:PRTL_CRITICAL_SECTION_DEBUG DebugInfo;//调试用的
第二个参数:LONG LockCount;//初始化为-1,n表示有n个线程在等待
第三个参数:LONG RecursionCount;//表示该临界区的拥有线程对此资源获得临界区次数,初为0
第四个参数:HANDLE OwningThread;//即拥有该临界区的线程句柄
第五个参数:HANDLE LockSemaphore;//实际上是一个自复位事件
第六个参数:DWORD SpinCount;//旋转锁的设置,单CPU下忽略
由这个结构可以知道:临界区会记录拥有该临界区的线程句柄,即临界区是有“线程所有权”概念的。事实上它会用第四个参数OwningThread来记录获准进入临界区的线程句柄,如果这个线程再次进入,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新临界区并将等待中的线程换回可调度状态。
问题出现
服务器工作线程每5s会定时上报当前还有多少消息堆积未处理,正常情况应该都是<=10左右。如果出现cpu不足、io、死锁、假死等问题导致工作线程无法正常消费,那么消息堆积数量会逐渐上升。某天在告警群里面提示某服务的消息堆积从100、400、1000、3000、5000 逐渐上升,第一感觉就是服务出问题了,立刻让运维查看服务器cpu、内存、io,均正常,而且同物理主机的其他服务正常运行,所以问题应该是该服务程序出现了某些状况,可能是死锁、也可能是死循环、也可能是长时间sleep….. 立刻保存fulldump,然后重启服务,先恢复线上服务,以免给客户带来更大损失。
分析过程
下载dump
下载地址(提取码:33lg)
步骤1
使用windbg 打开dump,并设置好pdb、操作系统pdb;
使用命令 “~*kv” 显示所有线程当前的函数调用栈,效果如下(大部分省略)
1 | 0:021> ~*kv |
步骤2
观察所有线程的函数调用。 这个服务并不复杂,总共只有二十几个线程(以前分析的都是至少100个线程的dump),然后重点查看服务进程的主要工作线程(是因为工作线程不工作才导致消息堆积)。
这个服务的工作线程有4个分别是10、11、12、13, 至于怎么确定的?
1、看日志。良好的编码习惯,程序中的每个线程启动、退出都会在日志中记录
2、对代码逻辑的熟悉
步骤3
切换到其中任意一个工作线程,比如10号线程~10s
kv
1 | 0:010> ~10s |
从中,我们看到调用栈并没有什么问题,大概意思就是收到了一个GameUserArenaResult消息,然后通过mq publish出去,然后mq模块正在进行connect。感觉没有问题,接着看11号线程
~11s
kv
1 | 0:010> ~11s |
这个堆栈就有意思了,大概就是收到了OnGameUserArenaResult消息,然后AddMsg,进入一个AutoLock,然后RtlEnterCriticalSection,看到这个说明很可能有死锁问题。我们继续观察这个临界区的具体信息
1 | 03ccf558 01216e48 007124f8 0070b3d8 030c7658 ntdll!RtlEnterCriticalSection+0x150 (FPO: [Non-Fpo]) |
注意其中的第三列、第四列、第五列, 分别代表了这个函数的第一个入参、第二个入参、第三个入参, 之前看过临界区的结构体,所以使用以下命令
1 | !cs 007124f8 |
1 | 0:011> !cs 007124f8 |
这个临界区正在被 0x00001188 的线程占用,所以导致11s线程无法进入临界区,那么我们就去 0x00001188 线程在做什么。
通过线程id显示线程号命令~~[0x00001188]
1 | 0:011> ~~[0x00001188] |
4号线程,然后我们切换~4s
kv
1 | 0:004> kv |
4号线程MQ正在publish一个消息,而这个消息中有一个临界区,这个临界区可能又正在被其他线程占用。 继续查看临界区
1 | 0:004> !cs 00720b68 |
这个临界区正在被0x00001590 线程占用,再看看这个线程号是多少~~[0x00001590]
1 | 0:004> ~~[0x00001590] |
???? 怎么又回到10号线程了, 再仔细看看10s线程
之前我们看的时候觉的没问题,但这里肯定是有问题的,会不会’connect’ 阻塞了呢? 带着疑问查看了下消息堆积时段的日志,有MQ断开的日志。带着猜测自己重现现场试试看,在本地启动服务,然后重启mq服务,发现问题必现。
解决方法就是使用非阻塞式的connect函数,问题解决。
总结
这里介绍了几个windbg的常用命令,讲解了死锁的一般分析方法,对于这类问题更多的是观察、思考。希望能带给你帮助,如果觉的有用,请帮忙打赏下吧!后续还有一些有意思的dump来分享,比如内存泄漏、死锁+崩溃、踩内存、高cpu等。现在的dump素材没有以前的多了,不然我可以把很多案例都写出来,有些问题没有案例还是挺难讲的。