在.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對象本身是托管對象,它是如何與文件這個非托管資源產生聯系的呢?
大致說來,C#中打開文件的操作會經過下列步驟:
三、通過句柄操作對象的好處
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對象時,應該首先檢查哪裡出現了句柄洩露,而不是更改上限數量;另一方面,更改上限並不意味著應用程序就真的可以創建和使用這麼多對象句柄,實際可用的數量同時受制於當前系統可用內存。