使用Windbg分析多线程临界区死锁问题分享

news2024/10/5 7:21:42

目录

1、多线程死锁场景及多线程锁的类型

1.1、发生死锁的场景说明

1.2、锁的类型

2、问题实例说明

3、使用Windbg初步分析

4、进一步分析死锁

4.1、使用!locks命令查看临界区对象信息

4.2、通过占用临界区锁的线程id找到目标线程

4.3、如何将!locks命令打印出来的临界区对象与发生死锁的线程对应起来

4.4、查看代码,排查死锁的原因

4.5、解决办法

5、最后    


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/125529931        为了实现多线程之间能安全地访问一些共享资源(比如内存资源),我们会给共享资源加锁,以保证某个时刻不会出现一个线程在写资源、另一个线程在读资源的冲突情况。但加锁后,如果控制的不好,则可能会出现多线程之间的死锁问题。死锁问题排查起来则比较费时费力,今天我们就通过一个多线程死锁的实例来介绍如何使用Windbg来排查多线程死锁问题。

1、多线程死锁场景及多线程锁的类型

     死锁一般发生在多个线程之间,一般会涉及到两个或两个以上的锁。下面我们先大概地讲述一些发生死锁的场景及用于线程间同步的锁的类型。

1.1、发生死锁的场景说明

        比如当前有两个线程,线程1和线程2;当前有两个锁,锁1和锁2。假设线程1占用了锁1,正在申请锁2,同时线程2占用了锁2,正在申请锁1,两线程都占用了各自的锁,都在申请对方占用的锁,各不相让,如下所示:

这样就导致了死锁,这是个典型的死锁场景。

        还有一个比较典型的场景是,线程1和线程2之间发生了死锁,导致了线程3的死锁。假设线程2占用了线程3要申请的锁3,因为线程1与线程2之间产生了死锁,导致线程2一直在占用锁3,一直没有释放。而线程3的代码进入了要申请锁3的代码中,因为线程2一直在占用锁3不释放,这样也导致了线程3的死锁,如下所示:

本文中的问题实例就属于这个死锁场景。

1.2、锁的类型

        此处我们以Windows平台的多线程锁为例来展开。在Windows平台中可以用多个对象来实现多线程间的锁,比如临界区对象事件对象互斥量对象信号量对象等。

        这些对象主要分用户态对象和内核态对象,其中临界区属于用户态对象,事件、互斥量和信号量则属于内核态对象。使用用户态对象的好处是,不用在用户态与内核态之间切换,在效率上相对高一些,所以在Windows平台上用户态的临界区用的比较多一些。使用内核态对象时,大部分程序代码都运行在用户态的,当操作到这些内核态对象时在底层就需要切换到内核态中,完成对应的操作后再返回到用户态代码中。如果代码在用户态和内核态之间频繁的切换,则执行效率上会有损伤。

        用户态的临界区锁,只能用于一个进程中的多个线程间的同步。而事件、互斥量和信号量都属于内核态的对象,除了可以用于一个进程中的多个线程的同步,还可以跨进程使用。

        使用Windbg去排查用户态的临界区死锁,则相对容易一些,Windbg默认是在用户态中的。如果要排查内核态锁引发的死锁,则要复杂一些,Windbg需要切入到内核态中去分析。

        本案例中主要讲述用户态的临界区锁引发的死锁,对于内核态的死锁实例,我们后面会写一篇专门的文章去介绍。

2、问题实例说明

        平台兄弟项目组维护的某在线视频播放器在测试同事的Win11 PC上运行过程中出现了卡死现象,UI界面没法操作了,如下所示:

这个问题虽然不是必现,但复现的概率挺大,开发人员在自己的PC机倒腾了几下就复现了。

        看现象,估计是播放器UI界面无法操作了,估计大概率是UI主线程卡死了,于是他们到Windows系统的任务管理器中找到出问题的播放器进程,然后将包含进程上下文完整信息导出到dump转储文件中,然后用Windbg去打开这个dump文件去分析。

        但维护该项目的同事都是搞服务器侧开发的,他们对Linux系统上的C++开发比较熟练,对Windows系统上的调试技术不太了解,于是邀请我过去帮他们分析一下。

其实帮兄弟项目组分析问题也挺好,可以了解到相关的代码及编程思想,能够见识更多的异常排查的素材,积累更多的经验!既帮助了被人,也积累了自己!

3、使用Windbg初步分析

        由于目标程序的UI主线程卡住了,所以用Windbg打开dump文件后,先去查看UI主线程(0号线程)的函数调用堆栈。

在Windows GUI界面程序中,UI线程就是程序的主线程,是进程的0号线程。

于是使用~0s命令切换到UI线程,然后输入kn命令查看UI主线程的函数调用堆栈,如下所示:

看到UI主线程中确实卡住了,调用了RtlEnterCriticalSection要获取临界区锁,但一直没获取到,一直卡在那里了。大概率是发生死锁了!应该是要获取的临界区锁,被其他线程占用了,始终没释放,所以UI线程始终获取不到。

        本例中发生死锁的是临界区锁,属于用户态的锁,相对于内核态的锁,排查起来要方便很多。
对于~0s命令,是切换到0号线程中,命令中的数字0就是目标线程的编号。可以输入~命令查看当前进程中的所有线程,如下所示:

可以看到线程的编号,还可以看到线程id。

4、进一步分析死锁

   既然是临界区死锁,我们可以使用!locks命令去查看当前进程中的所有临界区对象信息。

4.1、使用!locks命令查看临界区对象信息

        可以先到Windbg的帮助文档中查看一下!locks命令的说明,如下所示:

该命令会将当前进程中的所有临界区对象信息打印出来,这其中肯定会包含发生死锁的临界区对象。
        但输入!locks命令后却提示:

根据提示信息,!locks命令没法解析Windows系统库ntdll中的RtlCriticalSectionList接口,需要去检查一下系统库ntdll.dll库的pdb文件。估计!locks命令内部用到了ntdll库中的RtlCriticalSectionList接口。
        ntdll.dll是系统库,如何去获取系统库的pdb文件呢?其实,很简单,我们只要设置包含微软在线的pdb符号库下载地址:

http://msdl.microsoft.com/download/symbols

设置好后,Windbg会自动到该服务器上下载对应版本的pdb文件。一般我们在Windbg中设置的pdb路径如下:

C:\Users\Administrator\Desktop\pdbdir;srv*f:\mss0616*http://msdl.microsoft.com/download/symbols

关于上述pdb路径的说明,可以参见这篇文章,这里就不再赘述了:
pdb符号库文件详解icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/125508858设置上述pdb路径后,Windbg就会自动去下载其需要的Windows系统库的pdb文件,再次执行!locks命令,就可以看到临界区对象列表了,如下所示

列表中只看到了一个临界区对象,那这个对象肯定是上述函数调用堆栈中要操作的临界区对象。从打印出来的临界区的详细信息中可以看到两点关键信息:

1)临界区对象数据结构的地址0x010ecb7c;
2)拥有临界区所有权(占用临界区)线程id为ac8。

       在Windbg中设置微软在线的pdb符号库下载地址后,Windbg会自动去下载Windows系统库的pdb文件,有了系统库的pdb符号库文件,我们就能在函数调用堆栈中看到更详细的系统接口的调用。有时有没有系统库的pdb符号库文件,看到的函数调用堆栈会有一定的差异,能看到对系统库内部接口的调用,对问题的排查也有很大的指引左右。

4.2、通过占用临界区锁的线程id找到目标线程

        从上面的分析知晓,UI主线程请求的临界区锁,就是!locks命令打印出来的那个临界区对象:

从临界区对象信息得知,该临界区对象正被id为ac8的线程占用。为啥该临界区对象被id为ac8的线程占用没释放呢?这就需要去看看id为ac8线程的函数调用堆栈了!

        那如何才能切换到id为ac8的线程中呢?很简单,使用~命令将进程中的所有线程打印出来,看看id为ac8线程是几号线程。打印出来的线程列表如下所示:

根据线程id为ac8线程在线程列表中对应的线程号为9,即9号线程,于是使用~9s命令切换到该目标线程中,然后使用kn命令将该线程的函数调用堆栈打印出来,看看该线程是如何占用目标临界区锁,以及为啥没有释放锁的。

       9号线程的函数调用堆栈如下所示:

从函数调用堆栈可以看出,9号线程也死锁了,卡在了WaitForSingleObject接口上没有返回,可以多次go多次查看函数调用堆栈,都卡在WaitForSingleObject函数接口上没返回,那基本可以确定9号线程也死锁了。同时9号线程占用了UI主线程要获取的锁,因为9号线程死锁了,一直在占用UI线程的锁,导致UI线程获取不到锁。即9号线程死锁了,导致UI主线程也死锁了。

4.3、如何将!locks命令打印出来的临界区对象与发生死锁的线程对应起来

        如果!locks命令打印出来的临界区对象中有多个,那我们怎么知道当前的UI主线程操作的是哪个临界区对象呢?每个临界区对象后面会显示该临界区对象对应的结构体对象地址,使用kv命令重新打印UI主线程的函数调用堆栈:

使用该命令可以将调用函数时的前三个参数值打印出来,在函数调用堆栈中找到操作临界区的系统函数ntdll!RtlEnterCriticalSection,该接口中应该有个参数传入的是临界区结构体对象地址。

        ntdll!RtlEnterCriticalSection是Windows系统库ntdll.dll的内部函数,不是系统对外公开的API函数,我们如何知道RtlEnterCriticalSection中的哪个参数是临界区结构体对象地址呢?这里有个很重要的技巧,我们可以到ReactOS开源操作系统的代码中查看ntdll!RtlEnterCriticalSection接口的参数。ReactOS开源操作系统系统库中函数名和参数基本和Windows的系统库是一致,所以可以到ReactOS中去查看ntdll!RtlEnterCriticalSection接口都哪些参数,如下:

该函数只有一个参数,即RTL_CRITICAL_SECTION结构体对象的首地址,RTL_CRITICAL_SECTION结构体的定义如下:

 我们可以拷贝RTL_CRITICAL_SECTION字串到VS中,然后Go到该结构体的定义处:

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    //  The following three fields control entering and exiting the critical
    //  section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

我们可以对比一下ReactOS开源代码中该结构体的内容与VS下的RTL_CRITICAL_SECTION结构体,两者是完全一样的。

        从函数调用堆栈中看:

调用ntdll!RtlEnterCriticalSection接口的第一个参数值为010ecb7c,即RTL_CRITICAL_SECTION结构体对象的首地址,与!locks命令打印出来的临界区列表中的每个临界区的地址比较一下:

就能确定当前UI线程的要操作的临界区对象是哪个了。然后根据目标临界区对象的OwningThread值,确定目标临界区对象被哪个线程占用了,然后去查看这个线程的函数调用堆栈,这样就找到了继续分析下去的线索了。

         此外,查看开源代码时,推荐大家使用Source Insight工具,该工具小巧且运行速度快,使用起来非常方便。关于如何使用Source Insight,可以参见我之前写的文章:

使用Source Insight查看编辑源代码 icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/124347857

4.4、查看代码,排查死锁的原因

        9号线程发生死锁,占用了UI线程(0号主线程)要申请的临界区锁,于是到9号线程中去查看函数调用堆栈,去查看对应的源码:

9号线程中调用了CHttpsRequset::HttpsGetSMLevel函数,而在CHttpsRequset::HttpsGetSMLevel函数中使用了一个函数范围的锁CAutoWriteLock tWriteLock(m_rwLock),如下:

这个锁就是UI线程要申请的临界区对象锁。而CHttpsRequset::HttpsGetSMLevel函数中调用了CheckUkeyStatus函数,CheckUkeyStatus函数中调用了XXXSecAuthCheckUkeyState函数:

最终在XXXSecAuthCheckUkeyState函数内部发生了死锁,所以导致上层的CHttpsRequset::HttpsGetSMLevel函数始终没有执行完,没有返回,所以导致函数范围的锁CAutoWriteLock tWriteLock(m_rwLock)一直没有释放,导致UI线程一直获取不到锁,导致UI线程也死锁了,导致UI线程卡死了,导致播放器程序没法操作了。

4.5、解决办法

        确定引发死锁的原因后,如何修改代码去避免死锁呢?这就是兄弟项目组需要去解决的问题了。大家很多时候都喜欢使用函数锁(在函数中定义一个局部变量),进入函数时去申请锁,退出函数时该局部变量析构时去释放锁,即这个锁作用于整个函数。但使用锁的一个重要原则是,尽量锁的范围越小越好,所以有时函数锁并不是一个很好的选择。

5、最后    

        很多时候去封装支持跨平台的线程锁类时,在Windows下使用CriticalSection临界区锁,在Linux下使用linux系统中的系统函数去创建锁。在Windows系统中,临界区是用户态对象,而信号量、互斥量等是内核态对象,之所以在Windows下优先选用临界区实现,是因为临界区是用户态对象,在使用时不要像信号量等内核态对象那样要在用户态和内核态之间来会切换,避免了用户态与内核态之间切换的开销。

        此外,Windows曾经开源过部分源码,但后来又取消了开源,ReactOS克隆了WindowsNT内核和系统层。我们可以到ReactOS中查看系统库内部函数的参数以及函数内部的实现,可以查看regsvr32控件注册程序的内部实现,可以查看WindowsSEH结构化异常的处理机制等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/136432.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

# 分布式理论协议与算法 第二弹 ACID原则

ACID 原则是在 1970年 被 Jim Gray 定义,用以表示事务操作:一个事务是指对数据库状态进行改变的一系列操作变成一个单个序列逻辑元操作,数据库一般在启动时会提供事务机制,包括事务启动 停止 取消或回滚。 但是上述事务机制并不真…

(1分钟)速通ikdtree

Ikdtree算法来自fast-lio2 其中i是incremental的缩写,表示增量的kdtree。 ​ Ikdtree算法来自fast-lio2 其中i是incremental的缩写,表示增量的kdtree。 ​ Ikdtree算法来自fast-lio2 其中i是incremental的缩写,表示增量的kdtree。 ​ 编辑…

性能成本难两全?OpenMLDB 实时计算双存储引擎让你不必纠结(附测评报告)

内存和磁盘的双存储引擎架构 1.1 使用场景描述 OpenMLDB 的线上服务部分为了满足不同的性能和成本需求,提供了两种分别基于内存和磁盘的存储引擎。关于这两种存储引擎的使用考量,和推荐匹配场景,见如下表。 [外链图片转存失败,源站可能有防…

使用ES Term query查询时一定要注意的地方

文章目录一、Term query简介二、Term query和Match query对比1. 数据准备2. 使用Match query查询3. 使用Term query查询三、分词器修改字段值的场景1. 删除大多数标点符号2. 英文大写转小写3. 超过max_token_length部分单独拆分一、Term query简介 使用Term query可以根据精确…

【计算机模型机设计】8指令多周期(硬布线)MIPS CPU设计报告

2023年第一篇文章来咯~ 8指令多周期(硬布线)MIPS CPU设计报告一、设计概述(基本类似于上一篇)1.1设计目的1.2设计任务1.3设计要求1.4技术指标二、总体方案设计2.1主要功能部件2.2数据通路设计三、详细设计与实现3.1主要功能部件的…

Cookie 和 Session、实现用户登录逻辑

Cookie 和 Session一、回忆 Cookie二、理解会话机制 (Session)三、Cookie 和 Session 的区别四、核心方法4.1 HttpServletRequest 类中的相关方法4.2 HttpServletResponse 类中的相关方法4.3 HttpSession 类中的相关方法4.4 Cookie 类中的相关方法五、代码示例: 实现用户登陆5.…

转速传感器信号隔离变送模块PCB焊接方波信号输出

特点 转速传感器信号直接输入,方波信号输出正弦波、锯齿波信号输入,方波信号输出200mV峰值微弱信号的放大与整形不改变原波形频率,响应速度快电源、信号:输入/输出 3000VDC三隔离辅助电源:5V、12V、15V或24V直流单电源…

【学Vue就像玩一样】什么是计算属性?什么使监视属性?

目录 1.计算属性 2.监视属性 2.1监视属性 2.2深度监视 3.computed和watch的区别 本栏会细致的将Vue划分为两大模块,基础篇,进阶篇。想要顺利通关vue篇首先要拥有三剑客,当然node ajax法宝也会辅佐你通关的,在学习的途中如果遇…

【回答问题】ChatGPT上线了!给我推荐20个比较流行的自动驾驶算法模型

给我推荐20个比较流行的自动驾驶算法模型 当前比较流行的自动驾驶模型有: 卷积神经网络(CNN) 长短时记忆网络(LSTM) 自适应调和滤波器(ADF) 自适应估计滤波器(AEF) 线性…

Vue组件及组件之间的通信

目录 一、Vue脚手架的使用 1、通过命令行使用vue-cli的指令创建:vue init webpack 项目名称 2、使用webStorm软件:本质仍然使用vue脚手架 3、使用vue ui创建:vue提供的图形化的操作界面 二、Vue的组件和组件之间的通信 1、组件&#xff…

使用线性回归、LGBM对二手车价格进行预测

使用线性回归、LGBM对二手车价格进行预测 目录使用线性回归、LGBM对二手车价格进行预测说明数据导入、查看和清洗数据说明导入训练集导入测试集合并数据查看数据整体情况处理数据检查并处理缺失变量EDA年份和价格地区和价格前任里程和价格燃料类型和价格传动装置类型Mileage与价…

数据库 SQL 高级用法

目录 一、INSERT INTO SELECT 用法 二、逻辑控制语句 三、公式表表达式 四、存储程序 五、触发器 一、INSERT INTO SELECT 用法 INSERT INTO SELECT 语句从一个表复制数据,然后把数据插入到一个已存在的表中。 1、从一个表中复制所有的列插入到另一个已存在的表…

React(coderwhy)- 01

React的介绍(技术角度) React是什么?React:用于构建用户界面的 JavaScript 库React的官网文档:https://zh-hans.reactjs.org/React的特点: 声明式编程组件化开发多平台适配Hello React react需要3个依赖&am…

【数字图像处理】骨骼锐化

源码链接:skeleton.cpp 一、实验要求 附件是人体骨骼核扫描图像,我们的目的是通过图像锐化突出骨骼的更多细节来增强图像。图像灰度的动态范围很窄并且有很高的噪声内容。 二、实验内容 按照课本冈萨雷斯的《数字图像处理》上面的思路,整…

【代码题】五道链表面试题

目录 1.移除链表元素 2.反转链表 3.链表的中间结点 4.链表中倒数第k个结点 5.合并两个有序链表 1.移除链表元素 点击进入该题 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val val 的节点,并返回新的头节点 。 思路&am…

CTF-AWD入门手册

引文 AWD赛制是一种网络安全竞赛的赛制。AWD赛制由安全竞赛专家及行业专家凭借十多年实战经验,将真实网络安全防护设备设施加入抽象的网络环境中,模拟政府、企业、院校等单位的典型网络结构和配置,开展的一种人人对抗的竞赛方式,…

语言和文法的形式定义---编译原理

文法的构建问题 * 参考已有的模型 最经典的即是算数表达式的模型,其有多个算术运算符号和优先级别。 文法与正则表达式与有穷自动机的转换 显然是3型文法,也就是正则文法才有相应的性质,因为只有3型文法才是右部至多仅有两个符 号&#xf…

【Vue路由】props配置、replace属性、编程式路由导航、缓存路由组件

文章目录props配置props值为对象props值为布尔值props值为函数总结\<router-link>的replace属性总结编程式路由导航案例实现总结缓存路由组件案例实现总结props配置 我们可以看看我们原来如何使用传递过来的参数的&#xff1a; 我们要写一大长串去从$route身上拿到我们…

传统目标跟踪——光流法

目录 一、光流法 二、LK光流法 2.1 实现原理 2.2 API 三、代码 四、总结 一、光流法 光流&#xff1a;空间运动物体在观察成像平面上像素运动的瞬时速度。 光流法利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧之间存在的对应关系&#xff0c;…

密码学_RSA

RSA是1977年由罗纳德李维斯特&#xff08;Ron Rivest&#xff09;、阿迪萨莫尔&#xff08;Adi Shamir&#xff09;和伦纳德阿德曼&#xff08;Leonard Adleman&#xff09;一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。 RSA是非…