在.NET程式設計中,得益于有效的記憶體管理機制,對象的建立和使用比較友善,大多數情況下我們無須關心對象建立和配置設定記憶體的細節,也可以放心的把對象的清理交給自動垃圾回收來完成。由于.NET類庫對系統底層對象進行了封裝,我們也不需要調用Windows API來操作非托管對象。但不直接操作非托管對象,并不意味着程式不會間接建立這些對象,如果不了解.NET對象與非托管資源的關系,我們很有可能因為不恰當的使用這些托管對象,而導緻非托管資源洩露。本文嘗試說明Windows對象和句柄的基本概念,以及.NET程式設計中的對象與它們的關系,并結合一些簡單的示例程式來探讨句柄洩露的話題。
一、什麼是句柄?
Windows程式設計中,程式需要通路各種各樣的資源,如檔案、網絡、視窗、圖示和線程等。不同類型的資源被系統封裝成不同的資料結構,當需要使用這些資源時,程式需要依據這些資料結建構立出不同的對象,當操作完畢并不再需要這些對象時,程式應當及時釋放它們。在Windows中,應用程式不能直接在記憶體中操作這些對象,而是通過一系列公開的Windows API由對象管理器(Object Manager)來建立、通路、跟蹤和銷毀這些對象。當調用這些API建立對象時,它們并不直接傳回指向對象的指針,而是會傳回一個32位或64位的整數值,這個在程序或系統範圍内唯一的整數值就是句柄(Handle)。随後程式再次通路對象,或者删除對象,都将句柄作為Windows API的參數來間接對這些對象進行操作。在這個過程中,句柄作為系統中對象的辨別來使用。
對象管理器是系統提供的用來統一管理所有Windows内部對象的系統元件。這裡所說的内部對象,不同于進階程式設計語言如C#中“對象”的概念,而是由Windows核心或各個元件實作和使用的對象。這些對象及其結構,要麼不對使用者代碼公開,要麼隻能使用句柄由封裝好的Windows API進行操作。C#程式設計中,多數情況下,我們并不需要與這些Windows API打交道,這是因為.NET類庫對這些API又進行了封裝,但我們的托管程式仍然會間接建立出很多Windows内部對象,并持有它們的句柄。
如上所說,句柄是一個32位或64位的整數值(取決于作業系統),是以在32位系統中,C#完全可以用int來表示一個句柄。但.NET提供了一個結構體System.IntPtr專門用來代表句柄或指針,在需要表示句柄,或者要在unsafe代碼中使用指針時,應當使用IntPtr類型。
二、C#中建立檔案句柄的過程
舉例來說,檔案屬于一種非托管的系統資源。在C#中,可以用File類的靜态方法Open來得到一個FileStream對象,來對磁盤檔案進行讀寫操作。FileStream對象本身是托管對象,它是如何與檔案這個非托管資源産生聯系的呢?

- 調用.NET靜态方法System.IO.File.Open時,File類會建立一個FileStream對象并傳入必要的參數,如檔案路徑,FileMode和FileAccess選項。FileMode枚舉表明是希望建立新檔案,打開已有檔案,覆寫原有檔案或是在原檔案上追加新内容;FileAccess枚舉表明是希望讀檔案、寫檔案或兩者都有。
- 接着FileStream調用自己的Init方法進行初始化,在這個過程中,有更多細節需要考慮。為了建立一個檔案,初始化方法需要更多額外的資訊和檢查,比如本程序在使用檔案時是否允許其它程序讀寫檔案,檔案路徑是否有效,是否有足夠的權限,目标檔案是否是允許被通路的檔案類型,是否正确設定了FileMode和FileAccess選項的組合等。
- 完成這些必要的檢查後,FileStream.Init調用Win32Native.SafeCreateFile方法。
- Win32Native類封閉了大量的Windows API,SafeCreateFile方法以P/Invoke的方式調用kernel32.dll中的CreateFile API,并傳回SafeFileHandle。SafeFileHandle是一個有趣的類型,繼承自SafeHandle,包含了真正的IntPtr類型的檔案句柄。.NET的設計者有意讓這個句柄字段對外不可見,但如果你非要拿到這個句柄值,SafeFileHandle也提供了DangerousGetHandle()方法滿足你的要求:都告訴你Dangerous了,你自己看着辦。
- 包含着檔案句柄的SafeFileHandle會被傳回并存放在FileStream對象中。随後的讀取和寫入操作,FileStream都會使用這個句柄與Windows API進行互動,直到最終關閉句柄。至始至終,我們的代碼都無需直接關心句柄的存在,FileStream負責了絕大部分工作。
三、通過句柄操作對象的好處
Windows不允許應用程式直接通路記憶體中更底層的對象,而是由對象管理器統一管理,總的來說,至少有以下好處:
- 在作業系統層面上,為所有程式使用系統資源提供了統一的接口和機制。如果沒有對象管理器,不同程式會有各種各樣的實作方式來通路資源,并且這些代碼散落在各種,難以規範,也無從協調解決資源的争用。
- 将需要在系統級别保護的對象隔離起來,提供更高安全性。
- 所有對系統關鍵資源的通路都經由對象管理器,使得系統可以友善的追蹤和限制資源的使用,進行權限控制。
四、檢視程序的句柄數量
到現在為止,本文讨論的全是看不見的概念,有必要來直覺的看一下系統中的句柄使用情況。有多種方式可以檢視程序的句柄使用情況,先從兩個工具開始,Windows任務管理器和Process Explorer。
任務管理器預設不顯示句柄數,需要在“檢視”-“選擇列”中勾選“句柄數”後,才會顯示程序中目前打開的句柄數量。如下圖所示,可以看到記事本程序目前打開59個句柄。
系統自帶的任務管理器檢視句柄數量很友善,但如果想知道這些句柄具體是什麼,可以使用Process Explorer。Process Explorer是Windows Sysinternals工具包中的一個程序檢視器,可以從這裡下載下傳。如果你看到的視圖跟下圖不同,可以點選View,選中Show Lower Pane,并在Lower Pane View中選擇Handles。在清單中選擇程序後,下方面闆中會顯示該程序中句柄的詳細清單。
五、為什麼關注句柄數
句柄指向的是諸如視窗、線程、檔案、菜單、程序和定時器之類的系統資源,和所有被稱為“資源”的事物一樣,稀缺性是它們共同的特點。對于計算機和作業系統來講,記憶體是一種稀缺資源,而所有的句柄和對象都存儲在記憶體中。基于這個事實,作業系統不允許程序無限制的建立對象和句柄。對于任務管理器中的“句柄數”來講,每一程序允許打開的句柄數理論上來講可達2^24個,但由于記憶體的限制,實際數字大打折扣。在我的測試中,32位的.NET程序“句柄數”在達到1500萬以上後,程式開始出現各種各樣的問題。事實上絕大多數程式不會使用到這麼多句柄,除非特殊需要,在軟體程式設計中,如果自己的程式“句柄數”上千甚至是幾千時,就需要引起特别注意,這一般說明程式中已經存在句柄洩露的情況。
你可能已經留意到,本文前面任務管理器中,除了顯示程序的“句柄數”之外,還顯示了“使用者對象”和“GDI對象”的數量,它們屬于另外兩種句柄。具體的差別我們将在後面介紹,現在我們需要清楚的是,系統對于這兩種對象同樣設定了數量限制。對于“使用者對象”和“GDI對象”來說,每個程序允許建立的數量上限是在系統資料庫中設定的,分别是HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows中的USERProcessHandleQuota項和GDIProcessHandleQuota項,在Windows 7的32位作業系統上,兩個項都被預設設定為10000。你可以更改這個設定,使用者對象最多隻能設定為18000個,GDI對象最多為65536個。但是改變這個設定是不被推薦的,一般情況下當你的應用程式需要用到超過10000個使用者對象或GDI對象時,應該首先檢查哪裡出現了句柄洩露,而不是更改上限數量;另一方面,更改上限并不意味着應用程式就真的可以建立和使用這麼多對象句柄,實際可用的數量同時受制于目前系統可用記憶體。
引用:http://www.cnblogs.com/silverb/p/5300255.html