1. 程式人生 > >.NET物件與Windows控制代碼:控制代碼的基本概念

.NET物件與Windows控制代碼:控制代碼的基本概念

在.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物件時,應該首先檢查哪裡出現了控制代碼洩露,而不是更改上限數量;另一方面,更改上限並不意味著應用程式就真的可以建立和使用這麼多物件控制代碼,實際可用的數量同時受制於當前系統可用記憶體。

作者:文禾

本文內容為原創,版權歸作者所有,轉載請保留原作者署名和出處。