Linux IPC进程间通信:无名管道(Pipe) 和有名管道(FIFO)
进程是操作系统资源分配的基本单位,每个进程拥有独立的地址空间,默认无法直接共享数据。但实际开发中,进程间常需交换数据(如父子进程协同处理任务、不同程序共享文件内容),这就需要IPC(InterProcess Communication,进程间通信) 机制。管道(Pipe)是Linux最古老、最基础的IPC方式,分为无名管道(Pipe) 和有名管道(FIFO) 。
一、IPC基础认知
1. 为什么需要IPC?
进程的地址空间是隔离的,内核会为每个进程分配独立的内存区域,进程无法直接访问其他进程的内存。但实际场景中:
- 父子进程需协同完成任务(如父进程读取文件,子进程处理数据);
- 无亲缘关系的进程需共享数据(如两个独立程序交换配置信息);
- 多进程需竞争共享资源(如多个进程访问同一个硬件设备)。
IPC的核心作用就是打破进程隔离,实现数据交换、同步或资源共享。
2. Linux IPC的主要类型
| IPC类型 | 分类 | 特点 |
|---|---|---|
| 无名管道(Pipe) | 古老IPC | 仅支持亲缘进程(父子/兄弟)通信,基于内核队列,文件系统不可见 |
| 有名管道(FIFO) | 古老IPC | 支持任意单机进程通信,文件系统可见(有文件名),底层仍是内核队列 |
| 信号 | 古老IPC | 用于进程间异步通知(如终止进程、处理异常),仅传递简单信号量,无数据 |
| 共享内存 | System V IPC | 最快的IPC方式,多个进程映射同一块物理内存,直接读写(需同步机制) |
| 信号量集 | System V IPC | 用于进程间同步,控制共享资源的排他性访问(避免死锁) |
| 消息队列 | System V IPC | 按类型/优先级传递数据块,使用频率低(已被Socket替代) |
| Socket | 网络IPC | 支持跨主机通信,也可用于本机进程通信,通用性最强 |
3. 管道的核心共性
无论无名管道还是有名管道,都具备以下核心特性:
- 半双工通信:数据只能单向流动(如读端→写端),实际编程中通常当作"单工"使用(固定读写方向);
- 特殊文件:管道是内核维护的伪文件(无实际磁盘存储),文件系统中不可见(无名管道)或仅显示为特殊文件(有名管道);
- 不支持定位操作 :无法使用
lseek()(文件IO)或fseek()(标准IO)调整读写位置,只能顺序读写; - 基于队列实现:管道的底层是内核队列,读写遵循"先进先出(FIFO)"规则;
- 阻塞特性 :
- 读端存在时,写管道超过64KB(默认上限)会阻塞;
- 写端存在时,读空管道会阻塞;
- 读端关闭后写管道,会导致写进程退出(管道破裂);
- 写端关闭后读管道,读操作返回0(表示EOF,通信结束)。
二、无名管道(Pipe):亲缘进程的专属通信方式
1. 无名管道核心特性
- 适用场景 :仅支持亲缘进程(父子、兄弟)通信,由
fork()创建子进程后继承文件描述符实现; - 创建方式 :通过
pipe()函数创建,返回两个文件描述符(读端fd[0]、写端fd[1]); - 生命周期:随进程退出而销毁,无持久化;
- 读写方式 :优先使用文件IO(
read()/write()/close()),也可使用标准IO(fread()/fwrite(),但有缓冲区风险)。
2. 无名管道核心函数
c
#include <unistd.h>
int pipe(int pipefd[2]);
- 功能:创建并打开一个无名管道;
- 参数 :
pipefd[0]为固定读端,pipefd[1]为固定写端; - 返回值 :成功返回0,失败返回-1(需通过
perror()查看错误原因)。
3. 无名管道编程步骤
- 调用
pipe()创建管道,获取读/写文件描述符; - 调用
fork()创建子进程(子进程继承管道描述符); - 父/子进程关闭不需要的描述符(如父进程关闭读端,子进程关闭写端);
- 进程间通过
read()/write()读写管道; - 通信结束后,关闭管道描述符。
4. 无名管道实战代码解析
示例1:读阻塞场景(01pipereadblock.c)
功能:父进程休眠3秒后才向管道写数据,子进程读空管道会阻塞,直到父进程写入数据。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int fd[2]={0};
int ret = pipe(fd); // 1. 创建管道
if(-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork(); // 2. 创建子进程
if(pid>0) // 父进程
{
close(fd[0]); // 3. 父进程关闭读端(只写)
int i = 3;
while(i--) // 休眠3秒,模拟数据准备
{
printf("father 准备数据\n");
sleep(1);
}
char buf[1024]="hello ,son";
write(fd[1],buf,strlen(buf)); // 4. 向管道写数据
close(fd[1]); // 5. 关闭写端
}
else if(0== pid) // 子进程
{
close(fd[1]); // 3. 子进程关闭写端(只读)
char buf[1024]={0};
// 读阻塞:管道为空,子进程阻塞直到父进程写入数据
read(fd[0],buf,sizeof(buf));
printf("father say:%s\n",buf); // 输出:father say:hello ,son
close(fd[0]); // 5. 关闭读端
}
else
{
perror("fork");
return 1;
}
return 0;
}
关键解析 :子进程执行read(fd[0], buf, sizeof(buf))时,管道为空且写端(父进程fd[1])未关闭,因此子进程阻塞3秒,直到父进程写入数据后才继续执行。
示例2:写阻塞场景(02pipewriteblock.c)
功能:父进程持续向管道写数据,子进程不读数据,管道写满64KB后父进程阻塞。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int fd[2]={0};
int ret = pipe(fd);
if(-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork();
if(pid>0) // 父进程(写数据)
{
close(fd[0]);
char buf[1024]={0};
memset(buf,'a',sizeof(buf)); // 填充1024个'a'
int i = 0 ;
// 写阻塞:管道默认上限64KB(64*1024=65536字节)
// 写入65次*1024=66560字节,第65次写入时阻塞
for(i=0;i<65;i++)
{
write(fd[1],buf,sizeof(buf));
printf("已写入第%d次\n",i);
}
close(fd[1]);
}
else if(0== pid) // 子进程(不读数据)
{
close(fd[1]);
while(1) { sleep(1); } // 子进程休眠,不读取管道数据
close(fd[0]);
}
else
{
perror("fork");
return 1;
}
return 0;
}
关键解析 :无名管道的默认缓冲区大小为64KB,父进程写入64次(64*1024=65536字节)后,管道已满,第65次write()会阻塞,直到子进程读取数据释放缓冲区。
示例3:管道破裂场景(03pipebroken.c)
功能:若读端关闭后仍向管道写数据,写进程会被内核终止(管道破裂)。
注:原代码未完全体现"管道破裂",此处补充修正版逻辑(子进程提前关闭读端):
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd[2] = {0};
int ret = pipe(fd);
if (ret == -1) { perror("pipe error"); exit(1); }
pid_t pid = fork();
if (pid > 0) { // 父进程(写)
close(fd[0]);
sleep(1); // 等待子进程关闭读端
char buf[1024] = "hello world";
// 子进程已关闭读端,此处写管道会导致父进程被终止(管道破裂)
int w_ret = write(fd[1], buf, strlen(buf));
if (w_ret == -1) { perror("write error"); }
close(fd[1]);
}
else if (pid == 0) { // 子进程(读)
close(fd[1]);
close(fd[0]); // 子进程提前关闭读端
sleep(3); // 休眠,观察父进程行为
}
else { perror("fork error"); exit(1); }
return 0;
}
关键解析 :子进程关闭读端(fd[0])后,管道的读端不存在,父进程再执行write()会触发SIGPIPE信号,默认导致父进程终止(即"管道破裂")。
示例4:读EOF场景(04readeof.c)
功能:写端关闭后,读管道返回0(EOF),表示通信结束。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd[2] = {0};
int ret = pipe(fd);
if (ret == -1) { perror("pipe error"); exit(1); }
pid_t pid = fork();
if (pid == -1) { perror("fork error"); exit(1); }
else if (pid > 0) { // 父进程(写)
close(fd[0]);
char *buf = "hello world\n";
write(fd[1], buf, strlen(buf));
close(fd[1]); // 关闭写端
exit(0);
}
else if (0 == pid) { // 子进程(读)
close(fd[1]);
sleep(3); // 等待父进程关闭写端
while (1) {
char buf[1024] = {0};
int ret = read(fd[0], buf, sizeof(buf));
if (ret == 0) { // 写端关闭,read返回0(EOF)
printf("通信结束,写端已关闭\n");
break;
}
printf("buf = %s\n", buf); // 输出:buf = hello world
}
close(fd[0]);
}
return 0;
}
关键解析 :父进程关闭写端后,子进程read()返回0,这是判断"管道通信结束"的核心依据。
示例5:管道实现文件拷贝(05pipecp.c)
功能 :父进程读取图片文件1.png,通过管道传递给子进程,子进程将数据写入2.png,实现文件拷贝。
c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd[2] = {0};
int ret = pipe(fd);
if (-1 == ret) { perror("pipe error\n"); return 1; }
pid_t pid = fork();
if (pid > 0) { // 父进程:读1.png,写管道
close(fd[0]);
// 打开源文件(1.png)
int src_fd = open("/home/linux/1.png", O_RDONLY);
if (-1 == src_fd) { perror("open src"); return 1; }
while (1) {
char buf[1024] = {0};
int rd_ret = read(src_fd, buf, sizeof(buf));
if(rd_ret<=0) break; // 源文件读取完毕
write(fd[1], buf, rd_ret); // 写入管道
}
close(fd[1]);
close(src_fd);
}
else if (0 == pid) { // 子进程:读管道,写2.png
close(fd[1]);
// 创建目标文件(2.png)
int dst_fd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);
if (-1 == dst_fd) { perror("open dst"); return 1; }
while(1) {
char buf[1024]={0};
int rd_ret = read(fd[0],buf,sizeof(buf));
if(rd_ret<=0) break; // 管道读取完毕(写端关闭)
write(dst_fd,buf,rd_ret); // 写入目标文件
}
close(fd[0]);
close(dst_fd);
}
else { perror("fork"); return 1; }
return 0;
}
关键解析:利用管道实现父子进程的"数据接力",父进程读取文件数据写入管道,子进程从管道读取数据写入新文件,完成拷贝。该逻辑是多进程文件处理的典型场景。
三、有名管道(FIFO):任意进程的单机通信方式
无名管道仅支持亲缘进程通信,而有名管道(FIFO) 解决了这一限制------它在文件系统中存在一个可见的文件名,任意单机进程只要知道该文件名,就能通过FIFO通信。
1. 有名管道核心特性
- 适用场景 :支持无亲缘关系的进程通信,文件系统中可见(
ls -l显示为p类型文件); - 创建方式 :通过
mkfifo()函数创建,或命令行mkfifo 文件名; - 生命周期:文件系统中持久化(需手动删除),但数据仍存储在内核缓冲区,随进程退出/关闭管道销毁;
- 阻塞特性 :打开FIFO时,若读端未打开则写端
open()阻塞,反之亦然(可通过O_NONBLOCK设置非阻塞)。
2. 有名管道核心函数
c
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- 功能:创建有名管道文件;
- 参数 :
pathname:管道文件的路径+名称(如./myfifo);mode:文件权限(8进制,如0666表示所有用户可读可写);
- 返回值 :成功返回0,失败返回-1(若文件已存在,错误码为
EEXIST)。
3. 有名管道编程步骤
- 调用
mkfifo()创建有名管道文件; - 进程A以读/写模式
open()管道文件; - 进程B以对应模式
open()管道文件(如A读则B写); - 进程间通过
read()/write()读写管道; - 通信结束后,关闭管道描述符,手动删除管道文件(
unlink()或rm)。
4. 有名管道实战代码解析
示例6:FIFO读端(06fifor.c)
功能 :创建有名管道myfifo,以只读模式打开,阻塞等待写端写入数据后读取。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
int main(int argc, char *argv[])
{
// 1. 创建有名管道(已存在则忽略)
int ret = mkfifo("myfifo", 0666);
if (-1 == ret) {
if (EEXIST == errno) {} // 管道已存在,正常继续
else { perror("mkfifo"); return 1; }
}
// 2. 以只读模式打开(阻塞,直到写端打开)
int fd = open("myfifo", O_RDONLY);
if (-1 == fd) { perror("open"); return 1; }
// 3. 读取管道数据
char buf[1024] = {0};
read(fd, buf, sizeof(buf));
printf("从FIFO读取:%s\n", buf); // 输出:从FIFO读取:Hello from fifo
// 4. 关闭管道
close(fd);
// 5. (可选)删除管道文件
unlink("myfifo");
return 0;
}
示例7:FIFO写端(06fifow.c)
功能 :打开已创建的myfifo,以只写模式写入数据。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
int main(int argc, char *argv[])
{
// 1. 创建有名管道
int ret = mkfifo("myfifo", 0666);
if (ret == -1) { perror("mkfifo"); exit(EXIT_FAILURE); }
// 2. 以只写模式打开(阻塞,直到读端打开)
int fd = open("myfifo", O_WRONLY);
if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
// 3. 写入管道数据
char *buf = "Hello from fifo";
write(fd, buf, strlen(buf) + 1); // +1 包含字符串结束符'\0'
// 4. 关闭管道
close(fd);
// 5. (可选)删除管道文件
unlink("myfifo");
return 0;
}
关键解析:
- 先运行读端程序,读端
open("myfifo", O_RDONLY)会阻塞,直到写端打开管道; - 写端打开管道后,读端解除阻塞,写端写入数据,读端读取并输出;
- 通信结束后,需手动
unlink("myfifo")删除管道文件(否则文件会残留)。
四、拓展:文件描述符与标准IO的转换
管道的读写优先使用文件IO(read()/write()),但有时需结合标准IO(fgets()/fputs()),Linux提供fileno()和fdopen()实现两者转换。
1. fileno():将FILE*转为文件描述符(07fileeno.c)
c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
// 1. 标准IO打开文件
FILE* fp = fopen("/etc/passwd","r");
if(NULL== fp) { perror("fopen"); return 1; }
// 2. FILE* 转 文件描述符
int fd = fileno(fp);
char buf[100]={0};
read(fd,buf,sizeof(buf)-1); // 文件IO读取
printf("%s",buf); // 输出/etc/passwd的前99个字符
fclose(fp);
return 0;
}
2. fdopen():将文件描述符转为FILE*(08fdopen.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// 1. 文件IO打开文件
int fd = open("/etc/passwd",O_RDONLY);
if (-1 ==fd) { perror("open"); return 1; }
// 2. 文件描述符 转 FILE*
FILE* fp = fdopen(fd,"r") ;
if(NULL == fp) { perror("fdopen"); return 1; }
char buf[100]={0};
fgets(buf,sizeof(buf),fp); // 标准IO读取
printf("buf :%s",buf); // 输出/etc/passwd的第一行
fclose(fp);
return 0;
}
五、管道使用的核心注意事项
- 避免死锁:多进程竞争管道时,需通过信号量/互斥锁同步(死锁的四个必要条件:互斥、请求与保持、不剥夺、循环等待);
- 阻塞与非阻塞 :默认打开管道为阻塞模式,可通过
open()的O_NONBLOCK参数设置非阻塞(需处理EAGAIN错误); - 管道大小限制 :无名管道默认缓冲区64KB,可通过
fcntl(fd, F_SETPIPE_SZ, size)调整; - 管道破裂处理 :可捕获
SIGPIPE信号,避免写进程被终止; - 资源释放:通信结束后必须关闭管道描述符,有名管道需手动删除文件,避免资源泄漏。
六、总结
管道是Linux最基础的IPC方式,无名管道适用于亲缘进程的简单数据交换,有名管道支持任意单机进程通信,两者底层均基于内核队列,遵循"先进先出"规则。管道的核心是"阻塞特性"和"半双工通信",掌握其读写规则(如读空阻塞、写满阻塞、EOF判断)是编程的关键。
在实际开发中:
- 简单的父子进程协同任务,优先使用无名管道;
- 无亲缘关系的进程通信,使用有名管道;
- 需跨主机通信或复杂数据交互,使用Socket;
- 需高性能共享数据,使用共享内存(配合信号量同步)。