3、有名管道(FIFO):mkfifo()函数、FIFO文件特性、无亲缘关系进程通信、FIFO阻塞与非阻塞模式
好,咱们接着聊。上一章我们讲了匿名管道,它只能在父子进程或者兄弟进程之间用。说白了,就是得有血缘关系。那问题来了——两个毫不相干的进程,怎么通信?
答案就是:有名管道,也叫FIFO。
3.1 什么是FIFO?它和匿名管道有啥区别?
FIFO,全称是First In First Out,先进先出。它在文件系统里有一个真实的名字,是一个特殊的文件。你想想看,两个进程只要知道这个文件名,就能通过它来交换数据,管你什么亲缘关系。
我个人习惯把FIFO比作一个「公共信箱」。你往里面塞信,我来取。谁都可以塞,谁都可以取,只要大家约好使用同一个信箱。
| 特性 | 匿名管道 | 有名管道(FIFO) |
|---|---|---|
| 文件系统中有名字 | 无 | 有 |
| 通信进程关系 | 必须有亲缘关系 | 无亲缘关系也可以 |
| 创建方式 | pipe()系统调用 | mkfifo()系统调用 |
| 生命周期 | 随进程结束而消失 | 随文件删除而消失 |
3.2 mkfifo()函数:创建FIFO的钥匙
创建FIFO,核心就一个函数:mkfifo()。它的原型长这样:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数很简单:
pathname:你要创建的FIFO文件路径。比如"/tmp/my_fifo"。mode:权限位,和文件权限一样。比如0666表示所有人都能读写。
返回值:成功返回0,失败返回-1并设置errno。
注意:mkfifo()只是创建了一个特殊的文件节点。它不会分配任何磁盘空间,只是在内核中注册了一个管道对象。所以创建速度非常快。
嗯,这里要注意一点:如果FIFO文件已经存在,mkfifo()会失败,返回EEXIST错误。所以实际项目中,我一般会先检查文件是否存在:
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
int main() {
const char *fifo_path = "/tmp/my_fifo";
// 检查文件是否已存在
if (access(fifo_path, F_OK) == -1) {
// 文件不存在,创建FIFO
if (mkfifo(fifo_path, 0666) == -1) {
perror("mkfifo");
return 1;
}
printf("FIFO created: %s\n", fifo_path);
} else {
printf("FIFO already exists: %s\n", fifo_path);
}
return 0;
}
3.3 FIFO文件特性:看着像文件,用起来像管道
FIFO在文件系统里看起来就是一个普通文件。你用ls -l查看,它的类型标识是p(pipe):
$ ls -l /tmp/my_fifo
prw-rw-r-- 1 user user 0 Jan 1 12:00 /tmp/my_fifo
看到那个p了吗?这就是FIFO的标志。文件大小永远是0,因为它不存储数据,数据在内核缓冲区里流动。
我在项目中遇到过一个问题:有同事把FIFO当成普通文件来读写,结果发现数据读不出来。为什么?因为FIFO的数据是流式的,你读走了就没了,不能像普通文件那样反复读取。
避坑指南:我曾经在调试一个服务时,发现FIFO里写入的数据莫名其妙丢失了。查了半天,原来是另一个进程也在读同一个FIFO,把数据抢走了。记住:FIFO的数据是「一次性的」,一个进程读走,其他进程就没了。
3.4 无亲缘关系进程通信:两个陌生人怎么聊天?
这才是FIFO的真正用武之地。假设我们有两个程序:一个写数据(生产者),一个读数据(消费者)。它们没有任何关系,各自独立运行。
写端程序(writer.c):
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *fifo_path = "/tmp/chat_fifo";
// 创建FIFO(如果不存在)
mkfifo(fifo_path, 0666);
// 打开FIFO(写模式)
int fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open");
return 1;
}
const char *msg = "Hello from writer!";
write(fd, msg, strlen(msg) + 1);
printf("Sent: %s\n", msg);
close(fd);
return 0;
}
读端程序(reader.c):
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
const char *fifo_path = "/tmp/chat_fifo";
// 打开FIFO(读模式)
int fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buf[256];
read(fd, buf, sizeof(buf));
printf("Received: %s\n", buf);
close(fd);
return 0;
}
你看,这两个程序之间没有任何血缘关系。只要它们都访问同一个FIFO文件,就能通信。这就是FIFO的魅力。
3.5 FIFO阻塞与非阻塞模式:等还是不等?
这是FIFO里最容易踩坑的地方。我刚开始用FIFO时,就被阻塞问题折腾了好几天。
默认行为:阻塞模式
当你用open()打开FIFO时,默认是阻塞的。什么意思?
- 如果以读模式打开,open()会阻塞,直到有写端打开同一个FIFO。
- 如果以写模式打开,open()会阻塞,直到有读端打开同一个FIFO。
为什么会这样?因为FIFO需要两端都准备好,数据才能流动。你想想看,如果只有写端没有读端,数据写进去给谁看?
非阻塞模式:O_NONBLOCK
如果你不想让open()阻塞,可以加上O_NONBLOCK标志:
// 非阻塞读模式
int fd = open(fifo_path, O_RDONLY | O_NONBLOCK);
// 非阻塞写模式
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
非阻塞模式下:
- 读端open()会立即返回,即使没有写端。
- 写端open()会立即返回,但如果没有读端,open()会失败(返回-1,errno为ENXIO)。
个人经验:我建议在实际项目中,读端用阻塞模式,写端用非阻塞模式。为什么?因为读端通常需要等待数据,阻塞更自然;而写端如果发现没有读端,可以立即报错或重试,而不是卡死在那里。
读写操作的阻塞行为:
| 操作 | 阻塞模式 | 非阻塞模式 |
|---|---|---|
| read() | 如果没有数据,阻塞等待 | 如果没有数据,立即返回-1(EAGAIN) |
| write() | 如果缓冲区满,阻塞等待 | 如果缓冲区满,立即返回-1(EAGAIN) |
我记得有一次调试一个数据采集程序,写端疯狂往FIFO里写数据,读端处理不过来。结果写端被阻塞了,整个程序卡住。后来改成非阻塞写模式,配合select()轮询,问题就解决了。
避坑指南:我曾经在线上环境遇到过一个诡异的问题——FIFO的写端进程莫名其妙地挂起了。查了半天,发现是读端进程崩溃退出了,但写端还在阻塞等待。因为没有读端,FIFO的缓冲区满了,写端就永远阻塞在那里。所以,生产环境中一定要考虑读端异常退出的情况,建议加上超时机制或者使用非阻塞模式。
3.6 实战建议:FIFO的正确打开方式
说了这么多,总结几条我自己的经验:
- 创建时检查错误:mkfifo()可能因为权限、路径等问题失败,一定要检查返回值。
- 明确阻塞策略:根据业务场景选择阻塞还是非阻塞,不要想当然。
- 考虑进程退出:读端或写端退出时,另一端要能感知到。read()返回0表示对端关闭。
- 数据量控制:FIFO的内核缓冲区默认是64KB(不同系统可能不同),不要一次写入太多数据。
- 清理FIFO文件:程序退出时,记得用unlink()删除FIFO文件,避免留下垃圾文件。
好了,这一章就到这里。FIFO虽然简单,但用好了能解决很多实际问题。下一章我们聊聊消息队列,那又是另一种玩法了。