无名管道:pipe函数、管道的读写规则、管道的阻塞与非阻塞、父子进程管道通信示例
大家好,我是你们的老朋友。今天我们来聊聊进程通信里最基础、也最经典的一个机制——无名管道。说实话,我刚开始学 Linux 系统编程那会儿,觉得管道这东西挺简单的,不就是一根管子嘛,一头写一头读。但后来在项目中踩过几次坑,才发现里面门道不少。嗯,咱们今天就把这块彻底讲透。
1. pipe 函数:创建管道的入口
无名管道,说白了就是内核里的一块缓冲区。它没有名字,所以只能在有亲缘关系的进程之间用——最常见的场景就是父子进程。创建它的函数就是 pipe()。
#include <unistd.h>
int pipe(int pipefd[2]);
这个函数很简单,传入一个长度为 2 的 int 数组。调用成功后:
pipefd[0]是读端,用来从管道里取数据pipefd[1]是写端,用来往管道里塞数据
我个人习惯把这两个文件描述符分别叫做 fd_read 和 fd_write,这样代码读起来一目了然。返回 0 表示成功,-1 表示失败,常见的失败原因是系统文件描述符用完了。
2. 管道的读写规则:别想得太复杂
管道的读写规则其实很直观,但有几个细节容易让人迷糊。我当年刚入行时就在这上面栽过跟头。
2.1 写端写入,读端读出
数据从写端进去,从读端出来。先进先出,没有随机访问。你想想看,这就像一根水管,水只能从一头进另一头出,不能从中间接个水龙头。
2.2 读空与写满
- 读空:如果管道里没数据了,读操作会阻塞,直到有数据写入。
- 写满:管道有容量上限(通常是 65536 字节,也就是 64KB)。写满了再写,也会阻塞。
我在项目中遇到过一个问题:父进程往管道里写了一大堆数据,子进程读得慢,结果父进程写满了就卡住了。当时排查了半天,最后发现是子进程处理逻辑太慢,读端没及时取数据。解决办法?要么加大缓冲区,要么让子进程读得快一点。
2.3 写端关闭后,读端读到 EOF
当所有写端都被关闭后,读端再读会返回 0,表示读到文件末尾。这个特性很重要,很多管道通信的结束信号就是靠这个来传递的。
2.4 读端关闭后,写端收到 SIGPIPE
如果读端关了,写端还在写,内核会向写端进程发送 SIGPIPE 信号。这个信号的默认行为是终止进程。所以,如果你不想让进程挂掉,记得处理这个信号,或者忽略它。
3. 管道的阻塞与非阻塞
默认情况下,管道是阻塞模式的。但你可以通过 fcntl() 把它改成非阻塞。
#include <fcntl.h>
int flags = fcntl(pipefd[0], F_GETFL, 0);
fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK);
改成非阻塞后,读写行为会发生变化:
| 操作 | 阻塞模式 | 非阻塞模式 |
|---|---|---|
| 读空管道 | 阻塞等待数据 | 立即返回 -1,errno 设为 EAGAIN |
| 写满管道 | 阻塞等待空间 | 立即返回 -1,errno 设为 EAGAIN |
非阻塞模式适合用在事件驱动或轮询的场景里。比如你用 select() 或 epoll() 监控多个管道时,非阻塞模式就很有用。
4. 父子进程管道通信示例
光说不练假把式。咱们直接上一个完整的例子,看看父子进程怎么通过管道通信。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int pipefd[2];
pid_t pid;
char buf[1024];
// 1. 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 2. 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程:负责读数据
close(pipefd[1]); // 关闭写端
while (1) {
memset(buf, 0, sizeof(buf));
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n == 0) {
// 写端关闭,读到 EOF
printf("子进程:父进程关闭了写端,我退出了\n");
break;
} else if (n == -1) {
perror("read");
break;
}
printf("子进程收到:%s", buf);
}
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
// 父进程:负责写数据
close(pipefd[0]); // 关闭读端
const char *messages[] = {
"Hello, 子进程!\n",
"这是第二条消息\n",
"最后一条消息,再见!\n"
};
for (int i = 0; i < 3; i++) {
write(pipefd[1], messages[i], strlen(messages[i]));
sleep(1); // 模拟处理间隔
}
close(pipefd[1]); // 关闭写端,通知子进程结束
wait(NULL); // 等待子进程退出
printf("父进程:子进程已退出\n");
}
return 0;
}
这个例子很简单,但包含了所有关键点:
- 先 pipe,再 fork。顺序不能乱。
- 父子进程各自关闭不需要的端。子进程关写端,父进程关读端。
- 父进程写完数据后关闭写端,子进程读到 EOF 后退出。
- 父进程用 wait() 等待子进程结束,避免僵尸进程。
5. 知识体系总览
下面这张图把无名管道的核心知识点串起来了,方便你整体把握。
好了,关于无名管道的内容就讲到这里。记住,管道虽然简单,但它是理解更复杂 IPC 机制(比如命名管道、消息队列、共享内存)的基础。把这块吃透了,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321