Windows操作系统提供了多种机制用于实现应用程序间的通信及数据的共享,比如剪贴板(Clipboard)、组件对象模型(COM)、数据复制(Data Copy)、动态数据交换(DDE)、文件映射(File Mapping)、邮件槽(Mailslots)、管道(Pipes)、远程过程调用(RPC)、Windows套接字(Windows Sockets)等,这些技术统称为进程间通信(IPC, Interprocess communication)技术。 现在我用两篇文章就其中的管道做一下简单介绍。管道,顾名思义,就是供信息流动的通道。它有两个端点,一个用于信息的写入,即写入端,另一个用于信息的读出,即读出端。根据管道内信息的流向,管道可被设定为单向(One-way)或双向(Two-way, 或 duplex)。单向管道允许位于写入端的进程写入信息,位于读出端的进程读出信息;双向管道允许每端的进程既可以写入信息,又可以读出信息。
管道本质上是一段共享内存,供进程用作彼此互相通信的渠道。创建管道的进程被称作管道服务端(Pipe server),连接到现有管道的进程被称作管道客户端(Pipe client)。
Windows环境的管道有两种,一种是匿名管道(Anonymous Pipes),即不需为其指定名称,另一种叫命名管道(Named pipes),在创建时需要遵循指定的规则为其指定名称。前者比后者占用系统资源少,但功能没有后者强大。
本文只介绍匿名管道,命名管道在下一篇介绍。匿名管道(Anonymous pipes)匿名管道不需在创建时为其命名,不支持跨网络的通信,且只支持单向通信。若要用匿名管道实现进程间的双向通信,则要为每个方向单独创建一条匿名管道。
要创建一个匿名管道需要调用CreatePipe()函数,它返回两个句柄(handle),读、写各一个。读句柄只能对管道进行读操作,写句柄只能对管道进行写操作。
要使用匿名管道实现进程间通信,服务端进程必须通过某种方式将一个句柄传递到与之通信的另一个进程,至于传送哪一个,则要看所需要的信息传送方向了。这是一个与命名管道间的重要差异,通信双方都没有彼此知晓的名称,只能通过将一个句柄传递到对方的方式来建立联系了。
基于这种建立联系的方式,匿名管道通常用于父、子进程之间的通信。父进程可借助继承机制很自然地将匿名管道的句柄传递给子进程,从而在父子进程间建立管道连接。要实现这一点,父进程要在几个关键节点扫除障碍。首先父进程要在创建匿名管道时将传入CreatePipe()函数的SECURITY_ATTRIBUTES结构中的bInheritHandle成员设置为TRUE,这使得CreatePipe()返回的句柄允许被子进程继承;另外父进程在调用CreateProcess()函数创建子进程时要明确要求子进程继承父进程的所有句柄。下面将整个流程描述一下。
父进程通常是将标准输入(Standard input)、标准输出(Standard output)文件句柄重定向到匿名管道的读、写句柄来实现向子进程进行句柄传递的,父进程在子进程创建完毕后再将标准输入、输出文件句柄恢复原值。
假如我们现在希望子进程向管道写入、父进程读取子进程写入的内容,那我们需要向子进程传递匿名管道的写句柄。父进程调用GetStdHandle()函数获取当前标准输出文件句柄,将其暂存以备后面恢复使用,然后调用SetStdHandle()将标准输出文件句柄设置为匿名管道的写入句柄。现在创建子进程,这样通过继承机制,子进程的标准输出文件句柄就是匿名管道的写句柄了。
匿名管道的写句柄被子进程继承之后,父进程就不再需要该写句柄了,应调用CloseHandle()将其关闭,这一点很重要,后面会提到;而且还应及时用SetStdHandle()将已被重定向的标准输出文件句柄恢复为原值——稍早暂存的值。
子进程继承了父进程的标准输出文件句柄,这实际上就是匿名管道的写句柄。子进程可调用GetStdHandle()来获取这一句柄,用WriteFile()函数向管道写入信息,父进程用ReadFile()从管道读取信息,从而实现了父子进程的信息交流。
要从匿名管道读取信息,要用读句柄调用ReadFile()函数。如果管道为空ReadFile()会一直等待,直到写入端进程将信息写入管道才能返回。如果该管道的所有写入句柄都被关闭了,ReadFile()会返回0。这一点需特别注意,这就是为什么前面我们要求父进程在创建子进程后要用CloseHandle()关闭匿名管道的写句柄,因为如果不关闭,即使子进程关闭了自己的写句柄,父进程的ReadFile()也不会返回0——父进程还有一个写句柄未关闭!如果读取过程中出错ReadFile()也会返回,这时可用GetLastError()获得错误码。
要向匿名管道写入信息,要用写句柄调用WriteFile()函数。WriteFile()要在将指定字节数的信息都写入匿名管道后才会返回,如果此时管道是满的且还有未写入的信息,WriteFile()会等,直到读取端读走信息,空出足够空间供WriteFile()完成写操作。当然,同ReadFile()一样,如果写入操作中途出错,WriteFile()也会返回,可用GetLastError()获得错误码。
前面分析了父进程通过继承机制向子进程传递匿名管道的写句柄的过程,如果反过来,我们希望父进程向匿名管道写入信息,子进程从中读出信息,那么我们要向子进程传递读句柄,通过将父进程的标准输入文件句柄设置为读句柄,经过继承机制,子进程的标准输入文件句柄就是匿名管道的读句柄,就可以用GetStdHandle()获取用来读取信息了。这里需要注意的是一定要确保匿名管道的写句柄没有被继承到子进程,也是因为前面提到的原因,为了确保子进程的ReadFile()能在父进程关闭写句柄的时候能正确地返回0。
最后再做两点说明:写入匿名管道的信息是以字节流的形式写入的,读取进程无法从中分辨出哪些信息是由哪一次写操作写入的,除非读写双方事先约定如何来判断每次写入信息的结束方式。这又是一个匿名管道与命名管道相比比较逊色的地方。其实匿名管道的应用不仅限于父子进程间的通信,也可用于不相关进程间的通信,只要能将一个句柄正确地传递给对方就行,比如通过某种进程间通信的方式。
下一篇介绍命名管道。