DllMain中不當操作導致死鎖問題的分析--死鎖介紹

(轉載於breaksoftware的csdn博客)

最近在網上看到一些關於在DllMain中不當操作導致死鎖的問題,也沒找到比較確切的解答,這極大吸引了我研究這個問題的興趣。我花了一點時間研究了下,正好也趁機研究了下進程對DllMain的調用規律。因爲整個研究篇幅比較長,我覺得還是分開寫比較能突出重點。本文先說說死鎖。

        介紹死鎖之前,我說一個我小時候聽過的一個故事:

        某國際實驗機構將在全球各著名小學做個團隊合作的實驗。他們在中國選定了一個“被安排的”學校,然後“隨機”選出一些學生,讓這些學生作爲實驗的樣本參與中國區的實驗。實驗是這樣的:他們在N根細繩一頭捆着一支短粉筆,將這些粉筆放到一個細口瓶中。該瓶口只能容一支粉筆自由出入。然後繩的另一頭放在學生的手中,告知他們要迅速將自己手中繩子捆住的粉筆從瓶子中拽出來。中國學生經過討論後,決定出他們的方案。於是123之後,“聰明謙讓”的中國學生“一個個”並迅速的將各自的粉筆拽了出來。而同樣的實驗,在“苦大仇深”的外國學生中結果卻不理想。因爲他們同時一起往外拽繩子,導致所有的粉筆都卡在瓶口……

        這個故事影響了我很久,我一直在思考:外國人這麼笨麼?但是現在我回憶這個故事,卻想到了這個實驗中發生的一些現象和我們在編程中遇到的一些問題是如此的類似。想想,“中國學生”的思路就是“序列化執行”,而外國學生的現象就是因爲“競爭”而導致了“死鎖”。

        回到正題,我想熟悉計算機的同學應該對“死鎖”這個概念並不陌生。我們看一下wiki對Deadlock這個詞的解釋:

  1. A deadlock is a situation in which two or more competing actionsare each waiting for the other to finish, and thus neither ever does。  

        也就是說:多個操作相互等待其他結束從而導致它們都無法結束的一種場景。爲簡單描述,我以兩個相互影響因素來描述死鎖。


        上圖中紅色的部分就是故事中“所有粉筆卡在瓶口”那個糾結的時期。於是左右兩個例程都糾結於此,不再往下執行。

        以下我列出比較典型的死鎖案例

  1. // A線程中 hEventA未激活  
  2. WaitforSingleObject(hEventA, <strong>INFINITE</strong>);  
  3. SetEvent(hEventB);  
  1. // B線程中 hEventB未激活  
  2. WaitforSingleObject(hEventB, <strong>INFINITE</strong>);  
  3. SetEvent(hEventA);  
        線程A、B都在等待對方釋放一個事件再釋放對方要獲得的事件,否則它們都一直等下去。系統看不得它們不工作而傻等,於是就把它們都掛起了。它們就死鎖了。想想這和古惑仔片中兩個黑幫(線程A、B)相互扣押了對方的臥底(線程A扣押了hEventB,線程B扣押了hEventA),但是兩方的老大都要求對方先放人(A線程WaitforSingleObject(hEventA, INFINITE(無線等待)); B線程WaitforSingleObject(hEventB, INFINITE(無線等待));),於是他們就這樣僵持下去了(死鎖)。如果某方老大富有智慧,會先要求對方一段時間先放人以不失身份,但是等一會兒後(A線程WAIT_TIMEOUT == WaitforSingleObject(hEventA, 1000(小等一下));),自己先把對方的人先放了(SetEvent (hEventB);)。然後對方老大釋懷了(線程BWAIT_OBJECT_0==WaitforSingleObject(hEventB, INFINITE);),再釋放潛入的臥底(SetEvent (hEventA);),這樣的問題就解決了。可是編程中,出於種種原因,我們很難在一開始就發現是哪兒我們沒轉過彎。就像我題目中描述的問題,很多人無法理解爲什麼就在DllMain中加了點代碼就死鎖了,甚至代碼中不包括一點”等“性質的函數(其實是有,只是很隱蔽)。

        我們再看一個教科書式的死鎖案例

  1. // A線程  
  2. EnterCriticalSection(&g_csA);  //要進入臨界區g_csA  
  3. FunA1();                       //該函數不影響死鎖這個必然的結果,只是如果這個函數執行的消耗的時間很完美,將導致死鎖出現的概率大增  
  4. EnterCriticalSection(&g_csB);  //要進入臨界區g_csB  
  5. FunA2();  
  6. LeaveCriticalSection(&g_csB);  //要退出臨界區g_csB  
  7. LeaveCriticalSection(&g_csA);  //要退出臨界區g_csA  
  1. // B線程  
  2. EnterCriticalSection(&g_csB);  //要進入臨界區g_csB  
  3. FunB1();                       //該函數不影響死鎖這個必然的結果,只是如果這個函數執行的消耗的時間很完美,將導致死鎖出現的概率大增  
  4. EnterCriticalSection(&g_csA);  //要進入臨界區g_csA  
  5. FunB2();  
  6. LeaveCriticalSection(&g_csA);  //要退出臨界區g_csA  
  7. LeaveCriticalSection(&g_csB);  //要退出臨界區g_csB  
        如果A線程進入g_csA臨界區並運行到FunA1()時,B線程進入g_csB臨界區,並運行了FunB1()。這個時候,當A線程從FunA1()中退出後,試圖進入臨界區g_csB時是進入不了的,因爲此時B線程還在運行FunB1(),B還在g_csB臨界區中,於是A線程等待B退出臨界區g_csB。而B線程運行完FunB1()時,將試圖進入臨界區g_csA,它也進入不了,因爲線程A的操作在這個臨界區中。於是B就等待A線程退出g_csA。它們相互等待對方退出,而自己卻不去主動退出,這樣就是擠破了頭也沒法一起進行下去。最後說一下,此處的FunA1(),FunB1()並不影響死鎖產生的結果,但是會影響死鎖產生的概率。

        請大家記住這兩個例子,我們會在之後分析的DllMain中不當操作導致死鎖的案例中再次看到它們的身影。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章