程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET對象與Windows句柄(一):句柄的基本概念

.NET對象與Windows句柄(一):句柄的基本概念

編輯:關於.NET

在.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#中打開文件的操作會經過下列步驟:

  1. 調用.NET靜態方法System.IO.File.Open時,File類會創建一個FileStream對象並傳入必要的參數,如文件路徑,FileMode和FileAccess選項。FileMode枚舉表明是希望創建新文件,打開已有文件,覆蓋原有文件或是在原文件上追加新內容;FileAccess枚舉表明是希望讀文件、寫文件或兩者都有。
  2. 接著FileStream調用自己的Init方法進行初始化,在這個過程中,有更多細節需要考慮。為了創建一個文件,初始化方法需要更多額外的信息和檢查,比如本進程在使用文件時是否允許其它進程讀寫文件,文件路徑是否有效,是否有足夠的權限,目標文件是否是允許被訪問的文件類型,是否正確設置了FileMode和FileAccess選項的組合等。
  3. 完成這些必要的檢查後,FileStream.Init調用Win32Native.SafeCreateFile方法。
  4. Win32Native類封閉了大量的Windows API,SafeCreateFile方法以P/Invoke的方式調用kernel32.dll中的CreateFile API,並返回SafeFileHandle。SafeFileHandle是一個有趣的類型,繼承自SafeHandle,包含了真正的IntPtr類型的文件句柄。.NET的設計者有意讓這個句柄字段對外不可見,但如果你非要拿到這個句柄值,SafeFileHandle也提供了DangerousGetHandle()方法滿足你的要求:都告訴你Dangerous了,你自己看著辦。
  5. 包含著文件句柄的SafeFileHandle會被返回並存放在FileStream對象中。隨後的讀取和寫入操作,FileStream都會使用這個句柄與Windows API進行交互,直到最終關閉句柄。至始至終,我們的代碼都無需直接關心句柄的存在,FileStream負責了絕大部分工作。

三、通過句柄操作對象的好處

Windows不允許應用程序直接訪問內存中更底層的對象,而是由對象管理器統一管理,總的來說,至少有以下好處:

  1. 在操作系統層面上,為所有程序使用系統資源提供了統一的接口和機制。如果沒有對象管理器,不同程序會有各種各樣的實現方式來訪問資源,並且這些代碼散落在各種,難以規范,也無從協調解決資源的爭用。
  2. 將需要在系統級別保護的對象隔離起來,提供更高安全性。
  3. 所有對系統關鍵資源的訪問都經由對象管理器,使得系統可以方便的追蹤和限制資源的使用,進行權限控制。

四、查看進程的句柄數量

到現在為止,本文討論的全是看不見的概念,有必要來直觀的看一下系統中的句柄使用情況。有多種方式可以查看進程的句柄使用情況,先從兩個工具開始,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對象時,應該首先檢查哪裡出現了句柄洩露,而不是更改上限數量;另一方面,更改上限並不意味著應用程序就真的可以創建和使用這麼多對象句柄,實際可用的數量同時受制於當前系統可用內存。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved