2、无名管道(Pipe):pipe()函数详解、父子进程通信实战、管道读写规则与阻塞特性、管道容量(PIPE_BUF)分析

管道,说白了就是进程间的一条数据通道。我刚开始学的时候,觉得它特别像现实中的水管——一头进水,一头出水。只不过这里流的是字节流,不是水。

无名管道是最基础的IPC方式。它只能在有亲缘关系的进程间使用,比如父子进程。为什么?因为管道没有名字,进程得通过继承文件描述符才能访问它。嗯,这个限制其实挺大的,但胜在简单。

2.1 pipe() 函数详解

创建管道就一个函数:pipe()。原型长这样:

#include <unistd.h>
int pipe(int pipefd[2]);

参数是一个包含两个整数的数组。调用成功后:

  • pipefd[0] 是读端,用来从管道里取数据
  • pipefd[1] 是写端,用来往管道里塞数据

返回0表示成功,-1表示失败。失败原因通常是系统资源不够了,比如文件描述符用完了。

核心要点:管道是半双工的。数据只能从写端流向读端。如果你想双向通信,得创建两个管道。

我记得有一次,同事问我为什么管道不能像socket那样全双工。其实设计就是这么定的,半双工实现简单,开销也小。你想想看,父子进程通信的场景,大多数时候就是父进程发指令、子进程回报结果,一个方向就够了。

2.2 父子进程通信实战

来,我们写个完整的例子。父进程发消息,子进程接收并打印。

#include <stdio.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");
        return 1;
    }

    // 2. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // 子进程:关闭写端,只读
        close(pipefd[1]);
        
        read(pipefd[0], buf, sizeof(buf));
        printf("子进程收到:%s\n", buf);
        
        close(pipefd[0]);
    } else {
        // 父进程:关闭读端,只写
        close(pipefd[0]);
        
        char *msg = "Hello from parent!";
        write(pipefd[1], msg, strlen(msg) + 1);
        
        close(pipefd[1]);
        wait(NULL);  // 等待子进程结束
    }

    return 0;
}

这个例子有几个关键点:

  • 先创建管道,再fork。 这样父子进程才能共享文件描述符
  • 关闭不需要的端。 子进程关写端,父进程关读端。这是好习惯
  • 父进程要wait。 不然子进程可能变成孤儿进程

我的习惯:每次创建管道后,立刻在注释里标明哪个是读端、哪个是写端。代码一多容易搞混,尤其是多个管道的时候。

2.3 管道读写规则与阻塞特性

管道的读写行为,说白了就四个字:按规则阻塞。我当年踩过不少坑,现在给你总结清楚。

读端行为

  • 管道有数据: read() 立即返回,能读多少读多少
  • 管道为空,写端未关闭: read() 阻塞,直到有数据写入
  • 管道为空,写端已关闭: read() 返回0,表示读到文件尾

写端行为

  • 管道未满: write() 立即写入,返回写入的字节数
  • 管道已满: write() 阻塞,直到有空间可用
  • 读端已关闭: write() 会导致进程收到 SIGPIPE 信号,默认终止进程

我曾经踩过的坑:写了一个服务程序,子进程崩溃后父进程还在往管道里写数据。结果父进程被SIGPIPE信号干掉了,整个服务挂了。后来我学会了:要么注册SIGPIPE信号处理函数,要么在write前检查读端是否存活。

为什么会这样?因为管道是内核维护的一块缓冲区。读端关闭了,数据就没人收了,再写就是浪费资源。内核干脆给你发个信号,告诉你别写了。

2.4 管道容量(PIPE_BUF)分析

管道不是无限大的。它有一个容量上限,由 PIPE_BUF 定义。在Linux上,这个值通常是4096字节(4KB)。

系统 PIPE_BUF 大小 说明
Linux 4096 字节 POSIX要求至少512字节
macOS 512 字节 老版本可能更小
FreeBSD 512 字节 可通过sysctl调整

这个值为什么重要?因为它决定了原子写入的边界。

POSIX标准规定:如果写入的数据量不超过 PIPE_BUF,那么这次写入是原子的。也就是说,多个进程同时写管道时,不会出现数据交错的情况。

举个例子:

// 进程A写入 "AAAAA"
write(pipefd[1], "AAAAA", 5);

// 进程B写入 "BBBBB"
write(pipefd[1], "BBBBB", 5);

如果两个进程同时写,读端可能读到 "AABBB" 或 "BBAAA" 吗?

答案是:不会。因为5字节小于PIPE_BUF(4096),所以每次写入都是原子的。读端要么读到 "AAAAA",要么读到 "BBBBB",绝不会读到混合数据。

重要结论:如果你要保证写入的原子性,每次写入的数据量不要超过 PIPE_BUF。超过的话,内核可能会把一次写入拆成多次,数据就可能交错。

我个人习惯是:如果数据量超过PIPE_BUF,我会用消息边界标记,或者干脆换用其他IPC方式,比如消息队列。管道毕竟是为小数据量设计的。

嗯,关于无名管道,核心内容就这些。记住三点:

  1. pipe() 创建一对文件描述符,一个读一个写
  2. 父子进程通过继承来共享管道
  3. 读写有阻塞规则,PIPE_BUF 决定原子写入边界

下一章我们讲命名管道(FIFO),它解决了无名管道只能在亲缘进程间通信的问题。到时候你会发现,很多概念是相通的。