44 管道和 FIFO
管道
管道:一般指的是匿名管道。
创建方式:
- 可以用
pipe()
创建,通过fork()
或者 UNIX 域套接字共享给其他进程。 - 也可以通过
popen()
创建子进程。
popen()
和 system()
有一些差异:
system()
会为调用进程忽略 SIGINT 和 SIGTERM,但是popen()
不会忽略这些信号,因为调用进程没有阻塞等待子进程。popen()
不会阻塞 SIGCHLD。如果阻塞了,那么在对应的pclose()
之前就不能正常接受子进程退出的消息了。但这也有个问题:wait()
可能会接收到popen()
创建的子进程的消息,这样调用pclose()
的时候就会返回 -1 并设置errno
为 ECHLD。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。
实现要点:
- 服务器先以只读方式打开 FIFO(可能会阻塞),然后以只写方式打开 FIFO(获得第二个 fd)。持有可写的 fd 可以保证之后的
read()
在没数据时会阻塞,而不会失败返回。例子中的服务器就是想要在没数据时阻塞,这样做可以简化处理逻辑。 - 客户端通过
atexit()
或者提前unlink()
来保证用于客户端 - 服务器通信的 FIFO 被及时删除。
在管道和 FIFO 上使用非阻塞 I/O
如果以 O_NONBLOCK
打开 FIFO,且管道的另外一端没有被打开,那么在只读 / 只写的方式下打开有不同的效果(前面已经说了以同时读写的方式打开是非标准的):
- 如果是非阻塞 + 只读打开,那么就不需要等待写者加入。因为读取总是可以返回 0 个字节来表示 end-of-file。
- 如果是非阻塞 + 只写打开,如果没有读者加入就会失败,并将
errno
设置为 ENXIO。这是因为在没有读者的情况下允许了非阻塞打开 FIFO,那么今后写入的时候就会收到 SIGPIPE 信号,所以这种情况下,FIFO 被设计为在打开的时候就立即返回错误。
O_NONBLOCK
除了影响 FIFO 的打开行为之外,还会影响读写,因此可能需要在打开 FIFO 之后用 fcntl()
决定启用 / 关闭 O_NONBLOCK
标志。
对于管道和 FIFO,O_NONBLOCK
只会改变本该阻塞时读写的行为:
至于向管道 / FIFO 中写数据,也是差不多的表格,这里省略。在写入数据量小于等于原子写入保证时,如果写不下 + 非阻塞,则会立即失败并设置 errno
为 EAGAIN;如果能写下则全部写入。如果写入数据量大于原子写入保证,又是非阻塞调用,写多少算多少,完全写不下则失败并设置 errno
为 EAGAIN。