挺好的一篇分析文章,转载一下,作为收藏。
=====================================
对于任何一个现代的操作系统,进程间通信都是其系统结构的一个重要组成部分。而说到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(),读者想必已经熟悉。
双方都打开了一个共享内存区对象以后,就可以各自通过NtMapViewOfSection()将其映射到自己的用户空间。
[code]NTSTATUS STDCALL
NtMapViewOfSection(IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress OPTIONAL,
IN ULONG ZeroBits OPTIONAL,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PULONG ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType OPTIONAL,
IN ULONG Protect)
{
. . . . . .
PreviousMode = ExGetPreviousMode();
if(PreviousMode != KernelMode)
{
SafeBaseAddress = NULL;
SafeSectionOffset.QuadPart = 0;
SafeViewSize = 0;
_SEH_TRY. . . . . . _SEH_END;
. . . . . .
}
else
{
SafeBaseAddress = (BaseAddress != NULL ? *BaseAddress : NULL);
SafeSectionOffset.QuadPart = (SectionOffset != NULL ? SectionOffset->QuadPart : 0);
SafeViewSize = (ViewSize != NULL ? *ViewSize : 0);
}
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_OPERATION,
PsProcessType,
PreviousMode,
(PVOID*)(PVOID)&Process,
NULL);
. . . . . .
AddressSpace = &Process->AddressSpace;
Status = ObReferenceObjectByHandle(SectionHandle,
SECTION_MAP_READ,
MmSectionObjectType,
PreviousMode,
(PVOID*)(PVOID)&Section,
NULL);
. . . . . .
Status = MmMapViewOfSection(Section, Process,
(BaseAddress != NULL ? &SafeBaseAddress : NULL),
ZeroBits, CommitSize,
(SectionOffset != NULL ? &SafeSectionOffset : NULL),
(ViewSize != NULL ? &SafeViewSize : NULL),
InheritDisposition, AllocationType, Protect);
ObDereferenceObject(Section);
ObDereferenceObject(Process);
. . . . . .
return(Status);
}[/code]
以前讲过,NtMapViewOfSection()并不是专为当前进程的,而是可以用于任何已打开的进程,所以参数中既有共享内存区对象的Handle,又有进程对象的Handle。从这个意义上说,两个进程之间通过共享内存区的通信完全可以由第三方撮合、包办而成,但是一般还是由通信双方自己加以映射的。显然,这里实质性的操作是MmMapViewOfSection(),那是存储管理底层的事了,我们就不再追究下去了。
一旦把同一个共享内存区映射到了通信双方的用户空间(可能在不同的地址上),出现在这双方用户空间的特定地址区间就落实到了同一组物理页面。这样,一方往里面写的内容就可以为另一方所见,于是就可以通过普通的内存访问(例如通过指针)实现进程间通信了。
但是,如前所述,通过共享内存区实现的只是原始的、低效的进程间通信,因为共享内存区只满足了前述三个条件中的两个,只是提供了信息的(实时)传输,但是却缺乏进程间的同步。所以实际使用时需要或者结合别的进程间通信(同步)手段,或者由应用程序自己设法实现同步(例如定时查询)。