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)”也是类似的特殊文件,只是信槽所提供的是无连接的通信机制。一般把这样无连接的通信称为“数据报”机制,而命名管道所提