管道通信:匿名管道与命名管道的实战

大家好,我是你们的老朋友。今天我们来聊聊进程间通信里最经典、也最接地气的方式——管道。说白了,管道就是一根“数据水管”,一头接一个进程,另一头接另一个进程。数据从一头流进去,从另一头流出来。嗯,就这么简单。

但简单归简单,用起来坑可不少。我最早接触管道是在一个嵌入式监控项目里,当时需要把传感器采集进程的数据实时传给日志记录进程。一开始我用了共享内存,结果同步问题搞得我焦头烂额。后来换成管道,世界清净了。所以,管道这东西,你值得拥有。

匿名管道:父子进程的“私聊”通道

匿名管道,顾名思义,没有名字。它只能用于有亲缘关系的进程之间,比如父子进程。为什么?因为匿名管道是通过文件描述符传递的,子进程会继承父进程的文件描述符表,所以才能共享这根管子。

创建匿名管道的函数是 pipe()。原型很简单:

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

pipefd[0] 是读端,pipefd[1] 是写端。记住这个顺序,我当年就搞反过,结果数据死活读不出来,排查了半天才发现是读写端搞混了。你想想看,一个管子你拿着出口当入口,水能流过去才怪。

核心要点: 匿名管道是半双工的,数据只能单向流动。如果你需要双向通信,得创建两个管道。

来看一个完整的例子:父进程写数据,子进程读数据。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    pid_t pid;
    char buf[100];

    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }

    pid = fork();
    if (pid == 0) {
        // 子进程:关闭写端,读取数据
        close(fd[1]);
        read(fd[0], buf, sizeof(buf));
        printf("子进程收到:%s\n", buf);
        close(fd[0]);
    } else if (pid > 0) {
        // 父进程:关闭读端,写入数据
        close(fd[0]);
        char *msg = "Hello from parent!";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
        wait(NULL);
    } else {
        perror("fork");
        return 1;
    }
    return 0;
}

这里有个细节:用完的端口一定要关闭。不关的话,读端会一直等着数据,因为写端没关,系统认为还可能有人写。我曾经在一个循环里忘了关写端,结果子进程的 read() 永远阻塞,整个程序卡死了。嗯,血的教训。

避坑指南: 在 fork 之后,父子进程各自关闭不需要的端口。父进程关读端,子进程关写端。这样能避免很多莫名其妙的问题。

命名管道:让任意进程“群聊”

匿名管道只能用于亲缘进程,那没有血缘关系的进程怎么通信?命名管道(FIFO)就是干这个的。它有一个名字,存在于文件系统中,任何进程只要知道这个名字,就能打开它进行通信。

创建命名管道用 mkfifo()

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

pathname 是管道文件的路径,mode 是权限,跟 open() 一样。创建成功后,你就可以用 open()read()write() 来操作它了。

注意: 命名管道是阻塞的。默认情况下,读端会阻塞直到有写端打开,写端也会阻塞直到有读端打开。如果你不想阻塞,可以用 O_NONBLOCK 标志打开。

来看一个写端和读端分离的例子。先写一个写数据的程序:

// writer.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    mkfifo(fifo_path, 0666);

    int fd = open(fifo_path, O_WRONLY);
    write(fd, "Hello from writer!\n", 19);
    close(fd);
    return 0;
}

再写一个读数据的程序:

// reader.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    int fd = open(fifo_path, O_RDONLY);
    char buf[100];
    read(fd, buf, sizeof(buf));
    printf("Reader 收到:%s", buf);
    close(fd);
    return 0;
}

先运行 writer,再运行 reader。你会发现 writer 会阻塞,直到 reader 打开读端。这就是命名管道的同步特性。我个人习惯在写端先创建 FIFO,然后以只写方式打开,读端以只读方式打开。顺序无所谓,但一定要保证两端都打开才能传输数据。

匿名管道 vs 命名管道:一张表说清楚

特性 匿名管道 命名管道
适用范围 有亲缘关系的进程 任意进程
创建方式 pipe() mkfifo()
文件系统可见 不可见 可见,有路径名
通信方向 半双工 半双工(可双向,需两个)
阻塞特性 默认阻塞 默认阻塞,可设置非阻塞
生命周期 随进程结束而销毁 手动删除(unlink()

知识体系与核心逻辑

下面这张图帮你理清管道通信的整体脉络。我画图时习惯把重点放在“创建-连接-传输-关闭”这条主线上,你看完应该能一目了然。

管道通信核心逻辑 匿名管道 pipe() 创建 fork() 子进程继承 fd 关闭不用的端口 read/write 传输数据 close() 关闭 命名管道 mkfifo() 创建 open() 打开(阻塞等待) 两端都打开后传输 read/write 传输数据 close() + unlink() 删除 核心:创建 → 连接 → 传输 → 关闭,注意阻塞与同步

实战中的坑与经验

讲两个我踩过的坑吧。

第一个坑:管道缓冲区大小。 匿名管道的缓冲区默认是 65536 字节(64KB)。如果你一次写入超过这个量,写操作会阻塞,直到读端把数据读走。我曾经在一个高速数据采集程序里,写入速度远大于读取速度,结果管道满了,写端卡死。后来我改用双缓冲 + 非阻塞写才解决。

第二个坑:命名管道的清理。 命名管道文件不会自动删除。程序退出后,那个 FIFO 文件还留在文件系统里。下次运行前如果没删,mkfifo() 会失败(返回 -1,errno 设为 EEXIST)。我的习惯是在创建前先 unlink() 一下,或者用 access() 检查是否存在。

小技巧:ulimit -p 可以查看当前系统的管道缓冲区大小。如果你需要调整,可以修改内核参数,但一般不建议,64KB 对大多数场景够用了。

好了,关于管道通信就聊这么多。匿名管道适合父子进程间的简单通信,命名管道则让不相关的进程也能轻松对话。记住一点:管道是流式的,没有消息边界。如果你需要区分消息,得自己加分隔符或者固定长度。嗯,这个我们后面讲消息队列时会再提到。


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