內核對象是個比較難理解的概念,問題的根源就在於即使是《核心編程》書中也沒有說清楚它的定義,只是不停地舉例和描述它的性質,還有如何使用。 盲人摸象,難見全貌。只能儘可能列舉它的性質,註意使用了。 引用計數(書中的說法是使用計數)就是內核對象的一個很關鍵的性質。由於內核對象的擁有者是內核而不是進程,所以 ...
內核對象是個比較難理解的概念,問題的根源就在於即使是《核心編程》書中也沒有說清楚它的定義,只是不停地舉例和描述它的性質,還有如何使用。
盲人摸象,難見全貌。只能儘可能列舉它的性質,註意使用了。
引用計數(書中的說法是使用計數)就是內核對象的一個很關鍵的性質。由於內核對象的擁有者是內核而不是進程,所以只能由內核來做撤銷內核對象的操作。而通常一個內核對象不一定只被一個進程使用的,創建或者撤銷內核對象,就要看引用計數了。引用計數在內核對象被創建的時候被置為1,被進程訪問一次引用計數就遞增1。當引用計數降為0,內核就撤銷這個內核對象。(至於何時引用計數遞減1,書中沒有明確說明,不過有編程經驗的你肯定知道是什麼時候了)
安全性也是內核對象的一個重要的性質,它用於描述內核對象的訪問者。由於創建內核對象的時候幾乎都會有一個參數,指向Security_Attributes結構體的指針,例如CreateFileMapping(定義不詳)。如果這個指針是NULL值,那就是預設的安全性,只有管理對象的小組成員和創建者可以訪問;不過,只要你初始化並操作lpSecurityDescriptor這個成員,就可以設置它的安全性了。這樣,內核對象被訪問的時候(例如OpenFileMapping),在返回一個有效的句柄之前會執行一次安全檢查,如果不通過檢查返回的就不是一個有效句柄,而是NULL了,它的LastError是5(ERROR_ACCESS_DENIED)。
進程的內核對象句柄表,在進程被創建的時候被分配。當線程共有內核對象產生的時候,內核會在句柄表中找出一個空項,把內核對象的記憶體塊指針寫進去。
如果一個線程中調用函數返回一個句柄,這個句柄可以也只可以被線程中的所有線程使用。這些句柄的值實際上是句柄在當前進程句柄表中的索引,但不是固定的,如在Win2k中返回的句柄是用於標識句柄表的該對象的位元組數。如果給句柄表中傳入一個無效值,GetLastError則會返回6(ERROR_INVALID_HANDLE)。
如果調用函數創建內核對象失敗了,那麼句柄的值通常是0(NULL),也有些函數返回的是INVALID_HANDLE_VALUE。但是無論用什麼方式創建內核對象,都要通過CloseHandle來結束對對象的操作。這個函數會先檢查句柄表,確定傳入的索引是否有效,並查看引用計數,如果是0則撤銷這個對象。
一個無效的句柄傳給CloseHandle的話,GetLastError會返回ERROR_INVALID_HANDLE。如果進程正在被調試,則通知調試器,以確定這個錯誤。
加入忘記調用CloseHandle,有可能會產生資源泄漏。因為進程終止的時候,系統會掃描它的句柄表中的無效項目,然後關閉這些對象句柄。這時句柄的引用計數就有機會降為0而被撤銷了。
當進程間有父子關係的時候,父進程才有機會使用一個或多個內核對象句柄,並且父進程還可以產生一個可以訪問這個內核對象的子進程。步驟如下:
- 父進程創建內核對象時,必須指明這個句柄可以被繼承。(不是內核對象本身可以被繼承)
- 指定一個SECURITY_ATTRIBUTES結構並對它進行初始化,然後把結構的地址傳給Create函數。
- sa.bInheritHandle = TRUE;
每個句柄表項中都有一個標誌位,用以指明這個句柄是否有繼承性。如果是bInheritHandle屬性是TRUE,標誌位被置1,否則置0。
此後只要調用CreateProcess中的參數bInheritHandle是TRUE,那麼被創建的進程就在創建空的句柄表的同時,遍歷父進程的句柄表找到有繼承屬性的項目,並拷貝到新的句柄表中完全相同的位置、遞增引用計數。這樣即使父進程關閉了這個句柄,由於引用計數還沒到0,也要等子進程終止的時候才能置零。這樣子進程只要知道句柄的值就可以使用了。
改變句柄的標誌,可以使用SetHandleInformation,第一個參數是句柄,第二個參數就確定修改哪些標誌了。和每個句柄相關的就是HANDLE_FLAG_INHERIT和HANDLE_FLAG_PROJECT_FROM_CLOSE了。第三個參數dwFlags,可以用於指明內核對象的繼承標誌:
- 打開繼承標誌:SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
- 關閉繼承標誌:SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);
使用SetHandleInformation(hObj, HANDLE_FLAG_PROJECT_FROM_CLOSE, HANDLE_FLAG_PROJECT_FROM_CLOSE),會告訴系統這個句柄不應該被關閉,如果關閉則會產生一個異常。只有一種特殊的情況,就是子進程又產生了子進程,而有繼承屬性的句柄也有可能會被傳遞到新的子進程。但是舊的子進程有可能在新的子進程生成之前關閉這個句柄,那麼父進程就不能和新的子進程通信了,因為沒有繼承這個內核對象。這種情況下,告訴系統句柄不應該關閉才有意義。
跨進程共用內核對象的第二種方法是給對象命名。
- 進程A創建了一個名字為“JeffMutex”的互斥內核對象。
- 進程B也創建一個名字為“JeffMutex”的互斥內核對象,系統會有以下操作:
- 查看是否已經有同名的內核對象
- 如果同名,就檢查同名內核對象的類型。
- 如果類型也相同,系統會查看調用者的訪問許可權。
- 如果有就找個空項目,初始化再指向現有的內核對象
- 沒有許可權則返回NULL。
- 如果類型不匹配,返回NULL。
- 如果類型也相同,系統會查看調用者的訪問許可權。
- 不同名就創建新的內核對象。
- 如果同名,就檢查同名內核對象的類型。
- 沒有就創建新的內核對象。
- 查看是否已經有同名的內核對象
進程B調用函數成功後,不是返回一個內核對象,而是返回一個和進程相關的句柄值。
按名字共用的另一種方法是不使用Create*函數,而是Open*函數。原型相同。最後一個參數必須是0結尾的地址,不能傳遞NULL。如果不存在,GetLastError返回值是2(ERROR_FIEL_NOT_FOUND)。還得檢查訪問許可權,如果有就把引用計數遞增1。
為了保證對象的唯一性,建議創建GUID用來當作對象的名字。這種方法也多用於檢查你的應用程式有另一個進程正在運行。
跨進程共用內核對象的最後一個方法是使用DuplicateHandle函數。簡單地說,這個函數只是取出這個進程的句柄表中的一項,拷貝到另一個進程的句柄表中。
DuplicateHandle的參數雖然多,但不複雜。既然是複製句柄,自然少不了源進程和源句柄,以及目標進程和目標句柄了,設計句柄,就得有它的屏蔽值與繼承性,這裡給了三個。前四個參數不難理解,只是後面的參數要註意:
- dwOption參數可以是0,也可以是DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE。
- 如果設定了DUPLICATE_SAME_ACCESS,則目標進程的句柄擁有相同的訪問屏蔽,並讓函數忽略dwDesiredAccess參數。
- 如果設定DUPLICATE_CLOSE_SOURCE,則可以關閉源進程中的句柄。使用該標誌時,內核對象的引用計數不會受到影響。
最後書中的例子提到,使用DuplicateHandle函數的時候,dwDesiredAccess參數應該設置為只讀(FILE_MAP_READ),這樣就可以不影響源進程的句柄,提高健壯性。
DUPLICATE_CLOSE_SOURCE