fork 后父子进程可以用 pipe 传递字节流
pipe 是 Unix/Linux 中最基础的进程间通信机制之一。它创建一条由内核维护的单向字节流通道,一个文件描述符用于读取,另一个文件描述符用于写入。
它常用于父子进程通信、命令行管道、日志转发、数据流处理,以及和 splice、tee、vmsplice 等零拷贝接口组合使用。
函数签名
#include <unistd.h>
int pipe(int pipefd[2]);调用成功返回 0,失败返回 -1 并设置 errno。
pipefd 数组包含两个文件描述符:
pipefd[0] // 读端
pipefd[1] // 写端数据流向固定为:
write(pipefd[1]) -> kernel pipe buffer -> read(pipefd[0])pipe 是单向通道。如果两个进程需要双向通信,通常需要创建两个 pipe。
基本读写示例
同一个进程中也可以使用 pipe。下面示例先写入字符串,再从读端取出数据:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe");
return 1;
}
const char *msg = "hello pipe";
write(pipefd[1], msg, strlen(msg));
char buf[128];
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("read: %s\n", buf);
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}这段代码没有体现进程间通信,但展示了 pipe 的基本数据方向:写端进入,读端取出。
父子进程通信
pipe 最常见的用法是配合 fork。父进程先创建 pipe,然后 fork 子进程。fork() 会复制当前进程,形成父进程和子进程两条执行路径。调用返回后,两边都会继续从 fork() 后面的代码往下执行,但返回值不同:子进程中返回 0,父进程中返回子进程的 PID,失败时返回 -1。
fork 后,父子进程都持有同一组 pipe 文件描述符,因此可以通过关闭不用的一端来建立单向通信。
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
// pipefd[0] 是读端,pipefd[1] 是写端。
int pipefd[2];
// 创建一条匿名管道。失败时返回 -1,并设置 errno。
if (pipe(pipefd) < 0) {
perror("pipe");
return 1;
}
// fork 会创建一个子进程。
// 从这一行之后,父进程和子进程都会继续执行后面的代码。
pid_t pid = fork();
if (pid < 0) {
// pid < 0 表示 fork 失败,没有创建出子进程。
perror("fork");
return 1;
}
if (pid == 0) {
// pid == 0 表示当前代码运行在子进程中。
// 子进程只负责读取,所以关闭不用的写端。
close(pipefd[1]);
char buf[128];
// 从 pipe 的读端读取父进程写入的数据。
// 这里最多读取 sizeof(buf) - 1 字节,留 1 字节给字符串结尾 '\0'。
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0) {
// read 读取的是原始字节,不会自动补字符串结尾。
// 如果要按 C 字符串打印,需要手动添加 '\0'。
buf[n] = '\0';
printf("child read: %s\n", buf);
}
// 子进程读完后关闭读端并退出。
close(pipefd[0]);
return 0;
}
// 运行到这里的是父进程。
// 父进程只负责写入,所以关闭不用的读端。
close(pipefd[0]);
const char *msg = "hello from parent";
// 向 pipe 的写端写入数据,子进程会从读端读到这些字节。
write(pipefd[1], msg, strlen(msg));
// 写完后关闭写端。这样子进程继续 read 时可以正确感知 EOF。
close(pipefd[1]);
// 等待子进程退出,避免产生僵尸进程。
wait(NULL);
return 0;
}这里父进程只保留写端,子进程只保留读端。关闭不用的一端很重要,否则读端可能无法正确收到 EOF。wait(NULL) 用于等待子进程结束,避免子进程退出后短暂变成僵尸进程。
close 只关闭当前进程的 fd
fork() 之后,父进程和子进程里的 pipefd[0]、pipefd[1] 数字通常是一样的。但这个数字不是系统全局唯一的管道编号,而是各自进程文件描述符表里的索引。
可以把 fork 后的关系理解成:
内核 pipe 对象
↑
父进程 fd 3 -> 读端引用
父进程 fd 4 -> 写端引用
子进程 fd 3 -> 读端引用
子进程 fd 4 -> 写端引用当子进程执行:
close(pipefd[1]);关闭的是子进程 fd 表里的写端引用,不会关闭父进程 fd 表里的 pipefd[1]。关闭后可以理解成:
内核 pipe 对象
↑
父进程 fd 3 -> 读端引用
父进程 fd 4 -> 写端引用
子进程 fd 3 -> 读端引用
子进程 fd 4 已关闭此时父进程仍然可以通过自己的 pipefd[1] 写入数据。只有当所有进程里指向写端的 fd 都关闭后,读端继续读取才会得到 EOF。反过来,只有当所有进程里指向读端的 fd 都关闭后,写端继续写入才会触发 SIGPIPE 或返回 EPIPE。
因此示例中的 close 不是关闭对方的端口,而是关闭当前进程不需要的引用。这样可以让内核准确判断还有没有读者或写者。
pipe 是字节流
pipe 传输的是字节流,不是消息队列。它不保留每一次 write 的业务边界。
例如写端连续写入:
write(fd, "hello", 5);
write(fd, "world", 5);读端可能一次读到:
helloworld也可能分多次读到。应用层如果需要消息边界,应自行定义协议,例如固定长度头部、换行分隔、TLV 格式等。
有一个特殊规则:当单次写入长度不超过 PIPE_BUF 时,Linux 保证这次写入相对于其他写入是原子的。这个保证只解决多个写者并发写入时的数据交错问题,不等于读端一定按同样长度读取。
阻塞与 EOF
默认情况下,pipe 是阻塞的。
读端行为:
- pipe 中有数据:
read返回实际读取的字节数。 - pipe 中没有数据,但仍有写端打开:
read阻塞等待。 - 所有写端都已关闭:
read返回0,表示 EOF。
写端行为:
- pipe 缓冲区有空间:
write写入数据并返回。 - pipe 缓冲区已满:
write阻塞等待读端消费。 - 所有读端都已关闭:
write触发SIGPIPE,或在忽略信号时返回-1并设置errno = EPIPE。
这些规则决定了 pipe 程序必须认真关闭不再使用的文件描述符。
非阻塞 pipe
可以使用 pipe2 创建带标志的 pipe:
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
int pipefd[2];
pipe2(pipefd, O_NONBLOCK | O_CLOEXEC);常用标志:
O_NONBLOCK:读写操作不阻塞,没有数据或没有空间时返回错误。O_CLOEXEC:执行exec后自动关闭文件描述符,避免泄漏到新程序。
非阻塞 pipe 常与 poll、epoll 或事件循环组合使用。
命名管道 FIFO
普通 pipe 通常用于有亲缘关系的进程,因为文件描述符需要通过 fork 继承。没有亲缘关系的进程可以使用 FIFO,也叫命名管道。
创建 FIFO:
#include <sys/stat.h>
mkfifo("/tmp/myfifo", 0600);之后不同进程可以像打开普通文件一样打开它:
int fd = open("/tmp/myfifo", O_RDONLY);FIFO 仍然是字节流,读写语义和 pipe 接近,但它在文件系统中有名字,适合无亲缘关系进程之间建立简单通信通道。
与 splice 的关系
splice 经常需要 pipe 作为中转结构:
int pipefd[2];
pipe(pipefd);
splice(file_fd, NULL, pipefd[1], NULL, len, 0);
splice(pipefd[0], NULL, socket_fd, NULL, len, 0);这时 pipe 不一定是为了两个进程通信,而是作为内核中的数据搬运缓冲区。数据路径是:
file_fd -> pipe buffer -> socket_fd用户态不需要分配大块数据缓冲区,也不直接读取文件内容。
注意事项
pipe 的容量有限,不能把它当作无限队列使用。写端速度长期高于读端时,最终会阻塞或在非阻塞模式下返回 EAGAIN。
多个进程同时写同一个 pipe 时,应关注写入大小和协议边界。超过 PIPE_BUF 的写入可能和其他写者的数据交错。
长期运行的程序应统一管理文件描述符生命周期。忘记关闭写端会导致读端一直等不到 EOF;忘记关闭读端会让写端错误处理变得不明确。
总结
pipe 是一条由内核维护的单向字节流。它可以连接父子进程,也可以作为 splice 的内核中转缓冲区。正确使用 pipe 的关键是理解读端、写端、阻塞、EOF 和文件描述符关闭规则。
本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。