Linux管道(PIPE/FIFO)
进程间通信(IPC)概览
在Linux/Unix系统中,进程间通信(Inter-Process Communication, IPC)方式多样,各有适用场景,具体分类如下:
- 管道
- 匿名管道(pipe):适用于亲缘关系进程间的、一对一的通信
- 具名管道(fifo):适用于任何进程间的一对一、多对一的通信
- 套接字(socket):适用于跨网络的进程间通信
- 信号:异步通信方式
- system-V IPC对象
- 共享内存:效率最高的通信方式
- 消息队列:相当于带标签的增强版管道
- 信号量组(信号灯):用来协调进程间或线程间的执行进度
- POSIX信号量
- POSIX匿名信号量:适用于多线程,参数简单,接口明晰
- POSIX具名信号量:适用于多进程,参数简单,接口明晰
这些通信机制统称IPC,需根据实际场景选择合适的方式。
匿名管道(PIPE)
基本逻辑
不管是匿名管道还是具名管道,在Linux系统下都属于文件的范畴,区别是匿名管道没有名称,因此无法使用open创建或打开,事实上匿名管道有自己独特的创建接口,但其读写方式与普通的文件一样,支持read()/write()操作。
管道文件事实上还包括网络编程中的核心概念套接字,所谓的管道指的是这些文件不能进行“定位”,只能顺序对其读写数据,就像一根水管,拧开水龙头不断读取,就可以源源不断读到水管中的数据,但如果没有水出来那只能继续等待,不能试图“跳过”部分文件去读写水管的中间地带,这是管道的最基本的特性。
函数接口
创建匿名管道的函数接口:
#include <unistd.h>
int pipe( int fd[2] ); // 返回值:成功返回0,失败返回-1
关键注意事项
- 文件描述符:数组
fd需至少包含2个整型元素,分别对应:fd[0]:专用于读操作fd[1]:专用于写操作
- 通信范围:仅支持亲缘进程间通信(如父子、兄弟进程),因无文件名,非亲缘进程无法获取其描述符(描述符需通过继承传递)
- 写操作限制:不可多个进程同时对匿名管道写操作,否则会导致数据覆盖
使用示例(父子进程通信)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// 父进程:写数据到管道
void ParentProcess(int fd_write) {
char SendMsg[32];
while (1) {
bzero(SendMsg, sizeof(SendMsg)); // 清空缓冲区
fgets(SendMsg, sizeof(SendMsg), stdin); // 从标准输入获取数据
int ret_val = write(fd_write, SendMsg, strlen(SendMsg)); // 写入管道
printf("【%d】发送了%d字节数据..\n", getpid(), ret_val);
}
}
// 子进程:从管道读数据
void ChildProcess(int fd_read) {
char RecvMsg[32];
while (1) {
bzero(RecvMsg, sizeof(RecvMsg)); // 清空缓冲区
int ret_val = read(fd_read, RecvMsg, sizeof(RecvMsg)); // 读取管道
printf("【%d】收到%d字节数据:%s\n", getpid(), ret_val, RecvMsg);
}
}
int main(int argc, char const *argv[]) {
int pipefd[2]; // 0:读端,1:写端
// 创建匿名管道
if (pipe(pipefd)) {
perror("pipe error");
return -1;
}
// 创建子进程(子进程继承管道描述符)
pid_t tid = fork();
if (tid > 0) {
/* 父进程:使用写端 */
ParentProcess(pipefd[1]);
} else if (tid == 0) {
/* 子进程:使用读端 */
ChildProcess(pipefd[0]);
}
return 0;
}
具名管道(FIFO)
概述
具名管道(FIFO,First In First Out)是匿名管道的增强版,核心特性:
- 外在形态接近普通文件:有文件名、可通过
open()打开、支持read()/write()操作 - 本质仍为管道:不可定位(不支持
lseek())、遵循先进先出(FIFO)逻辑 - 关键优势:支持任意进程间通信(无需亲缘关系)、支持多路同时写入(写入原子性,数据不丢失)
函数接口
创建具名管道的函数接口:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 返回值:成功返回0,失败返回-1
关键注意事项
pathname:具名管道的路径+文件名(需创建在Linux系统目录下,不可在Windows共享文件夹创建,因Windows不支持管道文件)mode:文件权限模式(与open()一致,如0666),实际权限受系统umask影响(最终权限 = mode & ~umask)- 打开方式:支持
O_RDONLY(读)、O_WRONLY(写)、O_RDWR(读写),其中O_RDWR可避免“等待对方打开”的阻塞
使用示例
发送端示例(写FIFO)
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[]) {
// 打开具名管道(O_RDWR模式可立即打开,无需等待读端)
int fd = open("/home/even/myfifo", O_RDWR);
if (fd < 0) {
perror("open fifo error");
return -1;
}
perror("open fifo ");
char SendMsg[32] = {0};
while (1) {
bzero(SendMsg, sizeof(SendMsg)); // 清空缓冲区
fgets(SendMsg, sizeof(SendMsg), stdin); // 从标准输入获取数据
int ret_val = write(fd, SendMsg, strlen(SendMsg)); // 写入FIFO
printf("写入%d字节数据..\n", ret_val);
}
close(fd); // 关闭文件描述符,释放资源
return 0;
}
接收端示例(读FIFO)
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[]) {
// 打开具名管道
int fd = open("/home/even/myfifo", O_RDWR);
if (fd < 0) {
perror("open fifo error");
return -1;
}
perror("open fifo ");
char RecvMsg[32] = {0};
while (1) {
bzero(RecvMsg, sizeof(RecvMsg)); // 清空缓冲区
int ret_val = read(fd, RecvMsg, sizeof(RecvMsg)); // 读取FIFO
printf("读取%d字节数据:%s\n", ret_val, RecvMsg);
}
close(fd); // 关闭文件描述符,释放资源
return 0;
}
【拓展】FIFO实现日志服务程序
核心思路
利用FIFO的写入原子性,解决多个无关联进程同时写日志导致的数据混乱问题:
- 多个进程(P1~P5)无需协调,直接向FIFO写入日志数据
- 一个专门的日志服务器进程,从FIFO读取数据并写入日志文件
- FIFO保证每个进程的写入操作原子性,数据不丢失、不覆盖
关键优势
- 简化开发:业务进程无需关注日志同步(无需互斥锁、信号量)
- 数据安全:FIFO写入原子性保证日志完整
- 解耦:业务进程与日志写入进程分离,便于维护
管道的读写特性
当我们对一个管道文件(包括匿名管道、具名管道和网络socket)进行读写操作时,我们需要知道将会发生什么,比如读一个空管道会怎么样?对一个缓冲区已满的管道执行写入操作会怎么样等等,可以对这些读写操作做一个统一的整理。
术语约定
- 读者:对管道拥有读权限的进程(非“正在读”的进程)
- 写者:对管道拥有写权限的进程(非“正在写”的进程)
- 核心规则:所谓的读者、写者不是只正在读或者正在写的进程,而是只要拥有读写权限就称为管道的读者写者,当只有读者或者只有写者时,管道文件不支持单方向打开(只有同时出现读者和写者双方时才能被打开)!!
读写特性对照表
| 场景 | 操作结果 | 备注 |
|---|---|---|
| 读空管道,且存在写者 | 阻塞(等待写者写入数据) | 默认阻塞模式 |
| 读空管道,且无写者 | 返回0(类似读普通文件到末尾) | - |
| 写满管道,且存在读者 | 阻塞(等待读者读取数据) | 默认阻塞模式 |
| 写管道,且无读者 | 触发SIGPIPE信号(默认终止进程) | 管道破裂 |
| 多个写者同时写FIFO | 写入原子性(数据不覆盖) | 匿名管道不支持多写者 |
| 关闭读端,写者继续写 | 触发SIGPIPE信号 | 匿名管道、FIFO均适用 |
| 关闭写端,读者继续读 | 读完剩余数据后返回0 | - |
示例说明
- 仅写者(关闭读端):
int fd[2];
pipe(fd);
close(fd[0]); // 关闭读端,仅保留写端(进程为写者)
- 既是读者也是写者(读写权限打开):
int fd = open("fifo", O_RDWR); // 同时拥有读写权限
管道的阻塞特性
基本概念
仔细看管道读写特性的表会发现,当试图读取一个空管道,或者试图写入一个缓冲区已满的管道时,读写操作默认会进入所谓“阻塞(se)”的状态。所谓的阻塞实际上就是系统将该进程挂起,等待资源就绪再继续调度的一种状态,这种阻塞的状态有利于系统中别的进程可高效地使用闲置CPU资源,提高系统的吞吐量。
- 阻塞:当读写操作无法立即完成时(如读空管道、写满管道),系统将进程挂起,等待资源就绪后再恢复执行
- 优势:提高CPU利用率,避免进程空循环浪费资源
- 默认行为:
- 普通文件:默认非阻塞(不可修改)
- 管道文件(PIPE/FIFO/socket):默认阻塞(可修改为非阻塞)
设置非阻塞模式
通过fcntl()函数修改文件描述符的属性,实现非阻塞模式:
#include <fcntl.h>
// 1. 获取当前文件描述符的属性
long flag = fcntl(fd[0], F_GETFL); // fd[0]为管道读端
// 2. 添加非阻塞属性(O_NONBLOCK)
flag |= O_NONBLOCK;
// 3. 重新设置文件描述符属性
fcntl(fd[0], F_SETFL, flag);
非阻塞模式的读写行为
| 场景 | 非阻塞模式结果 | 错误码 |
|---|---|---|
| 读空管道,且存在写者 | 返回-1(读取失败) | EAGAIN(资源暂时不可用) |
| 写满管道,且存在读者 | 返回-1(写入失败) | EAGAIN(资源暂时不可用) |
| 读空管道,且无写者 | 返回0(类似读文件到末尾) | - |
| 写管道,且无读者 | 触发SIGPIPE信号 | - |
管道关键补充知识点
管道缓冲区大小
- Linux系统中,管道默认缓冲区大小为64KB(可通过
fcntl()获取实际大小):#include <fcntl.h> int buf_size = fcntl(fd[0], F_GETPIPE_SZ); // 获取管道缓冲区大小 - 当写入数据量≤缓冲区大小时,写入操作可立即完成;超过则阻塞(默认模式)或返回失败(非阻塞模式)
管道破裂(SIGPIPE信号)
- 触发条件:写者向“无读者”的管道写入数据
- 默认行为:进程被终止
- 自定义处理:可通过
signal()函数捕获SIGPIPE信号,避免进程终止:#include <signal.h> void sig_handler(int sig) { printf("捕获到SIGPIPE信号(%d),管道破裂!\n", sig); } // 注册信号处理函数 signal(SIGPIPE, sig_handler);
匿名管道与具名管道对比表
| 特性 | 匿名管道(PIPE) | 具名管道(FIFO) |
|---|---|---|
| 适用进程 | 仅亲缘进程 | 任意进程 |
| 创建接口 | pipe() |
mkfifo() |
| 文件名 | 无 | 有(文件系统中存在节点) |
| 打开方式 | 继承描述符,无需open() |
需open()打开 |
| 多写者支持 | 不支持(数据覆盖) | 支持(写入原子性) |
| 通信方向 | 半双工(需双向通信需两个管道) | 半双工(同上) |
| 生命周期 | 随进程(进程退出后释放) | 随文件系统(需手动删除) |
管道的应用场景
- 父子进程间的数据传递(如命令行管道
cmd1 | cmd2,本质是匿名管道) - 无关联进程间的简单通信(具名管道)
- 日志收集系统(具名管道+日志服务器)
- 进程间的同步与协作(结合阻塞特性)
常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 打开FIFO时阻塞 | 仅以O_RDONLY/O_WRONLY打开,等待对方 | 用O_RDWR模式打开,或确保读写端同时打开 |
| 写入数据丢失 | 多个进程同时写匿名管道 | 改用FIFO(支持多写者原子性) |
| 读管道返回-1 | 非阻塞模式下资源未就绪 | 检查错误码EAGAIN,重试或延时 |
| 进程意外终止 | 触发SIGPIPE信号 | 捕获SIGPIPE信号,自定义处理逻辑 |

浙公网安备 33010602011771号