Linux 下的管道通信大家都不陌生,但匿名管道只能父子进程用,碰上没血缘关系的进程就抓瞎了。命名管道(FIFO)就是为这种事准备的——它给管道一个文件系统入口,任何进程只要知道路径就能通信。
这篇文章记录我使用 FIFO 的过程,包括创建、通信和那些容易掉的坑。
命名管道是什么?
命名管道本质上和匿名管道一样,都是内核缓冲区的字节流,半双工(单向流动)。区别在于它有一个'文件'挂在文件系统上,类型标识为 p。这个文件不存数据,只是一个入口。因为有了这个可见的路径,无关进程就能通过 open 获取读写描述符,直接通信。

关键特性:
- 半双工,想双向就得建两个 FIFO。
- 打开时默认阻塞:读端打开会等写端,写端会等读端,除非设置 O_NONBLOCK。
- 进程退出后 FIFO 文件还在,得手动 unlink 删除。
- 跨进程通信,跟血缘没关系。
创建命名管道
两种方式:命令行 mkfifo 命令,或者代码里调用 mkfifo 函数。
命令行方式
mkfifo myfifo
ls -l myfifo # 看到文件类型是 p,大小永远是 0
你往里面写多少数据,文件大小都不变,证实它只是个标识。
代码方式(mkfifo 函数)
#include <sys/stat.h>
#include <sys/types.h>
int mkfifo(const char *pathname, mode_t mode);
创建一个最小例子:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#define FIFO_PATH "./myfifo"
int main() {
int ret = mkfifo(FIFO_PATH, 0644);
if (ret == -1) {
if (errno != EEXIST) {
perror("mkfifo error");
return 1;
}
printf("FIFO already exists\n");
} else {
printf("FIFO created\n");
}
return 0;
}
打开 FIFO 的规则
用 open 打开时行为比较特殊,核心是读端和写端必须'配对'才能继续:
- 只读打开(O_RDONLY)默认阻塞,直到有人用只写打开。
- 只写打开(O_WRONLY)默认阻塞,直到有人用只读打开。
- 读写模式(O_RDWR)不阻塞,但通常不建议用,容易搞乱通信方向。
实际使用中,读端就 O_RDONLY,写端就 O_WRONLY,清晰明了。
写个例子:服务端与客户端通信
下面用 C++ 写一个简单的 server 和 client,通过 FIFO 传递字符串。
公共头文件 comm.h
#pragma once
#include <string>
const std::string fifoname="fifo";
Makefile
all: client server
client: client.cpp
g++ -o $@ $^ -std=c++11
server: server.cpp
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f client server
服务端 server.cpp
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "comm.h"
int main() {
// 1. 创建管道
umask(0);
int n = mkfifo(fifoname.c_str(), 0666);
if (n < 0) {
perror("mkfifo");
return 1;
}
// 2. 只读打开
int rfd = open(fifoname.c_str(), O_RDONLY);
if (rfd < 0) {
perror("open");
return 2;
}
char inbuffer[1024];
while (true) {
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0) {
inbuffer[n] = 0;
std::cout << "client say# " << inbuffer << std::endl;
} else if (n == 0) {
// 写端关闭
break;
} else {
perror("read");
break;
}
}
close(rfd);
unlink(fifoname.c_str()); // 删掉 FIFO 文件
return 0;
}
客户端 client.cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.h"
int main() {
int wfd = open(fifoname.c_str(), O_WRONLY);
if (wfd < 0) {
perror("open");
return 1;
}
std::string outstring;
while (true) {
std::cout << "Please Enter@ ";
std::cin >> outstring;
write(wfd, outstring.c_str(), outstring.size());
// 不用写 '\0'
}
close(wfd);
return 0;
}
编译运行:先执行 make,然后一个终端运行 ./server,另一个终端 ./client,输入内容就能看到服务端打印。
实际用到的地方
我刚接触 IPC 时,FIFO 常用来做日志服务的前端交互。后台守护进程开一个 FIFO 监听,工具程序往里写命令,控制日志级别或输出目标,简单又可靠。当然,两个独立服务之间传数据也能用它,复杂场景就得上消息队列了。
容易掉的坑
- 阻塞问题:忘了设 O_NONBLOCK,程序启动就卡住,还以为是 bug。打开前先确认对面会来。
- SIGPIPE 信号:读端关闭后写端继续写,进程会收到 SIGPIPE,默认动作是终止。可以忽略信号或处理它,但最好保证读端存活。
- 文件残留:每次创建前看看文件在不在,或者退出时用 unlink 清掉。长期运行的环境里,/tmp 下可能一堆 FIFO。
- 路径统一:读写双方路径必须完全一致,一个用相对路径一个用绝对路径就会找不到。
- 权限:mkfifo 给的 mode 太低,其他用户就打不开。0666 一般够用。
- 字节流边界:TCP 也有类似问题,每次读的数据可能不是完整的消息,需要自己定界。这里只是简单示例,读固定大小缓冲区,实际项目注意处理。
写完这些,FIFO 的基本用法就差不多了。它本质不复杂,知道它怎么工作的,再加上留意上面几个坑,完全可以应付一般的跨进程通信需求。后面如果想效率更高或者需要双向多路,可以再看看消息队列和共享内存。


