进行读写的功能(该调用目前也是 POSIX 规范的一部分(4) )。该系统调用能创建一对已连接的(UNIX 族)无名 socket。在 Linux 中,完全可以把这一对 socket 当成 pipe 返回的文件描述符一样使用,唯一的区别就是这一对文件描述符中的任何一个都可读和可写。
这似乎可以是一个用来实现进程间通信管道的好方法。不过,要注意的是,为了解决我前面的提出的使用 sort 的应用问题,我们需要关闭子进程的标准输入通知子进程数据已经发送完毕,而后从子进程的标准输出中读取数据直到遇到 EOF。使用两个单向管道的话每个管道可以单独关闭,因而不存在任何问题;而在使用双向管道时,如果不关闭管道就无法通知对端数据已经发送完毕,但关闭了管道又无法从中读取结果数据。——这一问题不解决的话,使用 socketpair 的设想就变得毫无意义。
令人高兴的是,shutdown 调用 (5) 可解决此问题。毕竟 socketpair 产生的文件描述符是一对 socket,socket 上的标准操作都可以使用,其中也包括 shutdown。——利用 shutdown,可以实现一个半关闭操作,通知对端本进程不再发送数据,同时仍可以利用该文件描述符接收来自对端的数据。没有错误检测的代码示意如下:
很清楚,这比使用两个单向管道的方案要简洁不少。我将在此基础上作进一步的封装和改进。
封装和实现
直接使用上面的方法,无论怎么看,至少也是丑陋和不方便的。程序的维护者想看到的是程序的逻辑,而不是完成一件任务的各种各样的繁琐细节。我们需要一个好的封装。
封装可以使用 C 或者 C++。此处,我按照 UNIX 的传统,提供一个类似于 POSIX 标准中 popen/pclose 函数调用的 C 封装,以保证最大程度的可用性。接口如下:
关于接口,以下几点需要注意一下:
- 与 pipe 函数类似,dpopen 返回的是文件结构的指针,而不是文件描述符。这意味着,我们可以直接使用 fprintf 之类的函数,文件缓冲区会缓存写入管道的数据(除非使用 setbuf 函数关闭文件缓冲区),要保证数据确实写入到管道中需要使用 fflush 函数。
- 由于 dpopen 返回的是可读写的管道,所以 popen 的第二个表示读/写的参数不再需要。
- 在双向管道中我们需要通知对端写数据已经结束,此项操作由dphalfclose函数来完成。
具体的实现请直接查看程序源代码,其中有详细的注释和 doxygen 文档注释(6) 。我只略作几点说明:
- 本实现使用了一个链表来记录所有 dpopen 打开的文件指针和子进程 ID 的对应关系,因此,在同时用 dpopen 打开的管道的多的时候,dpclose(需要搜索链表)的速度会稍慢一点。我认为在通常使用过程中这不会产生什么问题。如果在某些特殊情况下这会是一个问题的话,可考虑更改 dpopen 的返回值类型和 dpclose 的传入参数类型(不太方便使用,但实现简单),或者使用哈希表/平衡树来代替目前使用的链表以加速查找(接口不变,但实现较复杂)。
- 当编译时在 gcc 中使用了 “-pthread” 命令行参数时,本实现会启用 POSIX 线程支持,使用互斥量保护对链表的访问。因此本实现可以安全地用于 POSIX 多线程环境之中。
- 与 popen 类似(7) ,dpopen 会在 fork 产生的子进程中关闭以前用 dpopen 打开的管道。
- 如果传给 dpclose 的参数不是以前用 dpopen 返回的非 NULL 值,当前实现除返回 -1 表示错误外,还会把 errno 设为 EBADF。对于 pclose 而言,这种情况在 POSIX 规范中被视为不确定(unspecified)行为 (8)。
- 实现中没有使用任何平台相关特性,以方便移植到其它 POSIX 平台上。
下面的代码展示了一个简单例子,将多行文本送到 sort 中,然后取回结果、显示出来:
输出结果为:
总结
本文阐述了一个使用 socketpair 系统调用在 Linux 上实现双向进程通讯管道的方法,并提供了一个实现。该实现提供的接口与 POSIX 规范中的 popen/pclose 函数较为接近,因而非常易于使用。该实现没有使用平台相关的特性,因而可以不加修改或只进行少量修改即可移植到支持 socketpair 调用的 POSIX 系统中去。