自己所在的團隊在開發新版本過程中,一次測試環境發生了server死鎖,整個server的任務線程都被hang住。而死鎖的代碼就在我負責的程式日志部分中localtime_r函數調用處。
程式日記需要記錄列印日志的時間,而localtime_r函數就是用于将系統時間轉換為本地時間。同樣功能的函數還有localtime。兩個函數的差別是:localtime_r是thread-safe,其傳回的結果存在由使用者提供的buffer中;而localtime傳回的結果是指向static變量,多線程環境可被其他線程修改。localtime_r實作中有一把鎖,負責lock tzfile中的狀态變量,而server就在這裡發生死鎖。
經過分析死鎖是由于發kill信号,信号處理函數引起的。原線程列印程式日志獲得localtime_r中需要的鎖後,kill信号觸發中斷處理,正好配置設定給該線程進行中斷。信号處理函數中再次列印日志,調用localtime_r的鎖時發生死鎖。
之前的信号處理方式為異步方式,同時信号處理函數中做了很多事情。之前大家一直關注線程安全,卻從來沒有注意過異步信号處理函數的安全性。所在項目之前的信号處理函數實作一直是這個方案,但這次最新版本由于還在開發中,大家調用了大量日志列印,增加了死鎖的機率才将這個問題暴露出來。這也暴露了部分代碼場景思考不充分,測試不足。
信号處理函數不推薦做太多工作,如果調用函數需要是reentrant。reentrant可重新進入的,可以了解為一次調用發生後,不會對該函數的再次調用發生任何影響。即reentrant函數中不可以有static或global變量,不可以配置設定釋放記憶體,通常不可以使用修改使用者提供的對象,修改errno等等。
具體可以看
<a href="http://www.gnu.org/software/libc/manual/html_node/nonreentrancy.html#nonreentrancy">http://www.gnu.org/software/libc/manual/html_node/nonreentrancy.html#nonreentrancy</a>
自己的第一直覺是既然信号處理函數不可以做太多工作,需要調用non-reentrant函數,那就把日志列印全部去掉好了。但發現,所在項目的信号處理函數中會做大量工作,許多調試方法和調試資訊通過kill信号獲得,而且這些調用基本都是non-reentrant。是以隻能修改信号處理的方案。
信号處理的方式除了異步使用方式還有同步使用方式。同步信号處理方式即指定線程以同步的方式對從信号隊列中擷取信号進行處理。主要調用函數為sigwait,流程:
1、主線程設定信号掩碼,設定希望同步處理的信号;主線程的信号掩碼會被建立的線程繼承;
2、建立信号處理線程,信号處理線程循環調用sigwait(sigtimedwait)等待希望同步處理的信号并做信号處理;
3、建立其他線程。