44 管道和 FIFO

管道

管道:一般指的是匿名管道

创建方式:

  1. 可以用 pipe() 创建,通过 fork() 或者 UNIX 域套接字共享给其他进程。
  2. 也可以通过 popen() 创建子进程。

popen()system() 有一些差异:

  1. system() 会为调用进程忽略 SIGINT 和 SIGTERM,但是 popen() 不会忽略这些信号,因为调用进程没有阻塞等待子进程。
  2. popen() 不会阻塞 SIGCHLD。如果阻塞了,那么在对应的 pclose() 之前就不能正常接受子进程退出的消息了。但这也有个问题:wait() 可能会接收到 popen() 创建的子进程的消息,这样调用 pclose() 的时候就会返回 -1 并设置 errno 为 ECHLD。
  3. popen()pclose() 配套。除了关闭文件描述符之外,pclose() 还会回收子进程,所以不能用 fclose() 代替 pclose()

管道的一些性质

这些性质都是匿名 / 命名管道都有的。

1. 块缓冲

管道是块设备,因此 stdio 库的输出时会使用块缓冲(想要及时刷新缓冲区可以:1. 手动刷新;2. 用 setbuf() 修改缓冲模式;3. 分配伪终端使写入按照字符设备的方式刷新)。

2. 单向传输

管道的传输方向是单向的,如果想要双向数据传输,可以用 UNIX domain stream socket pairs(socketpair() 系统调用)。

3. 容量有限

管道的本质是缓冲区,所以容量是有上限的。Linux 2.6.11 起,管道的存储能力是 64KB(更早的内核固定使用一页作为缓冲区)。可以通过 fcntl(fd, F_SETPIPE_SZ, size) 来修改这个限制,非特权进程可以将这个值修改到 /proc/sys/fs/pipe-max-size 的数值以内的一个数。

/proc/sys/fs/pipe-max-size 的默认大小是 1M,只有特权进程(CAP_SYS_RESOURCE)才能修改:

eric@debian:~$ cat /proc/sys/fs/pipe-max-size
1048576

4. 数据少时保证原子写入

一次性写入不超过 PIPE_BUF 字节就是原子写入,如果写成功可以保证内容不会和其他写者交叉。这在有多个写者的情况下很有用。SUSv3 要求这个值至少为 512,Linux 上这个值是 4096。PIPE_BUF 可以通过宏获取,也可以通过 fpathconf(fd, _PC_PIPE_BUF) 来获取,我在虚拟机上测试用 fpathconf() 获取,也是 4096。

5. 如果写 / 读时管道的另外一端没有读 / 写者(已关闭)会怎么样?

如果管道没有读者(以可读方式打开管道的进程,可以是 O_RDWR,但损害可移植性),用 write() 写入数据会收到 SIGPIPE 信号并返回 EPIPE(如果没有被 SIGPIPE 杀死)。

如果管道没有写者,而且管道中也没有数据了,read() 从中读数据会返回 0(读取的字节数),否则会阻塞。Man 手册:If the file offset is at or past the end of file, no bytes are read, and read() returns zero.

命名管道(FIFO)

FIFO:命名管道,除了在文件系统中可以访问之外其他和普通管道相似

mkfifo [-m mode] pathname 可以创建命名管道。还有一个系统调用 mkfifo(3)

打开命名管道有同步的语义:只有同时有读者和写者打开一个命名管道时,双方的 open() 才能返回,否则将一直阻塞。将 FIFO 文件重定向到可执行程序中也有同步语义:

eric@debian:~/tlpi/fifo$ cat main.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
  long ret = fpathconf(0, _PC_PIPE_BUF);
  printf("ret = %ld\n", ret);
}
eric@debian:~/tlpi/fifo$ gcc main.c 
eric@debian:~/tlpi/fifo$ ./a.out < myfifo # 因为没有进程以写入方式打开 myfifo,所以阻塞
^C

有的系统(包括 Linux)支持以 O_RDWR 方式打开命名管道,这样进程同时作为读者和写者,对命名管道的打开就不会阻塞。但是这是非标准的,建议避免依赖这种写法。SUSv3 明确指出以 O_RDWR 标记打开一个 FIFO 的结果是未知的。

例子:使用 FIFO 作为 IPC 工具

C-S 架构,有一个公知的文件路径作为服务器的命名管道,客户端通过向其中写数据来向服务器发送请求。为了防止不同客户端之间的数据交叉,每个客户端都应该有一个用于和服务器通信的独立 FIFO(客户端可以打开 FIFO 之后用 unlink() 删掉以防止 FIFO 残留),和服务器取得联系之后就更换用于通信的 FIFO。

实现要点:

  1. 服务器先以只读方式打开 FIFO(可能会阻塞),然后以只写方式打开 FIFO(获得第二个 fd)。持有可写的 fd 可以保证之后的 read() 在没数据时会阻塞,而不会失败返回。例子中的服务器就是想要在没数据时阻塞,这样做可以简化处理逻辑。
  2. 客户端通过 atexit() 或者提前 unlink() 来保证用于客户端 - 服务器通信的 FIFO 被及时删除。

在管道和 FIFO 上使用非阻塞 I/O

如果以 O_NONBLOCK 打开 FIFO,且管道的另外一端没有被打开,那么在只读 / 只写的方式下打开有不同的效果(前面已经说了以同时读写的方式打开是非标准的):

  1. 如果是非阻塞 + 只读打开,那么就不需要等待写者加入。因为读取总是可以返回 0 个字节来表示 end-of-file。
  2. 如果是非阻塞 + 只写打开,如果没有读者加入就会失败,并将 errno 设置为 ENXIO。这是因为在没有读者的情况下允许了非阻塞打开 FIFO,那么今后写入的时候就会收到 SIGPIPE 信号,所以这种情况下,FIFO 被设计为在打开的时候就立即返回错误。

O_NONBLOCK 除了影响 FIFO 的打开行为之外,还会影响读写,因此可能需要在打开 FIFO 之后用 fcntl() 决定启用 / 关闭 O_NONBLOCK 标志。

对于管道和 FIFO,O_NONBLOCK 只会改变本该阻塞时读写的行为:

至于向管道 / FIFO 中写数据,也是差不多的表格,这里省略。在写入数据量小于等于原子写入保证时,如果写不下 + 非阻塞,则会立即失败并设置 errno 为 EAGAIN;如果能写下则全部写入。如果写入数据量大于原子写入保证,又是非阻塞调用,写多少算多少,完全写不下则失败并设置 errno 为 EAGAIN。