Linux进程间通信:命名管道(FIFO)详解
一、什么是命名管道(FIFO)
命名管道是一种特殊的文件类型,允许无亲缘关系的进程之间进行通信。与匿名管道(pipe)不同,FIFO在文件系统中有一个真实的文件名,任何知道该文件名的进程都可以访问它。
二、FIFO的核心特性
持久性:在文件系统中可见,直到被显式删除
半双工:数据单向流动(需要两个FIFO实现双向通信)
阻塞I/O:默认读写操作会阻塞,直到另一端有操作
字节流:数据以字节流形式传输,无消息边界
三、创建与使用FIFO
创建FIFO的两种方式
1. 命令行创建
# 创建命名管道
mkfifo /tmp/myfifo
# 查看文件类型
ls -l /tmp/myfifo
# 输出:prw-r--r-- 1 user user 0 Jan 1 12:00 /tmp/myfifo
# 'p' 表示管道文件
2. 程序内创建
#include <sys/types.h>
#include <sys/stat.h>
// 创建FIFO,mode为权限(如0666)
int mkfifo(const char *pathname, mode_t mode);
四、完整示例:生产者-消费者模型
生产者程序(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define FIFO_FILE "/tmp/myfifo"
#define BUFFER_SIZE 1024
int main() {
int fd;
char buff[BUFFER_SIZE];
// 创建FIFO(如果不存在)
if (mkfifo(FIFO_FILE, 0666) == -1) {
// 可能已存在,继续尝试打开
}
printf("生产者启动,等待消费者连接...\n");
// 打开FIFO进行写入(会阻塞直到消费者打开读取端)
fd = open(FIFO_FILE, O_WRONLY);
while (1) {
printf("输入消息 (输入'quit'退出): ");
fgets(buff, BUFFER_SIZE, stdin);
// 写入FIFO
write(fd, buff, strlen(buff)+1);
if (strncmp(buff, "quit", 4) == 0) {
break;
}
}
close(fd);
// unlink(FIFO_FILE); // 可选:删除FIFO文件
return 0;
}
消费者程序(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define FIFO_FILE "/tmp/myfifo"
#define BUFFER_SIZE 1024
int main() {
int fd;
char buff[BUFFER_SIZE];
printf("消费者启动,等待数据...\n");
// 打开FIFO进行读取(会阻塞直到生产者打开写入端)
fd = open(FIFO_FILE, O_RDONLY);
while (1) {
// 从FIFO读取数据
int bytes_read = read(fd, buff, BUFFER_SIZE);
if (bytes_read > 0) {
printf("收到消息: %s", buff);
if (strncmp(buff, "quit", 4) == 0) {
break;
}
}
}
close(fd);
return 0;
}
五、编译与测试
# 编译
gcc writer.c -o writer
gcc reader.c -o reader
# 终端1:运行消费者
./reader
# 终端2:运行生产者
./writer
六、高级用法与技巧
1. 非阻塞模式
// 非阻塞方式打开FIFO
int fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK);
// 或通过fcntl设置
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
2. 多客户端服务器模型
// 服务器处理多个客户端
#include <errno.h>
void server_handle_multiple_clients() {
int fd, bytes;
char buffer[256];
mkfifo("/tmp/server_fifo", 0666);
while (1) {
// 打开服务器FIFO(阻塞等待客户端)
fd = open("/tmp/server_fifo", O_RDONLY);
// 读取客户端请求
while ((bytes = read(fd, buffer, sizeof(buffer))) > 0) {
printf("收到请求: %s\n", buffer);
// 创建唯一的响应FIFO名
char response_fifo[50];
sprintf(response_fifo, "/tmp/client_%s_fifo", buffer);
// 打开客户端FIFO并发送响应
int resp_fd = open(response_fifo, O_WRONLY);
write(resp_fd, "响应数据", 10);
close(resp_fd);
}
close(fd);
}
}
3. 双向通信实现
// 进程A创建两个FIFO
mkfifo("/tmp/AtoB", 0666);
mkfifo("/tmp/BtoA", 0666);
// 进程A:向AtoB写,从BtoA读
// 进程B:向BtoA写,从AtoB读
七、FIFO的限制与注意事项
容量限制:Linux默认64KB(可配置)
原子性:写入数据量≤PIPE_BUF(通常4096字节)时保证原子性
阻塞问题:
- 读空FIFO会阻塞,直到有数据写入
- 写满FIFO会阻塞,直到有数据被读取
- 所有写入端关闭后,读取返回0
- 所有读取端关闭后,写入会触发SIGPIPE信号
清理问题:FIFO文件需要手动删除
// 删除FIFO文件
unlink("/tmp/myfifo");
八、与其他IPC方式的对比
特性
命名管道
匿名管道
消息队列
共享内存
无亲缘关系
✅
❌
✅
✅
文件系统可见
✅
❌
❌
❌
通信方向
半双工
半双工
全双工
全双工
性能
中
高
中
高
数据格式
字节流
字节流
消息
字节流
九、实际应用场景
Shell命令协同
# 终端1:创建并写入FIFO
mkfifo /tmp/myfifo
echo "Hello World" > /tmp/myfifo
终端2:从FIFO读取
cat < /tmp/myfifo
2. **日志收集系统**
```c
// 多个进程向同一个FIFO写入日志
// 日志收集进程从FIFO读取并统一处理
进程池任务分发// 主进程向FIFO写入任务
// 工作进程从FIFO读取并执行
十、最佳实践
错误处理:始终检查系统调用返回值
信号处理:处理SIGPIPE等可能信号
资源清理:程序退出前删除FIFO文件
权限控制:设置适当的文件权限(如0600)
超时机制:使用select/poll防止永久阻塞
// 使用poll实现超时读取
struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN;
int ret = poll(fds, 1, 3000); // 3秒超时
if (ret > 0 && (fds[0].revents & POLLIN)) {
read(fd, buffer, size);
}
命名管道作为一种简单有效的进程间通信机制,特别适用于单向数据流和无亲缘关系进程的通信场景。虽然功能相对简单,但在许多系统编程场景中仍然非常实用。