一、有名管道(FIFO)—— 让不相关的进程也能聊天

大家好,欢迎来到《C语言进程间通信实战》的第一章。今天咱们聊聊有名管道,也就是 FIFO。

无名管道大家可能听说过,它只能在有亲缘关系的进程间用,比如父子进程。但实际项目中,很多时候我们需要让两个毫不相干的进程通信——比如一个后台服务和一个监控程序。这时候,无名管道就歇菜了。怎么办?有名管道就是干这个的。

1.1 什么是有名管道?

有名管道,说白了就是一个有名字的管道文件。它存在于文件系统中,任何进程只要知道这个名字,就能打开它、读写它。我刚开始接触这个概念时,觉得它挺像「文件版的管道」——只不过数据不落盘,全在内核缓冲区里流动。

它的本质和无名管道一样,都是内核维护的一块缓冲区。区别在于:有名管道有一个路径名,可以像普通文件一样被 open() 打开。但请注意,它并不是真正的磁盘文件,你往里面写数据,数据不会存到硬盘上。

核心要点:有名管道 = 有路径名的内核缓冲区。它让任意两个进程都能通过同一个路径名建立通信。

1.2 创建有名管道:mkfifo

创建有名管道有两种方式:命令行用 mkfifo 命令,代码里用 mkfifo() 函数。我个人习惯在代码里创建,这样更可控。

函数原型长这样:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname:管道路径,比如 /tmp/my_fifo
  • mode:权限位,和 open() 的 mode 一样,比如 0666
  • 返回值:成功返回 0,失败返回 -1

举个例子:

#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    
    // 创建有名管道,权限为 0666
    if (mkfifo(fifo_path, 0666) == -1) {
        perror("mkfifo");
        exit(EXIT_FAILURE);
    }
    
    printf("FIFO created: %s\n", fifo_path);
    return 0;
}

小提示:如果文件已存在,mkfifo() 会失败并设置 errno 为 EEXIST。所以实际项目中,我一般先检查文件是否存在,或者用 access() 判断一下再创建。

1.3 打开与关闭有名管道

创建好之后,怎么用?用 open() 打开,用 close() 关闭。和普通文件一样,但有个关键区别——打开方式决定了读写行为

打开模式有三种:

打开模式 说明 阻塞行为
O_RDONLY 只读打开 阻塞直到有写端打开
O_WRONLY 只写打开 阻塞直到有读端打开
O_RDWR 读写打开 不阻塞(但很少用,容易死锁)

嗯,这里要注意:默认情况下,open() 一个 FIFO 是阻塞的。什么意思?如果你只读打开,而没有任何进程以写方式打开这个 FIFO,那么 open() 会一直卡在那里,直到有人来写。反过来也一样。

我曾经在项目里踩过这个坑——调试了半天,发现进程卡在 open() 上不动了,就是因为写端还没启动。后来我加了个超时机制才搞定。

如果你想避免阻塞,可以加上 O_NONBLOCK 标志:

int fd = open("/tmp/my_fifo", O_RDONLY | O_NONBLOCK);
if (fd == -1) {
    perror("open");
    exit(EXIT_FAILURE);
}

非阻塞模式下,如果没有写端,open() 会立即返回成功,但后续 read() 会返回 0(没有数据)。

1.4 读写规则

有名管道的读写规则,说白了和无名管道一样:

  • 写端写入的数据,读端按先进先出的顺序读取
  • 数据是字节流,没有消息边界。你写 100 字节,读端可能一次读 50 字节,分两次读完
  • 写端关闭后,读端 read() 返回 0(类似读到文件尾)
  • 读端关闭后,写端继续写会收到 SIGPIPE 信号,进程默认终止

举个例子,写端代码:

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

int main() {
    int fd = open("/tmp/my_fifo", O_WRONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    
    const char *msg = "Hello from writer!";
    write(fd, msg, strlen(msg) + 1);  // 写入字符串,包含 '\0'
    
    close(fd);
    return 0;
}

读端代码:

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

int main() {
    int fd = open("/tmp/my_fifo", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    
    char buf[128];
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        printf("Received: %s\n", buf);
    }
    
    close(fd);
    return 0;
}

注意:write() 写入的数据量如果超过 PIPE_BUF(通常 4096 字节),写入操作可能不是原子的。多个写进程同时写时,数据可能会交错。所以建议每次写入不超过 PIPE_BUF 字节。

1.5 阻塞特性详解

阻塞特性是 FIFO 最让人头疼的地方,也是面试常考的点。我总结一下:

  • open() 阻塞:读端等待写端,写端等待读端。这是最常见的阻塞场景
  • read() 阻塞:如果 FIFO 为空,且写端仍打开,read() 会阻塞等待数据
  • write() 阻塞:如果 FIFO 已满(缓冲区大小有限),write() 会阻塞直到有空间

为什么会这样?因为 FIFO 本质上是一个内核缓冲区,大小有限(通常 64KB 左右)。当缓冲区满了,写端自然要等一等。当缓冲区空了,读端也要等一等。

我记得有一次做嵌入式项目,两个进程通过 FIFO 传传感器数据。写端每 10ms 写一次,读端每 100ms 读一次。结果读端总是读到旧数据,因为缓冲区没及时清空。后来我调整了读写频率,并在读端加了 O_NONBLOCK,问题才解决。

1.6 有名管道 vs 无名管道

这里我画了一张对比图,方便大家理解:

有名管道 vs 无名管道 无名管道 (pipe) • 只能在有亲缘关系的进程间使用 • 没有文件名,不存在于文件系统 • 通过 pipe() 系统调用创建 • 生命周期随进程 • 半双工通信(单向) • 使用简单,适合父子进程 有名管道 (FIFO) • 任意进程间都能通信 • 有路径名,存在于文件系统 • 通过 mkfifo() 创建 • 生命周期可持久(文件不删就在) • 半双工通信(单向) • 使用稍复杂,但更灵活 共同点:都是内核缓冲区,半双工,字节流,有容量限制

说白了,无名管道适合「一家人」内部通信,有名管道适合「陌生人」之间通信。你想想看,如果两个进程没有父子关系,无名管道根本用不了。这时候有名管道就是唯一的选择。

1.7 应用场景

在实际项目中,我见过有名管道用在以下几个地方:

  • 日志收集系统:多个业务进程往同一个 FIFO 写日志,一个日志守护进程统一读取并落盘
  • 客户端-服务器模型:服务器创建一个 FIFO,客户端连接后通过它发送请求
  • 嵌入式系统中的任务间通信:比如两个独立的任务模块,通过 FIFO 交换控制命令
  • 进程间同步:利用 FIFO 的阻塞特性,实现简单的同步机制

我记得在一个物联网网关项目中,设备端有 3 个独立进程分别采集温度、湿度和光照数据。它们各自往一个 FIFO 写数据,一个汇总进程读取并打包上传。这个方案简单可靠,比用共享内存加信号量省事多了。

1.8 注意事项

避坑指南——我曾经踩过的坑:

  • open() 阻塞问题:记得先启动读端,再启动写端。或者用 O_NONBLOCK 避免卡死
  • SIGPIPE 信号:如果读端关闭了,写端继续写会收到 SIGPIPE。要么忽略它,要么处理它
  • 原子写入限制:单次写入不要超过 PIPE_BUF(通常 4096 字节),否则多进程并发写会乱序
  • 清理残留 FIFO 文件:程序退出时记得 unlink() 删除 FIFO 文件,否则下次启动会报 EEXIST
  • 不要用 O_RDWR:虽然可以,但容易导致死锁。读端和写端最好分开进程

我的个人习惯:在写 FIFO 程序时,我一般会封装一个工具函数,自动处理 open() 的重试和超时。这样主逻辑就干净多了。

好了,第一章的内容就到这里。有名管道虽然简单,但用好了能解决很多实际问题。下一章我们聊聊消息队列,那又是另一种通信方式了。


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