管道通信:匿名管道与命名管道的实战
大家好,我是你们的老朋友。今天我们来聊聊进程间通信里最经典、也最接地气的方式——管道。说白了,管道就是一根“数据水管”,一头接一个进程,另一头接另一个进程。数据从一头流进去,从另一头流出来。嗯,就这么简单。
但简单归简单,用起来坑可不少。我最早接触管道是在一个嵌入式监控项目里,当时需要把传感器采集进程的数据实时传给日志记录进程。一开始我用了共享内存,结果同步问题搞得我焦头烂额。后来换成管道,世界清净了。所以,管道这东西,你值得拥有。
匿名管道:父子进程的“私聊”通道
匿名管道,顾名思义,没有名字。它只能用于有亲缘关系的进程之间,比如父子进程。为什么?因为匿名管道是通过文件描述符传递的,子进程会继承父进程的文件描述符表,所以才能共享这根管子。
创建匿名管道的函数是 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() 永远阻塞,整个程序卡死了。嗯,血的教训。
命名管道:让任意进程“群聊”
匿名管道只能用于亲缘进程,那没有血缘关系的进程怎么通信?命名管道(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()) |
知识体系与核心逻辑
下面这张图帮你理清管道通信的整体脉络。我画图时习惯把重点放在“创建-连接-传输-关闭”这条主线上,你看完应该能一目了然。
实战中的坑与经验
讲两个我踩过的坑吧。
第一个坑:管道缓冲区大小。 匿名管道的缓冲区默认是 65536 字节(64KB)。如果你一次写入超过这个量,写操作会阻塞,直到读端把数据读走。我曾经在一个高速数据采集程序里,写入速度远大于读取速度,结果管道满了,写端卡死。后来我改用双缓冲 + 非阻塞写才解决。
第二个坑:命名管道的清理。 命名管道文件不会自动删除。程序退出后,那个 FIFO 文件还留在文件系统里。下次运行前如果没删,mkfifo() 会失败(返回 -1,errno 设为 EEXIST)。我的习惯是在创建前先 unlink() 一下,或者用 access() 检查是否存在。
ulimit -p 可以查看当前系统的管道缓冲区大小。如果你需要调整,可以修改内核参数,但一般不建议,64KB 对大多数场景够用了。
好了,关于管道通信就聊这么多。匿名管道适合父子进程间的简单通信,命名管道则让不相关的进程也能轻松对话。记住一点:管道是流式的,没有消息边界。如果你需要区分消息,得自己加分隔符或者固定长度。嗯,这个我们后面讲消息队列时会再提到。