Pipe 与 FIFO
是什么 / 解决什么问题
Pipe 是单向的字节流通道,用于进程间(或线程间)通信。匿名 pipe 用于有亲缘关系的进程,FIFO(命名管道)用于无亲缘关系的进程。
使用模式
匿名 pipe
#include <unistd.h>
int fds[2];
pipe(fds);
// fds[0]: 读端
// fds[1]: 写端
// pipe2: 支持 flags
pipe2(fds, O_NONBLOCK | O_CLOEXEC);
// 典型用法:父子进程通信
pid_t pid = fork();
if (pid == 0) {
// 子进程:读
close(fds[1]); // 关闭写端
char buf[256];
ssize_t n = read(fds[0], buf, sizeof(buf));
close(fds[0]);
} else {
// 父进程:写
close(fds[0]); // 关闭读端
write(fds[1], "hello", 5);
close(fds[1]);
}
重定向标准输入/输出
// 实现 "ls | grep foo"
int fds[2];
pipe(fds);
if (fork() == 0) {
// ls 进程:stdout → pipe 写端
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execlp("ls", "ls", NULL);
}
if (fork() == 0) {
// grep 进程:stdin ← pipe 读端
close(fds[1]);
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
execlp("grep", "grep", "foo", NULL);
}
close(fds[0]);
close(fds[1]);
wait(NULL);
wait(NULL);
FIFO(命名管道)
#include <sys/stat.h>
// 创建 FIFO 文件
mkfifo("/tmp/myfifo", 0644);
// 写端
int wfd = open("/tmp/myfifo", O_WRONLY);
write(wfd, "data", 4);
close(wfd);
// 读端(另一个进程)
int rfd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
read(rfd, buf, sizeof(buf));
close(rfd);
事件通知用途(self-pipe trick)
// 将异步信号转化为 epoll 事件
int pipe_fds[2];
pipe2(pipe_fds, O_NONBLOCK | O_CLOEXEC);
void sig_handler(int sig) {
int saved_errno = errno;
write(pipe_fds[1], "x", 1); // async-signal-safe
errno = saved_errno;
}
// 在 epoll 中监听 pipe_fds[0]
// 现代替代: signalfd 或 eventfd
行为与语义
缓冲区大小
默认 pipe 缓冲区: 64KB (16 个 4KB 页面)
Linux 2.6.35+: 可调整
// 获取/设置 pipe 容量
int size = fcntl(fds[0], F_GETPIPE_SZ); // 获取
fcntl(fds[0], F_SETPIPE_SZ, 1048576); // 设为 1MB
// 受 /proc/sys/fs/pipe-max-size 限制(默认 1MB)
// 非特权用户受 /proc/sys/fs/pipe-user-pages-soft 限制
阻塞行为
| 操作 |
条件 |
行为 |
| read |
pipe 为空且写端存在 |
阻塞等待数据 |
| read |
pipe 为空且写端已关闭 |
返回 0 (EOF) |
| write |
pipe 已满且读端存在 |
阻塞等待空间 |
| write |
读端已关闭 |
SIGPIPE + 返回 -1, EPIPE |
原子性保证
// PIPE_BUF (通常 4096 字节)
// 写入 ≤ PIPE_BUF 字节时保证原子(不会与其他写交错)
// 写入 > PIPE_BUF 字节时不保证原子
// 多进程同时写同一 pipe:
// 每次 write ≤ 4096: 消息完整不交错
// write > 4096: 数据可能交错
非阻塞 pipe
pipe2(fds, O_NONBLOCK);
// 读空 pipe: 返回 -1, EAGAIN(不阻塞)
// 写满 pipe: 返回 -1, EAGAIN(不阻塞)
// 或动态设置:
fcntl(fds[0], F_SETFL, O_NONBLOCK);
FIFO 的 open 行为
// O_RDONLY: 阻塞直到有进程打开写端
// O_WRONLY: 阻塞直到有进程打开读端
// O_RDONLY | O_NONBLOCK: 立即返回(即使没有写端)
// O_WRONLY | O_NONBLOCK: 如果没有读端,返回 ENXIO
// O_RDWR: 打开不阻塞(既是读又是写)
// 但 POSIX 未定义对 FIFO 的 O_RDWR 行为
性能考量
pipe vs socket vs shared memory
|
pipe |
Unix socket |
共享内存 |
| 方向 |
单向 |
双向 |
双向 |
| 数据拷贝 |
2次(write→内核→read) |
2次 |
0次 |
| 延迟 |
~几μs |
~几μs |
~几十ns |
| 适用 |
简单流式数据 |
需要双向/多对多 |
大量数据/低延迟 |
splice — 零拷贝 pipe
#include <fcntl.h>
// 从 fd 到 pipe 的零拷贝传输
ssize_t n = splice(file_fd, &off, pipe_fds[1], NULL, len, SPLICE_F_MOVE);
// 从 pipe 到 socket 的零拷贝传输
ssize_t n = splice(pipe_fds[0], NULL, socket_fd, NULL, len, SPLICE_F_MOVE);
// 组合使用:文件 → pipe → socket(避免数据进入用户空间)
// 这是 Linux 上 sendfile 的通用替代
vmsplice — 用户缓冲区直接映射到 pipe
struct iovec iov = { .iov_base = buf, .iov_len = len };
vmsplice(pipe_fds[1], &iov, 1, SPLICE_F_GIFT);
// SPLICE_F_GIFT: 内核可以直接使用这些页面(用户不应再修改)
常见陷阱与 FAQ
1. 忘记关闭不用的端
// 父进程忘记关闭读端 → 子进程 write 满后阻塞但不会收到 SIGPIPE
// 子进程忘记关闭写端 → 父进程 read 永远不会收到 EOF
pipe(fds);
if (fork() == 0) {
close(fds[0]); // 子进程不读,必须关闭读端
// ...
}
close(fds[1]); // 父进程不写,必须关闭写端
2. 大量小写入的性能
// 差:每次写一个字节
for (int i = 0; i < 1000000; i++)
write(fd, &byte, 1); // 100万次 syscall
// 好:缓冲后批量写
char buf[4096];
// 填充 buf...
write(fd, buf, 4096);
3. FIFO 删除后仍可用
unlink("/tmp/myfifo");
// 已经打开的 fd 仍然可用
// 新进程无法 open(文件不存在)
观测与调试
# 查看 pipe 缓冲区使用
cat /proc/<pid>/fdinfo/<fd>
# pos: 0
# flags: 04000 (O_NONBLOCK)
# pipe-size: 65536
# 查看 FIFO
ls -la /tmp/myfifo
# prw-r--r-- 1 user user 0 ... /tmp/myfifo
# 'p' 表示 pipe/fifo
# strace
strace -e trace=pipe2,read,write,splice -p <pid>
延伸阅读
man 7 pipe — pipe 行为详解
man 7 fifo — FIFO 语义
man 2 splice / man 2 vmsplice — 零拷贝
man 2 tee — pipe 内容复制