一、无名管道:进程间通信的入门课

大家好,我是你们这门课的老朋友。今天咱们聊聊进程间通信里最基础、也最经典的一个机制——无名管道。

说实话,我刚开始做嵌入式开发那会儿,对管道这东西挺不屑的。觉得它功能太简单,限制又多。直到有一次,我在一个资源受限的MCU项目里,需要让两个实时任务交换传感器数据,共享内存太复杂,消息队列又太重。最后我回头用了个管道,几行代码就搞定了。嗯,从那以后我再也不敢小看它了。

1.1 管道的概念与原理

管道(Pipe)是什么?说白了,就是内核里的一块缓冲区。它像一根水管,一头进水,一头出水。一个进程往里面写数据,另一个进程从里面读数据。

你想想看,这其实就是最朴素的「生产者-消费者」模型。写端是生产者,读端是消费者。数据在内核空间流动,用户程序不用操心同步问题——内核帮你搞定了。

管道分为两种:

  • 无名管道:只能用于有亲缘关系的进程(父子、兄弟)。没有名字,用完就消失。
  • 有名管道(FIFO):有路径名,可以在任意进程间通信。这个我们后面章节会讲。

今天咱们只聊无名管道。

核心要点:无名管道是半双工的。数据只能朝一个方向流动。如果你想双向通信,得开两根管道。

1.2 无名管道的创建:pipe函数

创建无名管道,靠的是 pipe() 系统调用。它的原型长这样:

#include <unistd.h>

int pipe(int pipefd[2]);

这个函数会给你返回两个文件描述符:

  • pipefd[0]:读端。你可以用 read() 从里面拿数据。
  • pipefd[1]:写端。你可以用 write() 往里面塞数据。

成功返回0,失败返回-1并设置errno。

个人习惯:我写代码时,喜欢把 pipefd[0]pipefd[1] 分别定义为 PIPE_READPIPE_WRITE 宏,这样代码可读性高很多。你也不妨试试。

来看一个最简单的例子:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int fd[2];
    
    if (pipe(fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    printf("读端 fd: %d\n", fd[0]);
    printf("写端 fd: %d\n", fd[1]);
    
    // 用完记得关闭
    close(fd[0]);
    close(fd[1]);
    
    return 0;
}

运行后你会看到两个数字,通常是3和4。为什么?因为0、1、2已经被标准输入、标准输出、标准错误占用了。

1.3 管道的读写特性

管道读写有几个关键特性,我当年踩过不少坑,今天一并告诉你:

  • 数据是字节流:没有消息边界。你写100字节,读端可能一次读到100字节,也可能分两次读到50+50。别指望它像UDP那样保边界。
  • 读空管道:默认情况下,读端会阻塞,直到有数据写入。
  • 写满管道:默认情况下,写端会阻塞,直到读端取走数据腾出空间。
  • 写端关闭后读:读端会读到文件结束符(EOF),即 read() 返回0。
  • 读端关闭后写:写端会收到 SIGPIPE 信号,进程默认会终止。如果你不想进程挂掉,记得处理这个信号。

我曾经踩过的坑:有一次我忘了关闭写端,结果读端一直阻塞在 read() 上,因为内核认为还有进程可能往管道里写数据。排查了半天才发现是文件描述符泄漏。记住:不用的端一定要关掉!

1.4 管道的阻塞与非阻塞

管道默认是阻塞模式。但你可以通过 fcntl() 把它改成非阻塞:

#include <fcntl.h>

// 获取当前标志
int flags = fcntl(fd[0], F_GETFL, 0);
// 设置非阻塞
fcntl(fd[0], F_SETFL, flags | O_NONBLOCK);

非阻塞模式下:

  • 读空管道:read() 立即返回 -1,errno 设为 EAGAIN
  • 写满管道:write() 立即返回 -1,errno 设为 EAGAIN

我个人建议,初学者先用阻塞模式。非阻塞模式配合 poll()select() 使用,那是进阶玩法。

1.5 父子进程间的管道通信

这才是管道的经典用法。流程很简单:

  1. 父进程先创建管道。
  2. 父进程 fork() 出子进程。
  3. 子进程继承父进程的文件描述符表,所以它也有管道的读写端。
  4. 决定数据流向:如果父写子读,父进程关闭读端,子进程关闭写端。

来看代码:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int fd[2];
    pid_t pid;
    char buf[1024];
    
    if (pipe(fd) == -1) {
        perror("pipe");
        exit(1);
    }
    
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程:读数据
        close(fd[1]);  // 关闭写端
        read(fd[0], buf, sizeof(buf));
        printf("子进程收到: %s\n", buf);
        close(fd[0]);
    } else {
        // 父进程:写数据
        close(fd[0]);  // 关闭读端
        const char *msg = "Hello from parent!";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
        wait(NULL);  // 等待子进程结束
    }
    
    return 0;
}

运行结果:

子进程收到: Hello from parent!

关键点:fork之后,父子进程各有一份文件描述符副本。如果不关闭不用的端,会导致引用计数不为0,读端永远读不到EOF,写端永远写不进去。这是新手最容易犯的错误。

1.6 兄弟进程间的管道通信

兄弟进程通信,其实也是通过父进程搭桥。流程是:

  1. 父进程创建管道。
  2. 父进程fork两次,生成两个子进程。
  3. 父进程关闭读写端,只做管理。
  4. 子进程A关闭读端,子进程B关闭写端。

说白了,还是那套「关掉不用的端」的逻辑。兄弟进程之间没有直接血缘关系,但通过父进程创建的管道,它们就能通信了。

1.7 管道的大小与容量限制

管道缓冲区有多大?这取决于系统。你可以用 fpathconf() 来查:

long size = fpathconf(fd[0], _PC_PIPE_BUF);
printf("PIPE_BUF = %ld\n", size);

在Linux上,PIPE_BUF 通常是4096字节。但注意,这是保证原子写入的最大值。如果你一次写入超过 PIPE_BUF 的数据,内核可能会分多次写入,读端就可能读到不完整的数据。

管道的总容量(不是 PIPE_BUF)在Linux 2.6.11之后是65536字节,也就是64KB。你可以通过 fcntl(fd, F_SETPIPE_SZ, size) 来调整,但一般不建议动它。

属性 说明
PIPE_BUF 4096字节(通常) 原子写入的最大值
总容量 65536字节(默认) 可通过fcntl调整
最大容量 受限于系统内存 有上限,但一般够用

1.8 管道的优缺点

聊了这么多,咱们总结一下管道的优缺点。

优点:

  • 使用简单。一个 pipe() 加一个 fork() 就能搞定。
  • 内核自动同步。你不用自己加锁。
  • 数据流自然。适合生产者-消费者模型。
  • 轻量。不涉及文件系统操作(无名管道)。

缺点:

  • 半双工。只能单向流动。
  • 只能用于有亲缘关系的进程。
  • 数据是字节流,没有边界。应用层需要自己定义协议。
  • 容量有限。大数据量传输可能阻塞。
  • 多读多写时,原子性只保证到 PIPE_BUF 大小。

我的建议:管道最适合做「小数据量、低频次、有亲缘关系」的进程间通信。如果你需要高频大数据量传输,或者需要任意进程间通信,可以考虑共享内存或消息队列。但作为入门,管道是理解IPC概念的最好起点。

知识体系总览

下面这张图,帮你把本章的知识点串起来:

无名管道知识体系 无名管道 (pipe) 创建:pipe() 函数 读写特性 阻塞与非阻塞 pipefd[0] 读端 pipefd[1] 写端 字节流,无消息边界 读空阻塞 / 写满阻塞 写端关闭 → EOF 读端关闭 → SIGPIPE 默认阻塞模式 fcntl() 设置非阻塞 非阻塞返回 EAGAIN

这张图把本章的核心知识点都串起来了。从创建管道开始,到读写特性,再到阻塞控制,每一步都有对应的系统调用和注意事项。你可以把它当作学习地图,哪里不会点哪里。


好了,关于无名管道,咱们就聊到这儿。内容不少,但都是干货。你回去可以自己动手写几个demo试试,比如让父子进程互相发消息,或者试试非阻塞模式下的行为。实践出真知,代码跑一遍比看十遍都管用。

公众号:蓝海资料掘金营,微信deep3321