Windows 进程间通信(三)

5. 命名管道(Named Pipe)和信箱(Mail Slot)
前面提到,如果从字面上理解,那么进程间通信也可以通过磁盘文件而实现。但是,把信息写入某个磁盘文件,再由另一个进程从磁盘文件读出,在速度上是很慢的。固然,由于文件缓冲区(Cache)的存在,对磁盘文件的写和读未必都经过磁盘,但是那并没有保证。再说,普通的文件操作也没有提供进程间同步的手段。所以通过普通的磁盘文件实现进程间通信是不太现实的。但是这也提示我们,如果能实现一种特殊文件,使得对文件的读写只在缓冲区中进行(而不写入磁盘),并且实现进程间的同步,那倒是个不坏的主意。命名管道就是这样一种特殊文件。实际上,命名管道还不仅是这样的特殊文件,它还是一种网络通信的机制,只是当通信的双方存在于同一台机器上时,才落入本文所说的进程间通信的范畴。
既然命名管道是一种特殊文件,它的创建、打开、读写等等操作就基本上都可以利用文件系统中的有关资源加以实现。当然,这毕竟是一种特殊文件,对于使用者来说,最大的特殊之处在于这是一个“先进先出”的字节流,不能对其执行lseek()一类的操作。
先看命名管道的创建,Windows的Win32 API上提供了一对库函数CreateNamedPipeA()和CreateNamedPipeW(),前者用于ASCII码字符串,后者用于“宽字符”即Unicode的字符串,实际上前者只是把8位字符转换成Unicode以后再调用后者。对CreateNamedPipeW()的调用大致如下:

[code] Handle = CreateNamedPipeW(L"[A].//pipe//MyControlPipe[/url]",
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
. . . . . .);[/code]

这里的管道名实际上是“//./pipe/MyControlPipe”,前面的“//.”表示本机,而“/pipe/MyControlPipe”则是路径名。如果把“.”换成主机名,那就可以是在另一台机器上的对象。参数PIPE_ACCESS_DUPLEX表示要建立的是“双工”、即可以双向通信的管道。另一个参数是一些标志位,表示要建立的管道采用“报文(message)”的方式、而不是字节流的方式读写,并且是阻塞方式、即有等待的读写。
CreateNamedPipeW()内部通过系统调用NtCreateNamedPipeFile()完成命名管道的创建,这是Windows内核为命名管道提供的唯一的系统调用:

[code]NTSTATUS STDCALL
NtCreateNamedPipeFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock,
ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions,
ULONG NamedPipeType, ULONG ReadMode, ULONG CompletionMode,
ULONG MaximumInstances, ULONG InboundQuota, ULONG OutboundQuota,
PLARGE_INTEGER DefaultTimeout)
{
NAMED_PIPE_CREATE_PARAMETERS Buffer;

. . . . . .
if (DefaultTimeout != NULL)
{
Buffer.DefaultTimeout.QuadPart = DefaultTimeout->QuadPart;
Buffer.TimeoutSpecified = TRUE;
}
else
{
Buffer.TimeoutSpecified = FALSE;
}
Buffer.NamedPipeType = NamedPipeType;
Buffer.ReadMode = ReadMode;
Buffer.CompletionMode = CompletionMode;
Buffer.MaximumInstances = MaximumInstances;
Buffer.InboundQuota = InboundQuota;
Buffer.OutboundQuota = OutboundQuota;

return IoCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
NULL, FILE_ATTRIBUTE_NORMAL, ShareAccess, CreateDisposition,
CreateOptions, NULL, 0, CreateFileTypeNamedPipe, (PVOID)&Buffer,
0);
}[/code]
在Windows中,文件系统是I/O子系统的一部分,文件的创建是由IoCreateFile()完成的,参数CreateFileTypeNamedPipe是个文件类型代码,它决定了所创建的是作为特殊文件的命名管道。至于IoCreateFile()如何创建此种特殊文件,那已经不在本文要讨论的范围内。
一般,命名管道是由“服务端”线程创建的,创建以后还要对其调用一个Win32 API库函数ConnectNamedPipe(),以等待“客户端”线程打开这个命名管道、从而使双方建立起点对点的连接(注意这里的Connect与Socket机制中的Connect意思完全不同)。

[code]BOOL STDCALL
ConnectNamedPipe(HANDLE hNamedPipe,
LPOVERLAPPED lpOverlapped)
{
PIO_STATUS_BLOCK IoStatusBlock;
IO_STATUS_BLOCK Iosb;
HANDLE hEvent;
NTSTATUS Status;

if (lpOverlapped != NULL)
{
lpOverlapped->Internal = STATUS_PENDING;
hEvent = lpOverlapped->hEvent;
IoStatusBlock = (PIO_STATUS_BLOCK)lpOverlapped;
}
else
{
IoStatusBlock = &Iosb;
hEvent = NULL;
}

Status = NtFsControlFile(hNamedPipe, hEvent, NULL, NULL, IoStatusBlock,
FSCTL_PIPE_LISTEN, NULL, 0, NULL, 0);
if ((lpOverlapped != NULL) && (Status == STATUS_PENDING))
return TRUE;

if ((lpOverlapped == NULL) && (Status == STATUS_PENDING))
{
Status = NtWaitForSingleObject(hNamedPipe, FALSE, NULL);
if (!NT_SUCCESS(Status))
{
SetLastErrorByStatus(Status);
return FALSE;
}
Status = Iosb.Status;
}

if ((!NT_SUCCESS(Status) && Status != STATUS_PIPE_CONNECTED) ||
(Status == STATUS_PENDING))
{
SetLastErrorByStatus(Status);
return FALSE;
}
return TRUE;
}[/code]
实际的操作是由NtFsControlFile()完成的,而NtFsControlFile()有点像Linux中的fcntl(),常常用来实现除标准的“读”、“写”等等以外的许多不同操作。
“客户端”线程通过打开文件建立与“服务端”线程的连接,所打开的就是作为特殊文件的命名管道。Windows没有特别为此提供系统调用,而是直接利用NtOpenFile(),例如:

[code] Status = NtOpenFile(&FileHandle, FILE_GENERIC_READ, &ObjectAttributes, &Iosb,
FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_SYNCHRONOUS_IO_NONALERT);[/code]
这里使用的参数FILE_SYNCHRONOUS_IO_NONALERT表示对文件的访问将是同步的、也即阻塞的。要打开的文件名在数据结构ObjectAttributes中,所以这里看不见。当然,这文件名必须与“服务端”所使用的相同。由此可见,“客户端”线程不必知道这是一个命名管道,甚至不必知道这是特殊文件,而只把它当成一般的文件看待。
在最简单的情况下,这就已经够了,下面就可以通过已经建立的连接进行通信了。具体的操作在形式上就跟普通文件的读/写一样。在读普通文件时,当事线程也会被阻塞(如果要读的内容不是已经在缓冲区中),此时它所等待的主要是磁盘完成其物理的读出过程,而磁盘完成其读出过程以后的中断则会解除它的阻塞,相当于执行了一次V操作。相比之下,对于命名管道,启动读操作的线程也会被阻塞(如果尚无已经到达的数据或报文),而此时等待的是管道的对方线程往管道中写数据。对方往管道中写数据,就解除了等待方线程的阻塞,实质上构成一次V操作。所以,普通文件的读/写同样也包含了同步机制,只不过那一般是进程与外部设备(或外部物理过程)之间的同步,而命名管道所需的是进程与进程之间的同步,但是这并没有本质上的不同。事实上,读者以后会看倒,在设备驱动程序中常常要用到NtWaitForSingleObject()之类的等待机制,而且这正是设备驱动得以实现的基础。
一般而言,如果在管道中尚无数据的时候启动读操作,当事线程就会被阻塞。但是,为避免被阻塞,也可以先通过PeekNamedPipe()查询一下是否已经有报文到达。

[code]BOOL STDCALL
PeekNamedPipe(HANDLE hNamedPipe, LPVOID lpBuffer,
DWORD nBufferSize, LPDWORD lpBytesRead,
LPDWORD lpTotalBytesAvail, LPDWORD lpBytesLeftThisMessage)
{
PFILE_PIPE_PEEK_BUFFER Buffer;
IO_STATUS_BLOCK Iosb;
ULONG BufferSize;
NTSTATUS Status;

BufferSize = nBufferSize + sizeof(FILE_PIPE_PEEK_BUFFER);
Buffer = RtlAllocateHeap(RtlGetProcessHeap(),0, BufferSize);

Status = NtFsControlFile(hNamedPipe, NULL, NULL, NULL, &Iosb,
FSCTL_PIPE_PEEK, NULL, 0, Buffer, BufferSize);
if (Status == STATUS_PENDING)
{
Status = NtWaitForSingleObject(hNamedPipe, FALSE, NULL);
if (NT_SUCCESS(Status)) Status = Iosb.Status;
}
if (Status == STATUS_BUFFER_OVERFLOW)
{
Status = STATUS_SUCCESS;
}

. . . . . .

if (lpTotalBytesAvail != NULL)
{
*lpTotalBytesAvail = Buffer->ReadDataAvailable;
}
if (lpBytesRead != NULL)
{
*lpBytesRead = Iosb.Information - sizeof(FILE_PIPE_PEEK_BUFFER);
}
if (lpBytesLeftThisMessage != NULL)
{
*lpBytesLeftThisMessage = Buffer->MessageLength -
(Iosb.Information - sizeof(FILE_PIPE_PEEK_BUFFER));
}
if (lpBuffer != NULL)
{
memcpy(lpBuffer, Buffer->Data,
min(nBufferSize, Iosb.Information - sizeof(FILE_PIPE_PEEK_BUFFER)));
}
RtlFreeHeap(RtlGetProcessHeap(),0, Buffer);

return(TRUE);
}[/code]
同样,实际的查询是由NtFsControlFile()完成的,只是用了另一个“命令码”。
除命名管道外,还有“信槽(MailSlot)”也是类似的特殊文件,只是信槽所提供的是无连接的通信机制。一般把这样无连接的通信称为“数据报”机制,而命名管道所提供的有连接通信则称为“虚电路”机制。“数据报”机制所提供的通信是“尽力传递”而不是“保证传递”,所以是不可靠的。此外,信槽所提供的通信不一定是点对点、也可以是广播式的。和命名管道一样,Windows为信槽也只提供了一个系统调用,即NtCreateMailslotFile()。而且它的内部也只是调用IoCreateFile(),不同的只是作为参数的文件类型为CreateFileTypeMailslot。既然这么相似,这里就不多说了。

6. 本地过程调用(LPC)
“本地过程调用”是建立在“端口(Port)”基础上的一种Client/Server结构的跨进程过程调用机制。而Port则是类似于Unix域Socket那样的进程间通信机制。离开了进程间通信,Client/Server结构就无从谈起。所以Port机制是基础,而LPC是建立在此种基础上的应用。
实际上,前面所讲的各种机制都是单项的手段,严格说来都不构成高效的进程间通信。例如共享内存区提供了传递数据的手段,却不具备进程间同步的功能;而信号量、互斥门、事件三者都是进程间同步的手段,却又缺少了传递数据的功能。所以,真要有高效的进程间通信,就得把这两方面结合起来。而“端口(Port)”恰恰就是把数据传递和进程间同步结合起来、集成在一起而形成的高效率进程间通信机制。
按说Port才是高效的进程间通信机制,可是Microsoft却又偏偏不把它归入进程间通信,也不作为进程间通信的手段在Win32 API上向应用软件提供,而是使它的应用局限于跨进程的过程调用。当然,本地过程调用本身确实不是进程间通信,而是进程间通信的一种应用。不过Windows内核自身倒是在利用Port作为进程间通信的手段。例如每个进程实际上都有一个Debug端口和一个Exception端口,在程序执行的某些点上、或者在发生异常时,往往会通过这两个端口给有关的进程发送信息。
由于LPC机制的复杂性,笔者将另撰专文予以介绍,这里就不多说了。

7. 报文(Message)
报文(Message)是一种用于“视窗(Window)”的线程间通信机制。在早期的Windows系统中,Message是实现于用户空间的“应用层”进程间通信机制。后来用户空间的许多基础设施都移到了内核中,成为模块Win32k.sys,并为此而对Windows的系统调用界面进行了扩充。这一扩充可真有点喧宾夺主,增加了六百多个“扩展系统调用”,而原来“正宗”的只不过是二百多个。这六百多个新增的系统调用大部分都是用于图形操作,但是也有用于Message机制的系统调用,包括NtUserGetMessage()、NtUserWaitMessage()、NtUserSendMessage()、NtUserPostMessage()等等。相应地,在Win32 API界面上则提供了GetMessage()、WaitMessage()、SendMessage()、PostMessage()等等库函数。这些库函数都在user32.dll中。
以前说过,每个线程在内核中都有个ETHREAD数据结构,此外还有个因Win32k而来的W32THREAD数据结构,其定义为:

[code]typedef struct _W32THREAD
{
PVOID MessageQueue;
FAST_MUTEX WindowListLock;
LIST_ENTRY WindowListHead;
LIST_ENTRY W32CallbackListHead;
struct _KBDTABLES* KeyboardLayout;
struct _DESKTOP_OBJECT* Desktop;
HANDLE hDesktop;
DWORD MessagePumpHookValue;
BOOLEAN IsExiting;
} W32THREAD, *PW32THREAD;[/code]
这个数据结构的第一个成分就是MessageQueue,就是用于Message传递的。
其实,要说Windows的各种进程间/线程间通信机制的“知名度”,恐怕莫过于这Message机制了。这是因为Windows应用程序的核心就是一个接收Message、处理Message的循环。典型Windows应用程序的基本结构如下:

[code]int WINAPI
WinMain(HINSTANCE hInst, HINSTANCE hPrevInstance,
LPSTR lpszCmdLine, int nCmdShow)
{
. . . . . .
RegisterClassEx(&wndclass);

/* Create main window */
hwndMain = CreateWindow(szFrameClass, szAppTitle, dwStyle, xPos, yPos,
xWidth, yHeight, 0, 0, hInstance, NULL);
ShowWindow(hwndMain, nCmdShow);
UpdateWindow(hwndMain);

while (GetMessage(&msg, NULL, 0, 0) != FALSE)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return(msg.wParam);
}[/code]
这里的GetMessage()就是用于Message接收的库函数,其内部就是通过扩展系统调用NtUserGetMessage()实现的。而Windows应用程序的核心就是这个while循环。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值