跳转至

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 内容复制