一、有名管道:让无关进程也能对话

管道这东西,大家应该不陌生。匿名管道只能在父子进程间用,说白了就是「血缘关系」限定。那没有亲缘关系的进程怎么通信?嗯,这就是有名管道(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 知识体系总览

下面这张图,帮你理清本章的核心逻辑:

有名管道(FIFO)知识体系 有名管道 FIFO 创建:mkfifo() 打开:open() 读写:read/write pathname 路径 mode 权限位 EEXIST 处理 O_RDONLY 阻塞 O_WRONLY 阻塞 O_NONBLOCK 非阻塞 原子性 < PIPE_BUF SIGPIPE 信号 read 返回 0 无亲缘关系进程通过 FIFO 路径名实现通信

1.6 避坑指南

最后,分享几个我踩过的坑:

  • FIFO 文件不会自动删除:进程结束后,FIFO 文件还在文件系统里。下次运行前记得清理,或者用 unlink() 删除。
  • 阻塞问题:open 阻塞是 FIFO 的一大特点,也是很多 bug 的来源。建议用 O_NONBLOCK 配合 select/poll 来管理。
  • 数据边界:FIFO 是字节流,没有消息边界。如果你需要区分消息,得自己加分隔符或者固定长度。
  • 权限问题:创建 FIFO 时注意 umask 的影响。我习惯在 mkfifo 后显式调用 chmod() 确保权限正确。

个人建议:如果你只是临时需要进程间通信,用匿名管道就够了。但如果你需要长期运行的、无亲缘关系的进程通信,FIFO 是个轻量级的好选择。当然,复杂场景下你可能要考虑消息队列或共享内存了。


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