最近項目中調出一個bug,某些時候程序會卡死不動,用windbg進行加載後用 ~*kb 命令列出所有的線程棧調用,發現有多個線程調用 WaitForMultipleObjects 在等待同一個內核對象:
輸入 !handle cc f 命令列出該內核對象的詳細信息:
發現是是一個Mutex對象,對象名是 Mutex_DebugMsg2 ,查找代碼知道這個Mutex是用寫log時鎖定文件寫入用的。代碼如下:
BOOL CDebugMsg::WriteLogA(LPSTR pszLog)
{
#ifdef _WRITEDEBUGMSGLOG
BOOL bRet = FALSE;
if (WaitForSingleObject(m_hMutex, INFINITE) != WAIT_OBJECT_0)
{
return bRet;
}
FILE* fp = _tfopen(m_ptszLogPath, _T("a+"));
if (fp)
{
TCHAR ptszTime[16] = {0};
char pszWriteMsg[MAXDEBUGMSGCHARNUM] = {0};
GetTimeString(ptszTime);
USES_CONVERSION;
_snprintf(pszWriteMsg, MAXDEBUGMSGCHARNUM, "%s %s\n", T2A(ptszTime), pszLog);
fputs(pszWriteMsg, fp);
fclose(fp);
bRet = TRUE;
}
ReleaseMutex(m_hMutex);
return bRet;
#endif
return FALSE;
}
2、Windbg查找Mutex所有者爲了知道那個線程佔用了Mutex沒有釋放,參考此篇文章說明(http://blog.csdn.net/gufeng99/article/details/46714711)
同時,爲了簡化問題原因查找,寫了小Demo起3個線程調用log記錄代碼,觸發上述bug,後開啓windbg進入內核調試模式。
2.1 查看測試Demo進程
2.2 查看進程的所有線程及等待狀態信息
發現有3個線程都在等待同一個Mutex,線程所有者是0xfffffa8004d1b590
2.3 查看Mutex所有者線程信息,看是那個線程在佔用
找到該線程所在進程的PID = 0xC30,也就是本進程,線程ID = 0xD74
2.4 停止內核附加,切換Windbg改用用戶態調試附加Demo進程,列出所有線程信息及調用棧
發現線程擁有者就是主線程,難道是主線程調用後未釋放成功麼?爲了驗證這個情況,於是在ReleaseMutex(m_hMutex)加上返回跟蹤輸出,結果每次釋放都是成功的。再看看代碼,可能的解釋不多了,想得到的可能就是WaitForSingleObject返回了,但返回值不是WAIT_OBJECT_0,導致沒有調用ReleaseMutex進行釋放。於是再跟蹤了下WaitForSingleObject的返回值,發現第一次調用時返回值是128。
去翻了下《windows核心編程》一書,原來Mutex和其他內核對象不一樣,當擁有Mutex的線程未進行釋放時被終止,這時Mutex處於被廢棄狀態,其他線程WaitForSingleObject可立即獲得Mutex的所有權,但返回值會是WAIT_ABANDONED(128),而並不是WAIT_OBJECT_0,這是Mutex非常特殊的一點。
到了這裏,原因就非常明瞭了,由於log輸出在項目中使用非常頻繁,而寫log時又需要重複調用fopen()和fclose打開關閉文件,這對IO的操作是比較費時間的,導致某些線程在此處由於等待超時被終止(由於其他Mutex是可跨進程的,所以其他進程中的線程意外終止或進程未正常退出都會有此問題),進而導致Mutex未被釋放處於廢棄狀態。而程序收到WAIT_ABANDONED後未是Mutex進行釋放,導致Mutex死鎖。這裏再次深刻的警示,線程是不能隨意終止的,同時頻繁打開關閉文件操作也是不合理的,正確的做法應該是打開一次後保存文件句柄,再程序退出時再關閉。
另由於這個bug不是必現(由於線程不是每次都會由於等待超時被終止),在調試過程中我保存了下dump,後來調試下dump的時候顯示handle信息是發現一個非常有意思的事情:
dump中在調用 !handle cc f 命令後,同時也將Mutex的擁有者線程也顯示出來了,而在即時調試的時候卻沒有該信息,需要繞一圈到內核中去查找。