一、有名管道:让无关进程也能对话
管道这东西,大家应该不陌生。匿名管道只能在父子进程间用,说白了就是「血缘关系」限定。那没有亲缘关系的进程怎么通信?嗯,这就是有名管道(FIFO)的用武之地了。
我个人习惯把有名管道理解成一个「文件形式的管道」。它在文件系统里有个名字,任何进程只要知道这个名字,就能打开它来读写数据。你想想看,这不就打破了血缘限制吗?
核心概念:有名管道(FIFO)是一种特殊的文件类型,它在文件系统中以文件名存在,通过 mkfifo 创建,任何进程都可以通过路径访问它。
1.1 mkfifo 函数:创建管道的起点
创建有名管道,最常用的就是 mkfifo 函数。它的原型很简单:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname 就是你要创建的 FIFO 文件路径。mode 是权限位,跟 open 函数的 mode 参数一样。返回值:成功返回 0,失败返回 -1 并设置 errno。
我在项目中遇到过一个问题:如果 FIFO 文件已经存在,mkfifo 会返回 -1,errno 是 EEXIST。所以创建前最好检查一下文件是否存在,或者直接用 access() 判断。
小技巧:你也可以用 mknod(pathname, S_IFIFO | mode, 0) 来创建 FIFO,效果一样。但我个人更推荐 mkfifo,语义更清晰。
1.2 FIFO 的创建与打开:像文件一样操作
创建完 FIFO 后,怎么打开它?用 open() 就行。没错,就是那个打开普通文件的 open 函数。
但这里有个坑——打开 FIFO 时,open 的行为取决于你指定的标志:
| 打开方式 | 行为说明 |
|---|---|
O_RDONLY |
阻塞直到有写端打开 |
O_WRONLY |
阻塞直到有读端打开 |
O_RDWR |
不阻塞,直接返回(但一般不推荐) |
O_NONBLOCK + O_RDONLY |
立即返回,即使没有写端 |
O_NONBLOCK + O_WRONLY |
如果没有读端,返回 -1,errno 为 ENXIO |
为什么会这样?因为 FIFO 是半双工的,读端和写端必须配对。open 阻塞就是在等对方出现。我曾经踩过这个坑:写了个服务端,open 时忘了开写端,结果一直卡在那不动。排查了半天才发现是 open 阻塞了。
// 创建 FIFO
if (mkfifo("/tmp/myfifo", 0666) == -1) {
perror("mkfifo");
exit(1);
}
// 打开读端(阻塞等待写端)
int fd = open("/tmp/myfifo", O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}
1.3 FIFO 的读写规则:数据流的规矩
FIFO 的读写规则,说白了就是「先进先出」。数据像水流一样,先写进去的先被读走。但有几个细节你得注意:
- 原子性:如果写入的数据小于
PIPE_BUF(通常是 4096 字节),write 操作是原子的。多个进程同时写不会互相穿插。 - 大于 PIPE_BUF:写入超过 PIPE_BUF 的数据,可能会被其他进程的写入打断,造成数据交错。
- 读端关闭:写端会收到 SIGPIPE 信号,write 返回 -1,errno 为 EPIPE。
- 写端关闭:读端 read 返回 0,表示文件结束。
注意:如果你用 O_RDWR 打开 FIFO,读端和写端是同一个进程,read 永远不会返回 0(因为写端一直开着)。这会导致一些奇怪的行为,我建议你尽量避免这种用法。
1.4 无亲缘关系进程通信:实战案例
好了,理论说完了,咱们来看个实际例子。假设有两个程序:一个发消息(writer),一个收消息(reader)。它们没有任何亲缘关系,通过 FIFO 通信。
writer.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
const char *fifo_path = "/tmp/chat_fifo";
// 创建 FIFO(如果不存在)
if (access(fifo_path, F_OK) == -1) {
if (mkfifo(fifo_path, 0666) == -1) {
perror("mkfifo");
exit(1);
}
}
// 打开写端
int fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}
char buf[256];
while (1) {
printf("输入消息: ");
fgets(buf, sizeof(buf), stdin);
write(fd, buf, strlen(buf) + 1);
}
close(fd);
return 0;
}
reader.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
const char *fifo_path = "/tmp/chat_fifo";
// 打开读端
int fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}
char buf[256];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
printf("收到: %s", buf);
}
close(fd);
return 0;
}
你看,这两个程序之间没有任何父子关系。只要它们都访问同一个 FIFO 文件,就能通信。这就是有名管道的魅力。
1.5 知识体系总览
下面这张图,帮你理清本章的核心逻辑:
1.6 避坑指南
最后,分享几个我踩过的坑:
- FIFO 文件不会自动删除:进程结束后,FIFO 文件还在文件系统里。下次运行前记得清理,或者用
unlink()删除。 - 阻塞问题:open 阻塞是 FIFO 的一大特点,也是很多 bug 的来源。建议用
O_NONBLOCK配合select/poll来管理。 - 数据边界:FIFO 是字节流,没有消息边界。如果你需要区分消息,得自己加分隔符或者固定长度。
- 权限问题:创建 FIFO 时注意 umask 的影响。我习惯在 mkfifo 后显式调用
chmod()确保权限正确。
个人建议:如果你只是临时需要进程间通信,用匿名管道就够了。但如果你需要长期运行的、无亲缘关系的进程通信,FIFO 是个轻量级的好选择。当然,复杂场景下你可能要考虑消息队列或共享内存了。