第十三章 进程间通信:管道


什么是管道

当从一个进程连接数据流到另一个进程时,我们使用术语管道( pipe )。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。

对于 shell 命令,命令的连接是通过管道字符来完成的,如:

cmd1 | cmd2

shell 负责安排两个命令的标准输入和标准输出:

进程管道

最简单的在两个程序之间传递数据的方法是使用 popen 和 pclose 函数。

popen 打开一个子进程,返回一个文件流,通过文件流从子进程的标准输入读取数据,或向子进程的标准输出写数据。

如何实现 popen

请求 popen 调用运行一个程序时,它首先启动一个 shell ,然后将命令字符串作为参数传递给它。这么做有一个好处和一个缺点。

好处是, Linux 可以用 shell 做参数扩展,从而可以让 popen 执行非常复杂的 shell 命令。

缺点是,不仅要启动被请求的程序,还需要启动一个 shell ,从节省资源的角度看, popen 函数的调用成本略高。

pipe 调用

pipe 函数相对更底层。通过这个函数两个程序传递数据不需要启动一个 shell 。

我们可以用 pipe, fork, exec 来模拟一个生产者、消费者程序。

管道关闭后的读操作

通常读取输入的程序不知道有多少数据需要读取,所以往往采用循环的方法,读取数据 - 处理数据 - 读取更多数据,直到没有数据可读为止。

当没有数据可读时, read 调用通常会阻塞,直到有数据到达为止。如果管道的另一端已被关闭,对一个已关闭写数据的管道做 read 调用将返回0而不是阻塞。

如果跨越 fork 调用使用管道,就会有两个不同的文件描述符可以用于管道写数据,一个在父进程中,一个在子进程中。只有把父子进程对管道的写文件描述符都关闭,管道才会被认为是关闭了,对 read 调用才会失败。

把管道用作标准输入和标准输出

为了完成这个工作,可以使用 dup 函数。

dup 函数打开一个新的文件描述符,与其参数共同指向同一个文件,并且它是可用的最小文件描述符。因此,只要关闭了标准输入0或标准输出1,那么获得的新描述符就是其中之一,接下来就可以使用标准输入输出函数进行操作。

如果参数是管道文件描述符,那么标准输入输出函数操作的实际上就是管道。

详见书本。

参考代码:

命名管道 FIFO

之前的讨论只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。

FIFO 可以让我们在不相关的进程之间传递数据,它通常被称为命名管道( named pipe )。它以一种特殊的文件的形式存在。

我们可以在命令行上创建命名管道:

$ mkfifo <filename>

mkfifo 函数在程序中创建一个命名管道。

访问 FIFO 文件

命名管道出现在文件系统中,所以可以像平常文件名一样在命令中使用。

比如,可以这样开始读一个管道:

$ cat < /tmp/my_fifo

在另一个终端,对这个管道进行写操作:

$ echo "Hello World" > /tmp/my_fifo

使用 open 打开 FIFO 文件

程序不能以 O_RDWR 模式打开 FIFO 文件进行读写操作,这样做的后果并未明确定义。 FIFO 只是为了单向传递数据,所以没有必要使用 O_RDWR 。

可以使用 O_NONBLOCK 的组合模式打开文件。

对 FIFO 进行读写操作

使用了 O_NONBLOCK 模式会影响到 FIFO 的 read 和 write 调用。

对一个空的、阻塞的 FIFO 的 read 调用将等待,直到有数据可以读时才继续。

对一个空的、非阻塞的 FIFO 的 read 调用将立刻返回0字节。

对一个满的、阻塞的 FIFO 的 write 调用将等待,直到有数据可以被写入时才会继续执行。

如果 FIFO 不能接收所有写入的数据,那么:

- 如果请求写入的数据长度小于等于 PIPE_BUF 字节,调用失败,数据不能写入。
- 如果请求写入的数据长度大于 PIPE_BUF 字节,将写入部分数据,返回实际写入的字节数,也可能是0 。

任一时刻,在一个 FIFO 中可以存在的数据长度是有限制的,它由 #define PIPE_BUF 定义。

系统规定:在一个以 O_WRONLY (非阻塞)打开的 FIFO 中,如果写入的数据长度小于等于 PIPE_BUF ,那么或者写入全部字节,或者一个字节都不写入。