Windows跨進程共享內核對象

對於Windows內核對象,如互斥鎖,信號量,線程對象,文件映射對象等,有時候需要多個進程共享這些對象以協同完成任務,此時就需要跨進程來共享內核對象,對於內核對象判定最簡單方法就是在創建過程中有一個 LPSECURITY_ATTRIBUTES 參數。共享內核對象有三種方法:

 

一、繼承對象句柄

對於每個進程,系統會分配一個句柄表,句柄表中的每個句柄有個標誌位來代表該句柄是否可以繼承。當該進程創建子進程時會遍歷這個表,如果可以繼承,則將這個句柄複製到子進程的句柄表中,由於句柄是內核對象,所以是獨立於進程而存在的,對於句柄有一個引用計數,當Create或繼承的時候會將引用計數加1,當CloseHandle時會將引用計數減一,當減完後引用計數爲0則刪除該內核對象。所以當父進程創建子進程,並指定了某個句柄可繼承,當子進程創建完成後,就算父進程調用CloseHandle那個句柄,也不會刪除,因爲子進程創建完成後,引用計數又加一了。

以CreateEvent爲例:HANDLE WINAPI  CreateEventA(
    _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
    _In_ BOOL bManualReset,
    _In_ BOOL bInitialState,
    _In_opt_ LPCSTR lpName
    );
所有內核對象創建時都需要指定參數1,如果不需要繼承則直接設爲NULL即可,如果需要繼承,則需要定義一個SECURITY_ATTRIBUTES結構體,並將bInheritHandle成員設爲TRUE。

SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(sa);

HANDLE hEvent= CreateEvent(&sa, TRUE, TRUE,NULL);

除了在創建時指定是否可繼承,如果父進程生成了兩個子進程,而只希望一個進程可以繼承這個句柄,可以在創建完成後手動修改句柄的繼承狀態,通過SetHandleInformation函數來指定。

BOOL WINAPI SetHandleInformation(
    _In_ HANDLE hObject,//待指定的對象
    _In_ DWORD dwMask,//待修改的標誌位
    _In_ DWORD dwFlags//待修改的標誌位的值
    );
目前可修改的dwMask有兩個屬性值HANDLE_FLAG_INHERIT(是否可繼承), HANDLE_FLAG_PROTECT_FROM_CLOSE(是否保護不可被關閉)。HANDLE_FLAG_PROTECT_FROM_CLOSE被指定後,子進程便不可以通過CloseHandle來關閉這個句柄,調用CloseHandle會引發異常,一般不用指定,除非特殊需求。
假設指定hEvent對象可繼承,則可以SetHandleInformation(hEvent,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT); 若要取消讓它不可以繼承,可以通過SetHandleInformation(hEvent,HANDLE_FLAG_INHERIT,0);

既然有SetXXXInfo那對應的就有GetXXXInfo,所以獲取句柄狀態可以通過使用GetHandleInformation,(windows的api名字取得真是通俗易懂==)。

DWORD dwFlags = 0;
GetHandleInformation(hEvent, &dwFlags);
bool bok = (dwFlags&HANDLE_FLAG_INHERIT);
if(bok)
{
    cout<<"Handle can be inherit"<<endl;
}

二、通過命名對象創建

對於創建內核對象時很多時候都有個參數用於指定名字,例如上面的CreateEvent的參數四爲lpName。這個名字用來代表這個內核對象,如果多個內核對象創建時取的同一個名字相當於就是同一個對象了,調用GetLastError會返回ERROR_ALREADY_EXISTS。使用一個簡單的例子來說明問題,比如使用Event來控制單生產者,單消費者,可能會寫出下面的代碼(爲了簡單說明問題,代碼中緩衝區其實沒有共享)

代碼是運行在同一個進程下的。假設現在用兩個進程來協同完成這項工作,則可以使用命名對象來實現。看下面兩個進程的實現方法

代碼中輸出了句柄的標識符,可以看到是相同的。兩個進程共享了eventEmpty和eventFull對象,左邊生產者會等待右邊消費者來給eventEmpty對象設置信號,右邊消費者會等待生產者設置eventFull對象的信號,不管先啓動哪個進程,這項任務都可以很好的完成。
另外一個很多博客都說的應用,就是來控制一個程序只允許執行一次也可使用這個命名對象來實現,可以在應用程序入口處創建一個內核對象,如果調用GetLastError後返回ERROR_ALREADY_EXISTS,則關閉應用程序即可。
另外,除了使用CreateXXX,也可以使用OpenXXX,參數都是相同的,區別就是OpenXXX如果該命名對象不存在則返回NULL,而CreateXXX如果該命名對象不存在則新建一個。

三、通過複製對象句柄

複製對象句柄是通過DuplicateHandle來實現,它的函數原型如下

BOOL DuplicateHandle(
   HANDLE hSourceProcessHandle,
   HANDLE hSourceHandle,
   HANDLE hTargetProcessHandle,
   PHANDLE phTargetHandle,
   DWORD dwDesiredAccess,
   BOOL bInheritHandle,
   DWORD dwOptions);

簡單說來,該函數取出一個進程的句柄表中的項目,並將該項目拷貝到另一個進程的句柄表中。

第一和第三個參數hSourceProcessHandle和hTargetProcessHandle是內核對象句柄。這些句柄本身必須與調用DuplicateHandle 函數的進程相關。此外,這兩個參數必須標識進程的內核對象。
第二個參數hSourceHandle是任何類型的內核對象的句柄。但是該句柄值與調用DuplicateHandle的進程並無關係。相反,該句柄必須與hSourceProcessHandle句柄標識的進程相關。
第三個參數hTargetProcessHandle時拷貝給某個進程的進程對象
第四個參數phTagetHandle是HANDLE 變量的地址,它將接收穫取源進程句柄信息拷貝的項目索引。返回的句柄值與hTargetProcessHandle標識的進程相關。
最後3 個參數用於指明該目標進程的內核對象句柄表項目中使用的訪問屏蔽值和繼承性標誌。dwOptions參數可以是0 (零),也可以是下面兩個標誌的任何組合:DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE。

如果設定了DUPLICATE_SAME_ACCESS標誌,則告訴DuplicateHandle 函數,你希望目標進程的句柄擁有與源進程句柄相同的訪問屏蔽。使用該標誌將使DuplicateHandle忽略它的dwDesiredAccess參數。

如果設定了DUPLICATE_CLOSE_SOURCE標誌,則可以關閉源進程中的句柄。該標誌使得一個進程能夠很容易地將內核對象傳遞給另一個進程。當使用該標誌時,內核對象的使用計數不會受到影響。

所以DuplicateHandle一般可以設計兩個進程或者三個進程,兩個進程的情況比如一個進程擁有對另一個進程想要訪問的對象的訪問權,或者一個進程想要將內核對象的訪問權賦予另一個進程。三個進程的情況就是進程C的句柄表中有S和T的進程內核對象的句柄,然後可以把S中的某個內核對象複製給T。但時T並不會知道它什麼時候收到了這個複製的內核對象,所以需要自己手動通過Windows消息機制或其他某種方法來告訴它。
用DumplicateHandle改編上面的例子:

梳理下用到的API:
SetConsleTitle:設置控制檯的標題,用以保證在FindWindow時找到獨一無二的窗口
FindWindow:根據窗口標題名獲取窗口句柄
GetWindowThreadProcessId:根據窗口句柄獲取進程ID
上圖必須要先啓動右邊的進程,然後左邊的進程才能獲取到右邊的進程ID,並複製句柄到右邊的進程的句柄列表中。然後右邊的進程再輸入句柄的值,從而協調完成任務,效果與上面創建命名對象相同。

注意:在左邊的進程中由於target_empty與target_full是用來傳遞給右邊進程的,所以在左邊進程中不應該調用CloseHandle(target_full);否則將會使該句柄計數減一,關閉後右邊的行爲將不可預知。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章