管道
关于管道:
管道分为匿名管道和命名管道。
匿名管道的开销比命名管道小。管道有两个端,单向管道允许一端写入,另一端读取。而双向管道允许在两端都进行读写。创建管道的进程通常称为pipe server, 连接到一个管道的进程叫做pipe client。
匿名管道:匿名管道是没有命名的,
基于字节流
,单向的,通常用于父进程和子进程间的通信。匿名管道
只能用于父进程和子进程之间或者父进程的多个子进程之间
,
一般用于程序输入输出的重定向;
不能用于网络间主机的数据交换。
匿名管道的操作流程:
1)调用CreatePipe函数创建一个匿名管道,并返回两个句柄:读/写句柄。读句柄拥有只读权限,而写句柄也只拥有只写权限。
2)为了进行通信,pipe server需要将创建成功的句柄传递给其他进程。通常这种传递方式是通过继承来实现的(进程允许自己的子进程继承某些内核句柄对象);另一种传递方式也可以是通过其他的进程间通信方式,调用DuplicateHandle函数,并发送到另一个毫不相干的进程中。
3)pipe server既可以发生读句柄,也可以发送写句柄,这完全根据读写的需要来决定。
4)如果是要从管道中读取信息,可以调用ReadFile函数,并把管道的读句柄传给它。ReadFile会在下面的情况下返回:
1. 有另外的进程在管道的写入端写数据。
2. 管道的所有写句柄都关闭。
3. 在读操作完成之前产生错误时。
5)如果是要向管道中写入信息,可以调用WriteFile函数,并将管道的鞋句柄传给它。WrieteFile会在下面的情况下返回:
1. 向管道写入指定长度的数据。
2. 写入时产生错误。
如果管道的buffer被写满了,那么直到有另外的进程从管道中读取数据时,
WriteFile才会返回。所以尽量将buffer设置大一点。在调用 CreatePipe的时候可以设置缓存大小。
6)匿名管道不支持异步读写数据。所以不能在使用匿名管道时调用ReadFileEx和WriteFileEx等函数。同时,ReadFile和WriteFile的lpOverlapped参数将被忽略。
7)匿名管道在所有的读写句柄被关闭后,将被销毁。同样,如果匿名管道句柄被销毁的话,那么它所有的读写句柄也将被关闭。
8)匿名管道的实现是基于一个拥有特殊名字的命名管道。所以在需要命名管道的地方,依然可以传递一个匿名管道的句柄。
管道句柄的继承:
pipe server可以通过下面的方式来控制它的句柄是否可被继承。
1)CreatePipe函数有一个SECURITY_ATTRIBUTES的参数,如果pipe server将bInheritHandle设置为true,那么CreatePipe创建的句柄将可以被继承。
2)pipe server可以用DuplicateHandle函数来改变句柄的继承性。pipe server可以创建一个可继承句柄的不可继承副本,也可以创建一个不可继承句柄的可继承副本。
3)CreateProcess函数可以允许pipe server来指定它的子进程是否可继承。
当一个子进程继承了一个管道句柄,系统就会允许它来获得该管道句柄。但是父进程必须告诉子进程这个句柄的值。父进程通常都是通过重定向标准输出句柄的方式来实现的,读取端作为子进程的标准输入、写入段作为子进程的标准输出:
1. 调用GetStdHandle来获取到当前的标准输出句柄。(注意需要将该句柄保存,方便后来恢复)
2. 调用SetStdHandle函数来将标准输出句柄设置到管道的写端句柄。现在父进程就可以创建子进程了。
3. 父进程调用CloseHandle函数来关闭管道的写端句柄,因为子进程继承写句柄后,父进程将不再需要持有该写句柄了。(这步很重要)
4. 子进程通过调用GetStdHandle来获取它的标准输出句柄,也就是管道的写入端句柄了。
5. 子进程可以调用WriteFile来写入管道。
6. 写入完成后,子进程需要调用CloseHandle来关闭管道的写入端句柄。
7. 通信完成后,调用SetStdHandle来恢复现在保存的标准输出句柄。
在上述过程中,父进程通过ReadFile来获取管道中的数据。写入管道中的数据是以字节流的形式发生的,也就是说,从管道中读取数据的父进程根本不知道它读的数据是写入端分几次写的,除非父子进程事先有约定在每次写完后添加特殊的标识来表示写入操作的结束。当管道的所有句柄都被关闭后,ReadFile将返回0.
匿名管道的例子可以参照下面
Creating a Child Process with Redirected Input and Output
http://msdn.microsoft.com/en-us/library/windows/desktop/ms682499%28v=vs.85%29.aspx
1
命名管道:
像其他可以命名的内核对象一样,命名管道有自己的名称。一个命名管道的所有实例都共享同一个名称,但是不同的实例有独自的buffer和句柄。命名管道允许pipe server同时与多个pipe client进行通信。任何进程都可以访问到命名管道,所以命名管道使毫不相干的进程间进行通信变得很容易。任何进程都可以同时作为pipe server和pipe client,这点跟p2p很相似。pipe server可以调用CreateNamedPipe来实例化一个命名管道,调用ConnectNamedPipe来等待客户端的管道连接。pipe client通过调用CreateFile和CallNamedPipe来连接到pipe server。命名管道不仅用于本机间的进程间通信,同时也可以用于网络上不同主机间的通信。pipe server不能在其他机器上创建命名管道。pipe client必须在编译期明确要连接的pipe的名称。
命名管道名称:\\ServerName\pipe\PipeName ( 网络机器
)
\\.\pipe\PipeName ( 本机 )
命名管道具有以下几个特征:
(1)命名管道是单向或者双向的,所以两个进程可以通过同一管道进行交互。
(2)命名管道不但可以面向字节流,还可以面向消息,所以读取进程可以读取写进程发送的不同长度的消息。
(3)多个独立的管道实例可以用一个名称来命名。例如几个客户端可以使用名称相同的管道与同一个服务器进行并发通信。
(4)命名管道可以用于网络间两个进程的通信,而其实现的过程与本地进程通信完全一致。
(1)命名管道是单向或者双向的,所以两个进程可以通过同一管道进行交互。
(2)命名管道不但可以面向字节流,还可以面向消息,所以读取进程可以读取写进程发送的不同长度的消息。
(3)多个独立的管道实例可以用一个名称来命名。例如几个客户端可以使用名称相同的管道与同一个服务器进行并发通信。
(4)命名管道可以用于网络间两个进程的通信,而其实现的过程与本地进程通信完全一致。
命名管道的类型、读取和等待模式:
pipe server通过CreateNamedPipe可以通过参数dwPipeMode设置管道的类型模式、读取模式和等待模式。而pipe client也可以通过CreateFile来设置这些属性。
1)类型模式
类型模式指出的是数据如何被写入到命名管道中。数据可以作为字节流和消息流的方式被传输。字节流方式跟匿名管道类似,系统不会区分每次写操作写了多少数据。消息流方式下,系统将每次写入的数据看做不同的消息单元。
2)读取模式
读取模式决定了数据如何从命名管道中被读出。数据可以被设置为byte-read模式或者message-read模式。字节流类型模式的管道只能设置为byte-read模式。而消息流类型模式的管道既可以设置为byte-read模式也可以设置为message-read模式。
字节流读取方式在所有可读数据读取完之后,或者在指定字节数的数据读取完成之后才算成功。消息流读取方式只有在整条消息读取完成才算成功。
3)等待模式
等待模式决定ReadFile(无数据可读的情况下)、WriteFile(写缓存区不够的情况下)和ConnectNamedPipe(无客户端连接的情况下)函数调用在遇到较长的数据流时的等待决策。
blocking-wait模式下,管道上的操作无限等待另一端操作的完成,之后当前端才会返回。
nonblocking-wait模式下,上述调用将立即返回。默认情况下,所有的命名管道都是blocking-wait模式的。需要nonblocking-wait模式,就要在CreateNamedPipe函数中设置为PIPE_NOWAIT。
命名管道的多个实例:
最简单的命名管道是pipe server创建一个管道的实例,连接上一个单独的pipe client,随后进程数据交换,断开连接,关闭管道句柄并结束。但是更多的情况下我们是一个pipe server能够连接多个pipe clients。但是这样性能会很差,所以pipe server需要创建多个管道实例来同时处理多个pipe clients。可以用如下两种方式来满足上述要求:
1) 多线程server,即为每个管道实例创建一个单独的线程。(容易实现,但如果pipe clients太多的情况下,多线程会消耗大量的系统资源成为瓶颈)
2) 单线程server利用重叠结构。(相比多线程,性能较好,但如何协调多个客户端的实现比较繁琐)
命名管道操作流程:
1)pipe server调用CreateNamedPipe来创建命名管道,并通过nMaxInstances参数来指定最多同时存在的管道实例数量。pipe server可以重复调用CreateNamedPipe来创建多个pipe实例,只要不操作之前设置的最大数量。
2)pipe server创建好一个管道实例后,pipe client就可以通过调用CreateFile或者CallNamedPipe连接到pipe server上来。如果暂时还没有pipe instance可供连接,那么pipe client也可以调用WaitNamedPipe等待。
3)pipe clients和pipe serve可以调用如下函数来写入和读取管道。
1. ReadFile和WriteFile在字节流读取模式和消息流读取模式下均可调用。
2. ReadFileEx和WriteFileEx
在字节流读取模式和消息流读取模式下均可调用,但是必须在重叠结构中使用。
3. PeekNamedPipe
在字节流读取模式和消息流读取模式下均可调用,其作用是
读取数据但不将其移除。
4. TransactNamedPipe用于在消息流读取模式下的双向管道中,其作用是在一次操作中写入请求消息并读取回复消息。有助于提升网络性能。
注*:pipe server在pipe client启动前不应该执行阻塞性的读操作,否则会产生条件竞争。当client和server不再使用某个管道实例后,应该由server先调用FlushFileBuffers来确保client能够读取到所有的数据。FlushFileBuffers会等待客户端读取完成后才返回,然后server调用DisconnectNamedPipe来关闭连接。确保客户端断开连接后server调用CloseHandle来释放管道实例对象。然后,server可以继续调用ConnectNamedPipe来允许另一个客户端连接上来。