天天看點

記憶體洩露,記憶體溢出和死鎖

一記憶體洩露

   記憶體洩漏指由于疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況。記憶體洩漏并非指記憶體在實體上的消失,而是應用程式配置設定某段記憶體後,由于設計錯誤,失去了對該段記憶體的控制,因而造成了記憶體的浪費。記憶體洩漏與許多其他問題有着相似的症狀,并且通常情況下隻能由那些可以獲得程式源代碼的程式員才可以分析出來。然而,有不少人習慣于把任何不需要的記憶體使用的增加描述為記憶體洩漏,即使嚴格意義上來說這是不準确的。

  一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中配置設定的,大小任意的(記憶體塊的大小可以在程式運作期決定),使用完後必須顯示釋放的記憶體。應用程式一般使用malloc,realloc,new等函數從堆中配置設定到一塊記憶體,使用完後,程式必須負責相應的調用free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。

  記憶體洩漏可以分為4類:

  1. 常發性記憶體洩漏。發生記憶體洩漏的代碼會被多次執行到,每次被執行的時候都會導緻一塊記憶體洩漏。

  2. 偶發性記憶體洩漏。發生記憶體洩漏的代碼隻有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。是以測試環境和測試方法對檢測記憶體洩漏至關重要。

  3. 一次性記憶體洩漏。發生記憶體洩漏的代碼隻會被執行一次,或者由于算法上的缺陷,導緻總會有一塊僅且一塊記憶體發生洩漏。比如,在一個Singleton類的構造函數中配置設定記憶體,在析構函數中卻沒有釋放該記憶體。而Singleton類隻存在一個執行個體,是以記憶體洩漏隻會發生一次。

  4. 隐式記憶體洩漏。程式在運作過程中不停的配置設定記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡并沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對于一個伺服器程式,需要運作幾天,幾周甚至幾個月,不及時釋放記憶體也可能導緻最終耗盡系統的所有記憶體。是以,我們稱這類記憶體洩漏為隐式記憶體洩漏。                       

   記憶體洩漏會因為減少可用記憶體的數量進而降低計算機的性能。最終,在最糟糕的情況下,過多的可用記憶體被配置設定掉導緻全部或部分裝置停止正常工作,或者應用程式崩潰。

  記憶體洩漏可能不嚴重,甚至能夠被正常的手段檢測出來。在現代作業系統中,一個應用程式使用的正常記憶體在程式終止時被釋放。這表示一個短暫運作的應用程式中的記憶體洩漏不會導緻嚴重後果。

  在以下情況,記憶體洩漏導緻較嚴重的後果:

  * 程式運作後置之不理,并且随着時間的流失消耗越來越多的記憶體(比如伺服器上的背景任務,尤其是嵌入式系統中的背景任務,這些任務可能被運作後很多年内都置之不理);

  * 新的記憶體被頻繁地配置設定,比如當顯示電腦遊戲或動畫視訊畫面時;

  * 程式能夠請求未被釋放的記憶體(比如共享記憶體),甚至是在程式終止的時候;

  * 洩漏在作業系統内部發生;

  * 洩漏在系統關鍵驅動中發生;

  * 記憶體非常有限,比如在嵌入式系統或便攜裝置中;

  * 當運作于一個終止時記憶體并不自動釋放的作業系統(比如AmigaOS)之上,而且一旦丢失隻能通過重新開機來恢複。

2記憶體溢出

   記憶體溢出已經是軟體開發曆史上存在了近40年的“老大難”問題,象在“紅色代碼”病毒事件中表現的那樣,它已經成為黑客攻擊企業網絡的“罪魁禍首”。 如在一個域中輸入的資料超過了它的要求就會引發資料溢出問題,多餘的資料就可以作為指令在計算機上運作。據有關安全小組稱,作業系統中超過50%的安全漏洞都是由記憶體溢出引起的,其中大多數與微軟的技術有關。

  為了便于了解,我們不妨打個比方。緩沖區溢出好比是将十磅的糖放進一個隻能裝五磅的容器裡。一旦該容器放滿了,餘下的部分就溢出在櫃台和地闆上,弄得一團糟。由于計算機程式的編寫者寫了一些編碼,但是這些編碼沒有對目的區域或緩沖區——五磅的容器——做适當的檢查,看它們是否夠大,能否完全裝入新的内容——十磅的糖,結果可能造成緩沖區溢出的産生。如果打算被放進新地方的資料不适合,溢得到處都是,該資料也會制造很多麻煩。但是,如果緩沖區僅僅溢出,這隻是一個問題。到此時為止,它還沒有破壞性。當糖溢出時,櫃台被蓋住。可以把糖擦掉或用吸塵器吸走,還櫃台本來面貌。與之相對的是,當緩沖區溢出時,過剩的資訊覆寫的是計算機記憶體中以前的内容。除非這些被覆寫的内容被儲存或能夠恢複,否則就會永遠丢失。

  在丢失的資訊裡有能夠被程式調用的子程式的清單資訊,直到緩沖區溢出發生。另外,給那些子程式的資訊——參數——也丢失了。這意味着程式不能得到足夠的資訊從子程式傳回,以完成它的任務。就像一個人步行穿過沙漠。如果他依賴于他的足迹走回頭路,當沙暴來襲抹去了這些痕迹時,他将迷失在沙漠中。這個問題比程式僅僅迷失方向嚴重多了。入侵者用精心編寫的入侵代碼(一種惡意程式)使緩沖區溢出,然後告訴程式依據預設的方法處理緩沖區,并且執行。此時的程式已經完全被入侵者操縱了。

  入侵者經常改編現有的應用程式運作不同的程式。例如,一個入侵者能啟動一個新的程式,發送秘密檔案(支票本記錄,密碼檔案,或财産清單)給入侵者的電子郵件。這就好像不僅僅是沙暴吹了腳印,而且後來者也會踩出新的腳印,将我們的迷路者領向不同的地方,他自己一無所知的地方。

  緩沖區溢出的處理

  你屋子裡的門和窗戶越少,入侵者進入的方式就越少……

  由于緩沖區溢出是一個程式設計問題,是以隻能通過修複被破壞的程式的代碼而解決問題。如果你沒有源代碼,從上面“堆棧溢出攻擊”的原理可以看出,要防止此類攻擊,我們可以:

  1、開放程式時仔細檢查溢出情況,不允許資料溢出緩沖區。由于程式設計和程式設計語言的原因,這非常困難,而且不适合大量已經在使用的程式;

  2、使用檢查堆棧溢出的編譯器或者在程式中加入某些記号,以便程式運作時确認禁止黑客有意造成的溢出。問題是無法針對已有程式,對新程式來講,需要修改編譯器;

  3、經常檢查你的作業系統和應用程式提供商的站點,一旦發現他們提供的更新檔程式,就馬上下載下傳并且應用在系統上,這是最好的方法。但是系統管理者總要比攻擊者慢一步,如果這個有問題的軟體是可選的,甚至是臨時的,把它從你的系統中删除。舉另外一個例子,你屋子裡的門和窗戶越少,入侵者進入的方式就越少。

3死鎖

因為系統資源不足。

程序運作推進的順序不合适。

資源配置設定不當等。

如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因争奪有限的資源而陷入死鎖。其次,程序運作推進順序與速度不同,也可能産生死鎖。

産生死鎖的四個必要條件

互斥條件:一個資源每次隻能被一個程序使用。

請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。

不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。

循環等待條件:若幹程序之間形成一種頭尾相接的循環等待資源關系。

這四個條件是死鎖的必要條件,隻要系統發生死鎖,這些條件必然成立,而隻要上述條件之一不滿足,就不會發生死鎖。

對于線程同步問題,有了進一步的了解:詳見我寫的關于多線程同步的文章(已作修改):http://blog.csdn.net/yjgx007/archive/2004/09/04/94559.aspx,主線程A等待另一個線程B的完成才能繼續,線上程B中又要更新主線程A的界面,這裡涉及了同步問題以及由此可能産生的死鎖問題,同步問題在修改後的文章中講得比較清楚了,對于線程之間可能産生死鎖的淺析如下:

在等待線程B中更新主線程A的界面,如果未能正确處理A,B兩線程同步的問題,極有可能導緻兩線程間的死鎖,看下面代碼:

 UINT CMsiTestDlg::UpdateDeviceContent(LPVOID pParam)

{

 CMsiTestDlg* pDlg = (CMsiTestDlg*)pParam;

 int i = 0;

 do {

  pDlg->m_progress.SetPos(i); // 更新線程A中的進度條

  Sleep(500);

 } while(i++<10);

 return 0;

}

void CMsiTestDlg::OnButton1()

{

 MSG msg;

 CWinThread* m_pUpdateThread = AfxBeginThread(UpdateDeviceContent, (LPVOID)this);

 if (m_pUpdateThread)

 {

while (::WaitForSingleObject(m_pUpdateThread->m_hThread, INFINITE) != WAIT_OBJECT_0) //開始等待線程B至結束(線程結束時将傳回WAIT_OBJECT_0)

   PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE); //擷取目前線程消息(可能是A線程也可能是B線程)并将消息從消息隊列中移除

   DispatchMessage(&msg); // 重新分發消息

...

 }

 MessageBox("Thread is end!");

}

m_pUpdateThread->m_hThread是等待線程B,這樣調用似乎沒有什麼問題,在VC++中跟蹤到線程B的線程函數UpdateDeviceContent中的pDlg->m_progress.SetPos(i); 時,發現程式不能繼續執行,表現為線上程B中對主線程A的界面更新發生了阻塞(這裡暫不考慮界面線程,權當由主線程處理),為何?原因就在于WaitForSingleObject(m_pUpdateThread->m_hThread, INFINITE)最後一個參數INFINITE - 無限期等待線程B的結束傳回,進而産生了不幸:

線程A對線程B無限期等待造成未能重新分發消息(包括界面重繪WM_PAINT, 定時WM_TIMER以及硬體輸入和系統消息,就裡特指WM_PAINT消息),造成線程B的阻塞,線程B的阻塞又造成線程A的進一步等待造成線程A的阻塞,這就導緻了死鎖。

解決方法是:設定WaitForSingleObject的等待時間為一定值,如500毫秒,這樣,線程A如若等不到線程B的結束,也會傳回,并分發消息,使得線程B的執行得以正常繼續,進而也就保證了線程A和線程B之間的正常同步!

繼續閱讀