?? 漫談兼容內核之十六:windows的進程間通信.txt
字號:
漫談兼容內核之十六:Windows的進程間通信
[i]毛德操[/i]
對于任何一個現代的操作系統,進程間通信都是其系統結構的一個重要組成部分。而說到Windows的進程(線程)間通信,那就要看是在什么意義上說了。因為正如“Windows的跨進程操作”那篇漫談中所述,在Windows上一個進程甚至可以“打開”另一個進程,并在對方的用戶空間分配內存、再把程序或數據拷貝過去,最后還可以在目標進程中創建一個線程、讓它為所欲為。顯然,這已經不只是進程間的“通信”,而是進程間“操縱”了。但是這畢竟屬于另類,我們在這里要談論的是“正規”的進程間通信。
不管是兩個甚么樣的實體,凡是要通信就得滿足一個必要條件,那就是存在雙方都可以訪問的介質。顧名思義,進程間通信就是在不同進程之間傳播或交換信息,那么不同進程之間存在著什么雙方都可以訪問的介質呢?進程的用戶空間是互相獨立的,一般而言是不能互相訪問的,唯一的例外是共享內存區。但是,系統空間卻是“公共場所”,所以內核顯然可以提供這樣的條件。除此以外,那就是雙方都可以訪問的外設了。在這個意義上,兩個進程當然也可以通過磁盤上的普通文件交換信息,或者通過“注冊表”或其它數據庫中的某些表項和記錄交換信息。廣義上這也是進程間通信的手段,但是一般都不把這算作“進程間通信”。因為那些通信手段的效率太低了,而人們對進程間通信的要求是要有一定的實時性。
但是,對于實際的應用而言,光有信息傳播的實時性往往還不夠。不妨以共享內存區(Section)為例來說明這個問題。共享內存區顯然可以用作進程間通信的手段,兩個進程把同一組物理內存頁面分別映射到自己的用戶空間,然后一個進程往里面寫,另一個進程就可以讀到所寫入的內容。從信息傳播的角度看,這個過程是“即時”的,有著很高的實時性,但是讀取者怎么知道寫入者已經寫入了一些數據呢?要是共享內存區的物理頁面能產生中斷請求就好了,可是它不能。讓讀取者輪詢、或者定時輪詢、那當然也可以,但是效率就降下來了。所以,這里還需要有通信雙方行為上的協調、或稱進程間的“同步”。注意所謂“同步”并不是說雙方應該同時讀或同時寫,而是讓雙方的行為得以有序、緊湊地進行。
綜上所述,一般所說的“進程間通信”其實是狹義的、帶限制條件的。總的來說,對于進程間通信有三方面的要求:
l 具有不同進程之間傳播或交換信息的手段
l 進程間傳播或交換信息的手段應具有一定程度的實時性
l 具有進程間的協調(同步)機制。
此外,“進程間通信”一般是指同一臺機器上的進程間通信。通過網絡或通信鏈路進行的跨主機的通信一般不歸入進程間通信的范疇,雖然這種通信通常也確實是發生于進程之間。不過網絡通信往往也可以作用于本機的不同進程之間,這里并沒有明確的界線。這樣一來范圍就廣了,所以本文在介紹Windows的進程間通信時以其內核是否專門為具體的機制提供了系統調用為準。這樣,例如用于網絡通信的Winsock機制是作為設備驅動實現的,內核并沒有為此提供專門的系統調用,所以本文就不把它算作進程間通信。
先看上面三方面要求的第一項,即同一機器上的不同進程之間傳播或交換信息的手段,這無非就是幾種可能:
l 通過用戶空間的共享內存區。
l 通過內核中的變量、數據結構、或緩沖區。
l 通過外設的存儲效應。但是一般所講操作系統內核的“進程間通信”機制都把這排除在外。
由于通過外設進行的進程間通信一般而言實時性不是很好,所以考慮到上述第二方面的要求就把它給排除掉了。
再看進程間的同步機制。如前所述,進程間同步的目的是要讓通信的雙方(或多方) 行為得以有序、緊湊地進行。所以本質上就是雙方(或多方)之間的等待(睡眠)/喚醒機制,這就是為什么要在上一篇漫談中先介紹等待/喚醒機制的原因。注意這里的“等待”意味著主動進入睡眠,一般而言,所謂“進程間同步”就是建立在(主動)睡眠/喚醒機制基礎上的同步。不主動進入睡眠的同步也是有的,例如“空轉鎖(Spinlock)”就是,但是那樣太浪費CPU資源了。再說,在單CPU的系統中,如果是在調度禁區中使用Spinlock,還會引起死鎖。所以,一般不把Spinlock算作進程間同步手段。在操作系統理論中,“信號量(Semaphore)”是基本的進程間同步機制,別的大都是在此基礎上派生出來的。
另一方面,進程間同步的實現本身就需要有進程間的信息傳遞作為基礎,例如“喚醒”這個動作就蘊含著信息的傳遞。所以,進程間同步其實也是進程間通信,只不過是信息量比較小、或者很小的進程間通信。換言之,帶有進程間同步的進程間通信,實際上就是先以少量信息的傳遞使雙方的行為得到協調,再在此基礎上交換比較大量的信息。如果需要傳遞的信息量本來就很小,那么這里的第二步也就不需要了。所以,進程間同步就是(特殊的)進程間通信。
注意這里所說的進程間通信實際上是線程間通信,特別是分屬于不同進程的線程之間的通信。因為在Windows中線程才是運行的實體,而進程不是。但是上述的原理同樣適用于同一進程內部的線程間通信。屬于同一進程的線程共享同一個用戶空間,所以整個用戶空間都成了共享內存區。如果兩個線程都訪問同一個變量或數據結構,那么實際上就構成了線程間通信(或許是在不知不覺間)。這里仍有雙方如何同步的問題,但是既然是共享用戶空間,就有可能在用戶空間構筑這樣的同步機制。所以,一般而言,進程間通信需要內核的支持,而同一進程中的線程間通信則也可以在用戶空間(例如在DLL中)實現。在下面的敘述中,“進程間通信”和“線程間通信”這兩個詞常常是混用的,讀者應注意領會。
Windows內核所支持的進程間通信手段有:
l 共享內存區(Section)。
l 信號量(Semaphore)。
l 互斥門(Mutant)。
l 事件(Event)。
l 特殊文件“命名管道(Named Pipe)”和“信箱(Mail Slot)”。
此外,本地過程調用、即LPC,雖然并非以進程間通信機制的面貌出現,實際上卻是建立在進程間通信的基礎上,并且本身就是一種獨特的進程間通信機制。還有,Windows的Win32K模塊提供了一種線程之間的報文傳遞機制,一般用作“窗口”之間的通信手段,顯然也應算作進程間通信,只不過這是由Win32K的“擴充系統調用”支持的,而并非由基本的系統調用所支持。所以,還應增加以下兩項。
l 端口(Port)和本地過程調用(LPC)。
l 報文(Message)。
注:“Undocumented Windows 2000 Secrets”書中還列出了另一組用于“通道(Channel)”的系統調用,例如NtOpenChannel()、NtListenChannel()、NtSendWaitReplyChannel()等等,從這些系統調用函數名看來,這應該也是一種進程間通信機制,但是“Windows NT/2000 Native API Reference”書中說這些函數均未實現,調用后只是返回出錯代碼“STATUS_NOT_IMPLEMENTED”,“Microsoft Windows Internals”書中則并未提及。在ReactOS的代碼中也未見實現。
下面逐一作些介紹。
1. 共享內存區(Section)
如前所述,共享內存區是可以用于進程間通信的。但是,離開進程間同步機制,它的效率就不會高,所以共享內存區單獨使用并不是一種有效的進程間通信機制。
使用的方法是:先以雙方約定的名字創建一個Section對象,各自加以打開,再各自將其映射到自己的用戶空間,然后就可以通過常規的內存讀寫(例如通過指針)進行通信了。
要通過共享內存區進行通信時,首先要通過NtCreateSection()創建一個共享內存區對象。從程序的結構看,幾乎所有對象的創建、即所有形似NtCreateXYZ()的函數的代碼都是基本相同的,所以下面列出NtCreateSection()的代碼,以后對類似的代碼就不再列出了。
[code]NTSTATUS STDCALL
NtCreateSection (OUT PHANDLE SectionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection OPTIONAL,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL)
{
. . . . . .
PreviousMode = ExGetPreviousMode();
if(MaximumSize != NULL && PreviousMode != KernelMode)
{
_SEH_TRY
{
ProbeForRead(MaximumSize,
sizeof(LARGE_INTEGER),
sizeof(ULONG));
/* make a copy on the stack */
SafeMaximumSize = *MaximumSize;
MaximumSize = &SafeMaximumSize;
}
_SEH_HANDLE
{
Status = _SEH_GetExceptionCode();
}
_SEH_END;
if(!NT_SUCCESS(Status))
{
return Status;
}
}
/*
* Check the protection
*/
if ((SectionPageProtection & PAGE_FLAGS_VALID_FROM_USER_MODE) !=
SectionPageProtection)
{
return(STATUS_INVALID_PAGE_PROTECTION);
}
Status = MmCreateSection(&SectionObject, DesiredAccess, ObjectAttributes,
MaximumSize, SectionPageProtection,
AllocationAttributes, FileHandle, NULL);
if (NT_SUCCESS(Status))
{
Status = ObInsertObject ((PVOID)SectionObject, NULL,
DesiredAccess, 0, NULL, SectionHandle);
ObDereferenceObject(SectionObject);
}
return Status;
}[/code]
雖然名曰“Create”,實際上卻是“創建并打開”,參數SectionHandle就是用來返回打開后的Handle。參數DesiredAccess說明所創建的對象允許什么樣的訪問,例如讀、寫等等。ObjectAttributes則說明對象的名稱,打開以后是否允許遺傳,以及與對象保護有關的特性、例如訪問權限等等。這幾個參數對于任何對象的創建都一樣,而其余幾個參數就是專為共享內存區的特殊需要而設的了。其中MaximumSize當然是共享內存區大小的上限,而SectionPageProtection與頁面的保護有關。AllocationAttributes通過一些標志位說明共享區的性質和用途,例如可執行映像或數據文件。最后,共享緩沖區往往都是以磁盤文件作為后盾的,為此需要先創建或打開相應的文件,然后把FileHandle作為參數傳給NtCreateSection()。不過用于進程間通信的共享內存區是空白頁面,其內容并非來自某個文件,所以FileHandle為NULL。
顯然,創建共享內存區的實質性操作是由MmCreateSection()完成的。對于其它的對象,往往也都有類似的函數。我們看一下MmCreateSection()的代碼:
[code][NtCreateSection() > MmCreateSection()]
NTSTATUS STDCALL
MmCreateSection (OUT PSECTION_OBJECT * SectionObject,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL,
IN PFILE_OBJECT File OPTIONAL)
{
if (AllocationAttributes & SEC_IMAGE)
{
return(MmCreateImageSection(SectionObject, DesiredAccess, ObjectAttributes,
MaximumSize, SectionPageProtection,
AllocationAttributes, FileHandle));
}
if (FileHandle != NULL)
{
return(MmCreateDataFileSection(SectionObject, DesiredAccess, ObjectAttributes,
MaximumSize, SectionPageProtection,
AllocationAttributes, FileHandle));
}
return(MmCreatePageFileSection(SectionObject, DesiredAccess, ObjectAttributes,
MaximumSize, SectionPageProtection, AllocationAttributes));
}[/code]
參數AllocationAttributes中的SEC_IMAGE標志位為1表示共享內存區的內容是可執行映像(因而必需符合可執行映像的頭部結構)。而FileHandle為1表示共享內存區的內容來自文件,既然不是可執行映像那就是數據文件了;否則就并非來自文件,那就是用于進程間通信的空白頁面了。最后一個參數File的用途不明,似乎并無必要。我們現在關心的是空白頁面的共享內存區,具體的對象是由MmCreatePageFileSection()創建的,我們就不往下看了。注意這里還不涉及共享內存區的地址,因為尚未映射。
參與通信的雙方通過同一個共享內存區進行通信,所以不能各建各的共享內存區,至少有一方需要打開已經創建的共享內存區,這是通過NtOpenSection()完成的:
[code]NTSTATUS STDCALL
NtOpenSection(PHANDLE SectionHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes)
{
HANDLE hSection;
KPROCESSOR_MODE PreviousMode;
NTSTATUS Status = STATUS_SUCCESS;
PreviousMode = ExGetPreviousMode();
if(PreviousMode != KernelMode)
{
_SEH_TRY. . . . . . _SEH_END;
}
Status = ObOpenObjectByName(ObjectAttributes, MmSectionObjectType,
NULL, PreviousMode, DesiredAccess,
NULL, &hSection);
if(NT_SUCCESS(Status))
{
_SEH_TRY
{
*SectionHandle = hSection;
}
_SEH_HANDLE
{
Status = _SEH_GetExceptionCode();
}
_SEH_END;
}
return(Status);
}[/code]
這里實質性的操作是ObOpenObjectByName(),讀者想必已經熟悉。
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -