windows平台管道机制使用小结

最近需要在产品中用到windows平台的管道通讯机制,因此对管道进行了一番考察,将相关的信息记录如下(只记录要点,如何使用的代码请看MSDN上的例子):

管道是windows平台下的一种RPC机制,不但支持同机器的,也支持跨机器的访问。总之一句话,linux下的管道机制,直观好用,功能强大。windows的管道使用相对复杂,有一些“出乎意料”的地方。


匿名管道

匿名管道,没有名字,创建后返回两个handle,一个是读端一个是写端。由于没有名字,而windows的handle值又是进程内有效的。因此传递handle到另一个进程最方便的方法就是在创建管道的那个进程中,把handle值继承给子进程。否则要用DuplicateHandle函数为目标进程生成一份handle的复制品,再通过另一种RPC方式把这个值传递给目标进程。这个有点绕,本来因为管道简单,想用它来做RPC机制,但又不得不用另一种RPC机制来传管道的handle。(注,有窗口的进程可以通过WM_COPYDATA消息来传handle,没窗口的进程就有点麻烦。)

匿名管道创建出来是单向的,也就是说,如果你要用匿名管道在进程间进行双向通讯就要创建两个匿名管道,一个方向一个。MSDN上提到,匿名管道在内部是通过命名管道来实现的,只是系统底层为它维护了一个唯一的名字,但是命名管道可以是双工的,不知道为什么匿名的就不能双工。

匿名管道创建出来是同步的,不知道是否可以通过其他函数把它设置成异步的,这个我没有试了。

综上所述,匿名管道的使用场景非常有限。不过如果它刚好适合你的需求,你还是会觉得它很方便。


命名管道

命名管道有一个唯一的名字,任何进程都可以通过名字得到这个管道。另外命名管道在创建时可以设置很多参数来控制管道的属性。可以进行异步读写。 在某种程度上管道很像TCP,可以认为TCP以IP做名字,命名管道的名字是你自己定的;TCP用一个四元组来区分不同连接并进行消息的多路分离;命名管道是通过实例,即服务器端可以用同一个名字多次调用CreateNamedPipe来创建同一命名管道的多个实例,允许多个客户端同时通过同一个名字来得到管道和服务器进行通讯。可以创建的最大实例数是在调用CreateNamedPipe函数创建命名管道时指定的,以后不能再改。这个限制对于可扩展的服务器应用是比较郁闷的。

在服务器端,对于多实例的管道,可以用overlap机制也可以使用complet I/O机制来进行消息的多路分离。具体参见MSDN上的例子,很详细。


管道的吞吐量

我写了段代码进行测试,管道在windows XP下的表现最差,只能达到200M/秒左右。在NT、Vista和Win7下可以到500M/秒。为什么在XP下的表现这么差有点奇怪,毕竟现在XP是目前装机量最高的系统。


可创建的管道数量

管道实例和TCP连接一样属于内核对象,要占用内核资源,尤其是它们的读写缓冲都是占用内核的不可换页内存,因此实例的数量在理论上受制于系统资源即物理内存大小。我连续创建30000个命名管道,每个的读写缓冲都指定为4096B,系统性能似乎没有什么影响,创建的速度也很快。不过我只创建,并没有使用。那个缓冲区的大小,MSDN说是只是一个参考值,系统内部会进行动态的调整,因此也有可能我这种情况下,并没有实际给管道的实例分配缓冲区。创建到50000个时系统的性能出现明显的下降,创建的速度也下降了。我使用的系统是2G的物理内存。


另外几个值得注意的问题

一个是创建命名管道时的第二个参数(dwPipeMode),MSDN上对于可指定的两种模式的说明是,一种是字节流(TYPE_BYTE),一种是消息流(TYPE_MESSAGE),(注意同一管道的读端和写端的模式可以分别指定)。但并没有说明这两种模式之间的区别。我看到网上好多人在问这两者之间的区别是什么,但没看到有明确的回复。

其实字节流(TYPE_BYTE)就和TCP的数据流是一样的,系统在缓冲时可能会进行分割,读端单次读到的字节数可能和写端写入的字节数是不一至的,必须在应用层对字节流进行分帧处理。而(TYPE_MESSAGE)则相当于是windows的管道机制在字节流上做了一次分帧,系统保证在写端,一次以消息模式进行的一次写入的字节,在读端会被一次以消息模式进行的读完整的读出来,不多也不少。这样可以省去应用层的分帧逻辑。所以可以看到,如果指定写端为字节流模式,则读端也只能是字节流模式,不能再指定为消息流模式。如果指定写端为消息流模式,则读端还可以指定为字节流模式或是消息流模式。个人认为如果真有人把这两端的模式指定为不一至的话,是纯属找抽。

还有一个是和阻塞式管道相关的问题。我们这次在项目中用了管道,为了简化编程模型特地在单独的工作线程中使用管道,这样可以进行阻塞式的读。因为我们的应用不会同时开启很多的管道,所以用线程来隔离管道并使用阻塞式模型可以极大的简化代码,同时不影响性能。但是最后我在review代码时发现还是被实现成了在一线程一管道的方式下使用overlap异步模型,实现的同事告诉我,他们发现使用同步模型时在两端读写不对称时会出现管道被“卡死”,感觉不像全双工,更像是半双工的。所以又匆忙的改成了异步模式。我当时就觉得很奇怪,如果是这样的话,双工的阻塞式命名管道就没什么的意义了。

其实只要让两端的读线程不阻塞就可以避免这个卡死。虽然是用阻塞模式,但又要避免它阻塞。无语。。。

if (PeekNamedPipe(hPipe, NULL, 0, NULL, &dwByteAvailable, NULL) && 0 != dwByteAvailable) {
    BOOL bRes = ReadFile(hPipe, buf, BUFSIZE, &cbByteRead, NULL);
    //.....
} else {
    Sleep(500);
}

将读循环中的ReadFile替换成上述的代码,使用PeekNamedPipe在每次阻塞读之前先看看管道里有没有数据,如果没有就不读。这样可以保证每一次的阻塞读不会被阻塞。这样就避免了上述的问题。

最后的结论就是,尽量不要用windows的管道机制,就算真需要管道的语义也应该优先考虑TCP或是UDP。这样应用如果需要进行分布式扩展时会更加的灵活。

Leave a Reply