前言
本文介紹本人的一次使用Windbg分析dump文件找出死鎖的過程,并重點(diǎn)介紹如何確定線程所等待的鎖及判斷是否出現(xiàn)了死鎖。
對于如何安裝及設(shè)置Windbg請參考:《使用Windbg和SoS擴(kuò)展調(diào)試分析.NET程序》http://www.rzrgm.cn/shanyou/archive/2006/12/23/601004.html
起因
今天,部署到生產(chǎn)環(huán)境中的軟件再次發(fā)生了不響應(yīng)請求的問題,看了系統(tǒng)日志與軟件本身的log都沒發(fā)現(xiàn)異常,而在任務(wù)管理器中軟件占用了1G多的內(nèi)存,有點(diǎn)偏高(正常是300M左右)。由于本人不在現(xiàn)場,只能通過遠(yuǎn)程的方式查看,同時(shí)故障出現(xiàn)間隔比較長(將近一周),在生產(chǎn)環(huán)境中也就無法使用VS進(jìn)行調(diào)試。
無意中在資源監(jiān)視器的CPU頁看到軟件的線程數(shù)是1.7萬個(gè),內(nèi)存頁的提交內(nèi)存使用也將近18G,同時(shí)線程數(shù)與提交內(nèi)存也在緩慢增加。當(dāng)時(shí)就想是不是由于某種原因?qū)е戮€程無法退出從而在線程數(shù)太多的時(shí)候致使軟件不響應(yīng)請求(后來的調(diào)試也證實(shí)是死鎖導(dǎo)致的)。
由于故障難以重現(xiàn)(只在生產(chǎn)環(huán)境中長時(shí)間運(yùn)行才會(huì)出現(xiàn),在測試環(huán)境無法出現(xiàn)),只能對正在運(yùn)行的軟件進(jìn)行分析。
這時(shí)候就請出了大名鼎鼎的Windbg,下面是詳細(xì)的過程。
過程
一、抓取dump文件
抓取dump的方法,可以參考《抓取user mode dump文件的幾重境界》http://www.rzrgm.cn/pugang/archive/2013/02/18/2916211.html
我選擇的是使用圖形化操作的方式,在任務(wù)管理器的進(jìn)程頁中,右鍵需要抓取的程序,選擇“創(chuàng)建轉(zhuǎn)儲文件”
運(yùn)行完成后將會(huì)彈出成功對話框并提示dump文件的所在

二、在Windbg中加載dump文件與SOS.dll
運(yùn)行Windbg,在File菜單下選擇Open Crash Dump,選擇上面抓取的dump文件
在Windbg下側(cè)的命令輸入框中輸入“.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos”并回車加載SOS.dll。由于我是調(diào)試net4.0 64位的軟件,所以使用了Framework64\v4.0.30319下的sos,其它版本請選擇對應(yīng)位置的sos進(jìn)行加載
三、使用Windbg查找死鎖
使用“!threads”命令列出所有的線程,發(fā)現(xiàn)一共存在17306個(gè)線程
使用“~17306s”命令切換到最后一個(gè)線程,并使用“!clrstack”命令輸出當(dāng)前線程的調(diào)用堆棧,發(fā)現(xiàn)存在“System.Threading.Monitor.Enter(System.Object)”,表明線程正在請求一個(gè)鎖。由于得不到鎖,因此線程卡死
切換到其它線程查看調(diào)用堆棧,都是因?yàn)橥瑯拥脑驅(qū)е戮€程卡死,這時(shí)候可以初步判斷這些線程是因?yàn)樗梨i導(dǎo)致執(zhí)行不下去
使用“!syncblk”命令列出所有正在使用的鎖,其中MonitorHeld與Recursion列表示了請求鎖的線程數(shù)量情況,Info列表示哪個(gè)線程擁有了鎖,SyncBlock列表示鎖對象的地址。如MonitorHeld與Recursion的值為3775與1那行表示第40個(gè)線程擁有了這個(gè)鎖,其它(3775-1)/2=1887個(gè)線程在等待鎖,鎖對象地址為0000000003c812f0。看到如此多的線程在請求同一個(gè)鎖,就知道情況不正常,看來離死鎖的真相又近了一步
接下來的過程就是:找到某個(gè)線程(如線程A)請求的鎖(如鎖J),查看哪個(gè)線程(如線程B)擁有這個(gè)鎖(鎖J)及這個(gè)線程請求的鎖(鎖K),接著查看哪個(gè)線程(如線程C)擁有這個(gè)鎖(鎖K)及這個(gè)線程請求的鎖(鎖L),重復(fù)查看的過程,看最終是否有一個(gè)線程(如線程D)請求前面出現(xiàn)的任意一個(gè)鎖(如線程B擁有的鎖J),形成環(huán)狀,這時(shí)即可判斷其為死鎖

這里從線程17306開始分析,使用“!clrstack -l”命令列出當(dāng)前線程的調(diào)用堆棧及其使用的局部變量
在調(diào)用“System.Threading.Monitor.Enter(System.Object)”之前的一個(gè)方法內(nèi),應(yīng)該存在作為局部變量的線程請求的鎖對象
這里猜測下面的0000000003c812f0就是這個(gè)鎖對象,通過查找上面的鎖列表,確定了這個(gè)猜測,同時(shí)知道線程40擁有這個(gè)鎖
使用“~40s”命令切換到線程40,并使用“!clrstack -l”命令列出當(dāng)前線程的調(diào)用堆棧及其使用的局部變量,通過查找鎖列表確定000000000317ac10為當(dāng)前線程請求的鎖對象,同時(shí)知道線程26擁有這個(gè)鎖
同樣使用“~26s”與“!clrstack -l”命令找到線程26請求的鎖對象00000000044a81a8,這個(gè)鎖對象被線程43擁有
接著使用“~43s”與“!clrstack -l”命令找到線程43請求的鎖對象000000000317ac10,這個(gè)鎖對象被線程26擁有
此時(shí)可以發(fā)現(xiàn)線程26與線程43之間形成了死鎖

結(jié)果
終于真相大白了,上面的過程成功找到了死鎖
也由此推斷由于死鎖的存在,導(dǎo)致后面新建的線程由于得不到請求的鎖,一直不能執(zhí)行下去,更不可能釋放所占用的內(nèi)存,從而使得線程數(shù)與內(nèi)存占用在一直升高,直到軟件無法響應(yīng)請求為止
接下來的工作就是查看死鎖線程的調(diào)用堆棧,結(jié)合軟件源代碼分析死鎖形成時(shí)軟件的運(yùn)行情況,并更改處理邏輯以避免死鎖的產(chǎn)生
浙公網(wǎng)安備 33010602011771號