前言
本文介紹本人的一次使用Windbg分析dump文件找出死鎖的過程,並重點介紹如何確定線程所等待的鎖及判斷是否出現了死鎖。
對於如何安裝及設置Windbg請參考:《使用Windbg和SoS擴展調試分析.NET程序》http://www.cnblogs.com/shanyou/archive/2006/12/23/601004.html
起因
今天,部署到生產環境中的軟件再次發生了不響應請求的問題,看了系統日志與軟件本身的log都沒發現異常,而在任務管理器中軟件占用了1G多的內存,有點偏高(正常是300M左右)。由於本人不在現場,只能通過遠程的方式查看,同時故障出現間隔比較長(將近一周),在生產環境中也就無法使用VS進行調試。
無意中在資源監視器的CPU頁看到軟件的線程數是1.7萬個,內存頁的提交內存使用也將近18G,同時線程數與提交內存也在緩慢增加。當時就想是不是由於某種原因導致線程無法退出從而在線程數太多的時候致使軟件不響應請求(后來的調試也證實是死鎖導致的)。
由於故障難以重現(只在生產環境中長時間運行才會出現,在測試環境無法出現),只能對正在運行的軟件進行分析。
這時候就請出了大名鼎鼎的Windbg,下面是詳細的過程。
過程
一、抓取dump文件
抓取dump的方法,可以參考《抓取user mode dump文件的幾重境界》http://www.cnblogs.com/pugang/archive/2013/02/18/2916211.html
我選擇的是使用圖形化操作的方式,在任務管理器的進程頁中,右鍵需要抓取的程序,選擇“創建轉儲文件”
運行完成后將會彈出成功對話框並提示dump文件的所在
二、在Windbg中加載dump文件與SOS.dll
運行Windbg,在File菜單下選擇Open Crash Dump,選擇上面抓取的dump文件
在Windbg下側的命令輸入框中輸入“.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos”並回車加載SOS.dll。由於我是調試net4.0 64位的軟件,所以使用了Framework64\v4.0.30319下的sos,其它版本請選擇對應位置的sos進行加載
三、使用Windbg查找死鎖
使用“!threads”命令列出所有的線程,發現一共存在17306個線程
使用“~17306s”命令切換到最后一個線程,並使用“!clrstack”命令輸出當前線程的調用堆棧,發現存在“System.Threading.Monitor.Enter(System.Object)”,表明線程正在請求一個鎖。由於得不到鎖,因此線程卡死
切換到其它線程查看調用堆棧,都是因為同樣的原因導致線程卡死,這時候可以初步判斷這些線程是因為死鎖導致執行不下去
使用“!syncblk”命令列出所有正在使用的鎖,其中MonitorHeld與Recursion列表示了請求鎖的線程數量情況,Info列表示哪個線程擁有了鎖,SyncBlock列表示鎖對象的地址。如MonitorHeld與Recursion的值為3775與1那行表示第40個線程擁有了這個鎖,其它(3775-1)/2=1887個線程在等待鎖,鎖對象地址為0000000003c812f0。看到如此多的線程在請求同一個鎖,就知道情況不正常,看來離死鎖的真相又近了一步
接下來的過程就是:找到某個線程(如線程A)請求的鎖(如鎖J),查看哪個線程(如線程B)擁有這個鎖(鎖J)及這個線程請求的鎖(鎖K),接着查看哪個線程(如線程C)擁有這個鎖(鎖K)及這個線程請求的鎖(鎖L),重復查看的過程,看最終是否有一個線程(如線程D)請求前面出現的任意一個鎖(如線程B擁有的鎖J),形成環狀,這時即可判斷其為死鎖
這里從線程17306開始分析,使用“!clrstack -l”命令列出當前線程的調用堆棧及其使用的局部變量
在調用“System.Threading.Monitor.Enter(System.Object)”之前的一個方法內,應該存在作為局部變量的線程請求的鎖對象
這里猜測下面的0000000003c812f0就是這個鎖對象,通過查找上面的鎖列表,確定了這個猜測,同時知道線程40擁有這個鎖
使用“~40s”命令切換到線程40,並使用“!clrstack -l”命令列出當前線程的調用堆棧及其使用的局部變量,通過查找鎖列表確定000000000317ac10為當前線程請求的鎖對象,同時知道線程26擁有這個鎖
同樣使用“~26s”與“!clrstack -l”命令找到線程26請求的鎖對象00000000044a81a8,這個鎖對象被線程43擁有
接着使用“~43s”與“!clrstack -l”命令找到線程43請求的鎖對象000000000317ac10,這個鎖對象被線程26擁有
此時可以發現線程26與線程43之間形成了死鎖
結果
終於真相大白了,上面的過程成功找到了死鎖
也由此推斷由於死鎖的存在,導致后面新建的線程由於得不到請求的鎖,一直不能執行下去,更不可能釋放所占用的內存,從而使得線程數與內存占用在一直升高,直到軟件無法響應請求為止
接下來的工作就是查看死鎖線程的調用堆棧,結合軟件源代碼分析死鎖形成時軟件的運行情況,並更改處理邏輯以避免死鎖的產生