目录
前言
一、命名管道的诞生:解决匿名管道的核心痛点
1.1 匿名管道的致命短板
1.2 命名管道的核心设计思路
1.3 命名管道与匿名管道的核心关联
二、命名管道的核心认知:什么是 FIFO 文件?
2.1 FIFO 文件的本质
2.2 FIFO 文件的标识
2.3 命名管道的通信模型
三、命名管道的创建:mkfifo () 函数与命令行
3.1 命令行创建命名管道
3.2 程序中创建命名管道:mkfifo () 函数
3.2.1 mkfifo () 函数接口
3.2.2 核心注意点
3.2.3 简单创建示例
四、命名管道的打开规则:最关键的核心知识点
4.1 open () 函数的核心参数
4.2 以读方式打开命名管道(O_RDONLY)
4.2.1 阻塞模式(默认,仅 O_RDONLY)
4.2.2 非阻塞模式(O_RDONLY | O_NONBLOCK)
4.3 以写方式打开命名管道(O_WRONLY)
4.3.1 阻塞模式(默认,仅 O_WRONLY)
4.3.2 非阻塞模式(O_WRONLY | O_NONBLOCK)
4.4 命名管道打开规则总结
4.5 打开规则的本质
五、命名管道的读写规则:完全复用匿名管道
5.1 无数据可读时的读操作规则
5.2 管道缓冲区写满时的写操作规则
5.3 所有写端关闭后的读操作规则
5.4 所有读端关闭后的写操作规则
5.5 管道写入的原子性规则
5.6 命名管道读写规则的核心注意点
六、命名管道的实战开发:从基础案例到 Server-Client 通信
6.1 实战案例 1:命名管道基础读写
6.1.1 写端程序:fifo_write.c
6.1.2 读端程序:fifo_read.c
6.1.3 编译与运行
6.1.4 运行结果分析
6.2 实战案例 2:基于命名管道实现文件拷贝
6.2.1 发送端程序:fifo_copy_send.c
6.2.2 接收端程序:fifo_copy_recv.c
6.2.3 测试步骤
6.2.4 案例核心亮点
6.3 实战案例 3:基于命名管道实现 Server-Client 通信
6.3.1 服务端程序:serverPipe.c
6.3.2 客户端程序:clientPipe.c
6.3.3 编译与运行
6.3.4 案例核心亮点
七、命名管道的核心特点与使用场景
7.1 命名管道的核心特点
7.2 命名管道的典型使用场景
7.3 命名管道的局限性
7.4 命名管道与其他 IPC 方式的对比
八、命名管道开发的避坑指南
8.1 坑 1:未处理 SIGPIPE 信号,导致进程意外退出
8.2 坑 2:非阻塞模式下,错误判断不严谨
8.3 坑 3:多进程写管道时,数据混乱
8.4 坑 4:双向通信时,只创建一个管道,导致死锁
8.5 坑 5:FIFO 文件未删除,导致程序重启时 mkfifo 报错
8.6 坑 6:阻塞模式下,进程卡死,无法退出
总结
前言
在 Linux 进程间通信的学习中,匿名管道作为入门级的 IPC 方式,让我们理解了 “内核缓冲区 + 一切皆文件” 的设计思想,但它有一个致命的限制 ——只能用于具有亲缘关系的进程间通信。而命名管道(FIFO)作为匿名管道的 “升级版”,完美突破了这一限制,让任意无亲缘关系的进程也能实现高效的管道通信。
命名管道也叫 FIFO(First In First Out),是一种特殊的管道文件,它拥有磁盘级的文件名,通过文件系统实现进程间的关联,操作接口与匿名管道完全兼容,却能覆盖更广泛的通信场景。本文将从命名管道的核心概念出发,一步步拆解其原理、创建方式、打开规则、实战应用,结合硬核代码和通俗讲解,让你彻底吃透命名管道,掌握跨进程通信的关键技能。下面就让我们正式开始吧!

一、命名管道的诞生:解决匿名管道的核心痛点
在学习命名管道之前,我们先回顾一下匿名管道的核心局限性,这也是命名管道诞生的根本原因。
1.1 匿名管道的致命短板
匿名管道基于文件描述符继承实现通信,只有通过fork()创建的子进程(或兄弟进程)才能继承父进程的管道描述符,因此只能用于有共同祖先的亲缘进程(父 / 子、兄 / 弟)。
但在实际开发中,我们更多的需求是让完全独立的无亲缘进程通信:比如一个服务进程和一个客户端进程、两个独立启动的应用程序、不同用户的进程之间,此时匿名管道就完全无法满足需求。
1.2 命名管道的核心设计思路
为了解决匿名管道的亲缘限制问题,Linux 设计者在匿名管道的基础上,增加了文件系统的标识,也就是命名管道文件。
命名管道的核心设计思路可以总结为:给内核中的管道缓冲区,在文件系统中分配一个唯一的文件名,任何进程只要能访问这个文件,就能通过它连接到内核中的管道缓冲区,实现进程间通信。
简单来说,匿名管道是 “无名的内核缓冲区”,只有亲缘进程能找到;命名管道是 “有名的内核缓冲区”,任何进程只要知道文件名,就能找到并使用。
1.3 命名管道与匿名管道的核心关联
命名管道是匿名管道的超集,二者的内核实现和操作语义完全一致:
- 底层都是内核维护的单向字节流缓冲区;
- 都遵循相同的读写规则(阻塞 / 非阻塞、原子写入、SIGPIPE 信号等);
- 都使用read()/write()/close()等标准文件操作接口;
- 都是半双工通信,双向通信需要创建两个管道。
二者的唯一区别仅在于创建和打开的方式:
- 匿名管道通过pipe()函数创建,创建即打开,返回两个文件描述符;
- 命名管道通过mkfifo()函数创建(生成管道文件),通过open()函数打开,获取文件描述符。
可以说,只要掌握了匿名管道,学习命名管道只需要重点掌握创建和打开这两个新操作,其余内容完全可以复用。
二、命名管道的核心认知:什么是 FIFO 文件?
命名管道的核心是FIFO 文件,这是一种特殊的文件类型,与普通文件、目录、设备文件并列,存在于 Linux 的文件系统中,但又有其独特的属性。
2.1 FIFO 文件的本质
FIFO 文件仅作为进程间通信的 “标识”,它本身不存储任何数据,数据依旧存储在内核的管道缓冲区中。
当进程对 FIFO 文件执行read()/write()操作时,实际上是对内核中的管道缓冲区进行操作,FIFO 文件只是一个 “入口”。进程退出后,FIFO 文件会保留在文件系统中(除非手动删除),但内核中的管道缓冲区会被释放,数据也会丢失。
2.2 FIFO 文件的标识
在 Linux 中,通过ls -l命令查看文件时,FIFO 文件的文件类型标识为 p(pipe 的首字母),这是区分 FIFO 文件和其他文件的关键。
例如,创建一个名为mypipe的命名管道后,执行ls -l会看到如下结果:
prw-r–r– 1 root root 0 2月 11 15:00 mypipe
其中:
- p:表示文件类型为命名管道(FIFO);
- 大小为0:因为 FIFO 文件不存储数据,仅作为标识;
- 权限位与普通文件一致(如rw-r–r–),用于控制进程对管道的访问权限。
2.3 命名管道的通信模型
命名管道的通信模型非常简单,核心分为三步:
整个过程中,FIFO 文件就像一个 “桥梁”,连接了两个无亲缘进程和内核中的管道缓冲区,突破了匿名管道的亲缘限制。
三、命名管道的创建:mkfifo () 函数与命令行
命名管道有两种创建方式:命令行创建和程序中通过 mkfifo () 函数创建,前者适用于测试和手动操作,后者适用于程序开发,二者的效果完全一致。
3.1 命令行创建命名管道
在 Linux 终端中,直接使用mkfifo命令即可创建命名管道,语法如下:
# mkfifo [选项] 管道文件名
mkfifo mypipe
常用选项:
- -m:指定管道文件的权限,如mkfifo -m 0644 mypipe;
- 不指定权限时,默认权限由umask决定(通常为 0666 & ~umask)。
创建后,通过ls -l即可看到标识为p的 FIFO 文件,通过rm 管道文件名即可删除该文件。
3.2 程序中创建命名管道:mkfifo () 函数
在 C/C++ 程序中,通过mkfifo()函数创建命名管道,这是开发中的主流方式,函数原型定义在<sys/stat.h>头文件中。
3.2.1 mkfifo () 函数接口
#include <sys/types.h>
#include <sys/stat.h>
// 功能:创建一个命名管道(FIFO文件)
// 参数:
// pathname – 管道文件的路径+名称,如"./mypipe"
// mode – 管道文件的权限,如0644、0666,与open()函数的mode参数一致
// 返回值:成功返回0,失败返回-1,并设置errno
int mkfifo(const char *pathname, mode_t mode);
3.2.2 核心注意点
3.2.3 简单创建示例
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
int main()
{
// 将umask置0,让管道权限严格为0644
umask(0);
// 创建命名管道文件./mypipe,权限0644
if (mkfifo("./mypipe", 0644) < 0)
{
perror("mkfifo error");
return -1;
}
printf("命名管道创建成功\\n");
return 0;
}
编译运行该程序后,在当前目录下执行ls –l,就能看到prw-r–r–的 FIFO 文件mypipe。
四、命名管道的打开规则:最关键的核心知识点
命名管道的打开规则是学习的重中之重,也是与普通文件打开的最大区别,直接决定了进程的运行行为(阻塞 / 非阻塞)。
命名管道是单向通信的,必须有一个进程以读方式打开,一个进程以写方式打开,内核才会完成管道的初始化,否则打开操作会根据是否设置非阻塞标志产生不同的结果。
命名管道的打开规则围绕open()函数的flags 参数展开,核心分为读打开和写打开两种情况,每种情况又分为阻塞模式(默认)和非阻塞模式(O_NONBLOCK)。
4.1 open () 函数的核心参数
命名管道通过标准的open()函数打开,函数原型如下:
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
int open(const char *pathname, int flags, mode_t mode);
对于命名管道,核心关注flags参数,常用取值:
- O_RDONLY:以只读方式打开(读端);
- O_WRONLY:以只写方式打开(写端);
- O_NONBLOCK:非阻塞模式,与 O_RDONLY/O_WRONLY 配合使用(如 O_RDONLY | O_NONBLOCK);
- O_CREAT:如果文件不存在则创建(命名管道一般先通过 mkfifo () 创建,此参数很少用)。
4.2 以读方式打开命名管道(O_RDONLY)
4.2.1 阻塞模式(默认,仅 O_RDONLY)
如果进程以纯读方式打开命名管道,且此时没有任何进程以写方式打开该管道,则open()调用会阻塞,直到有进程以写方式打开该管道,才会返回文件描述符。
简单来说:读端先打开,会阻塞等待写端打开。
4.2.2 非阻塞模式(O_RDONLY | O_NONBLOCK)
如果进程以非阻塞读方式打开命名管道,无论此时是否有进程以写方式打开该管道,open()调用都会立刻返回成功,获取读端文件描述符。
4.3 以写方式打开命名管道(O_WRONLY)
4.3.1 阻塞模式(默认,仅 O_WRONLY)
如果进程以纯写方式打开命名管道,且此时没有任何进程以读方式打开该管道,则open()调用会阻塞,直到有进程以读方式打开该管道,才会返回文件描述符。
简单来说:写端先打开,会阻塞等待读端打开。
4.3.2 非阻塞模式(O_WRONLY | O_NONBLOCK)
如果进程以非阻塞写方式打开命名管道,且此时没有任何进程以读方式打开该管道,则open()调用会立刻失败,返回 – 1,errno设置为ENXIO。
4.4 命名管道打开规则总结
为了方便记忆,我们将命名管道的打开规则整理成表格,核心记住阻塞模式下,读 / 写端相互等待;非阻塞模式下,读端一定成功,写端可能失败:
| O_RDONLY(阻塞读) | 阻塞等待 | 立刻成功 | |
| O_RDONLY | O_NONBLOCK(非阻塞读) | 立刻成功 | 立刻成功 |
| O_WRONLY(阻塞写) | 阻塞等待 | 立刻成功 | |
| O_WRONLY | O_NONBLOCK(非阻塞写) | 失败(ENXIO) | 立刻成功 |
核心口诀:堵读等写,堵写等读;非堵读必成,非堵写看端。
4.5 打开规则的本质
命名管道的打开规则,本质是为了保证管道的通信双方都存在,避免出现 “一个进程向管道写入数据,但没有进程读取” 或 “一个进程从管道读取数据,但没有进程写入” 的无效操作,是内核对管道通信的基础保障。
而非阻塞模式则为程序开发提供了灵活性,让进程可以不等待另一端,直接执行后续逻辑,适用于需要轮询或处理多任务的场景。
五、命名管道的读写规则:完全复用匿名管道
命名管道的读写规则与匿名管道完全一致,因为二者的底层都是内核维护的字节流缓冲区,操作语义完全相同。这里我们对核心读写规则进行重点回顾,结合命名管道的场景做简单说明,让你无缝复用知识。
命名管道的读写规则同样与是否设置非阻塞模式(O_NONBLOCK)密切相关,同时涉及原子写入、SIGPIPE 信号等关键特性,是开发中必须遵守的规则,也是面试高频考点。
5.1 无数据可读时的读操作规则
- 阻塞模式(默认):read()调用会阻塞,直到有进程向管道写入数据,才会被唤醒并读取数据;
- 非阻塞模式(O_NONBLOCK):read()调用立刻返回 – 1,errno设置为EAGAIN(表示资源暂时不可用,可重试)。
5.2 管道缓冲区写满时的写操作规则
Linux 内核中管道缓冲区的默认大小为4096 字节(1 页),可以通过ulimit -p命令查看。当管道缓冲区被写满时:
- 阻塞模式(默认):write()调用会阻塞,直到有进程从管道读取数据,释放缓冲区空间,才会被唤醒并继续写入;
- 非阻塞模式(O_NONBLOCK):write()调用立刻返回 – 1,errno设置为EAGAIN。
5.3 所有写端关闭后的读操作规则
如果所有持有管道写端文件描述符的进程都关闭了写端,此时管道中剩余的数据可以正常读取;当数据读取完毕后,再次调用read()会返回 0,与读取普通文件到末尾的行为一致,表示 “管道已无数据,且不会再有新数据写入”。
这是命名管道通信中,读端判断写端退出的核心方式,也是实现通信结束的关键。
5.4 所有读端关闭后的写操作规则
如果所有持有管道读端文件描述符的进程都关闭了读端,此时进程向管道写端写入数据时,内核会向该进程发送SIGPIPE 信号,该信号的默认处理方式是终止进程。
这是一个非常重要的 “坑”,在命名管道开发中,如果未处理 SIGPIPE 信号,可能导致进程意外退出。因此,实际开发中需要通过signal()或sigaction()函数捕获并处理 SIGPIPE 信号(如忽略该信号)。
5.5 管道写入的原子性规则
Linux 内核保证了命名管道写入的原子性,规则与匿名管道一致:
- 当要写入的数据量不大于 PIPE_BUF(内核定义的宏,默认 4096 字节)时,内核保证写入操作的原子性 —— 数据会被完整写入,不会被其他进程的写入操作打断;
- 当要写入的数据量大于 PIPE_BUF时,内核不保证写入操作的原子性 —— 数据可能被拆分,与其他进程的写入数据交错存在于管道中。
PIPE_BUF的取值可以通过<limits.h>头文件查看,也可以通过命令getconf PIPE_BUF获取,不同系统的取值可能不同,但至少为 512 字节。
5.6 命名管道读写规则的核心注意点
命名管道的读写操作完全遵循文件的操作语义,但又有管道的特殊属性,核心注意点:
六、命名管道的实战开发:从基础案例到 Server-Client 通信
理论学习后,最关键的是实战。本节我们通过三个经典的实战案例,从简单到复杂,一步步实现命名管道的开发,分别是:基础读写案例、文件拷贝案例、Server-Client 通信案例,覆盖命名管道的核心使用场景。
6.1 实战案例 1:命名管道基础读写
需求:创建两个独立的程序,fifo_write.c(写端)和fifo_read.c(读端),实现写端向命名管道写入字符串,读端从命名管道读取并打印,验证命名管道的基本通信功能。
6.1.1 写端程序:fifo_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
// 错误处理宏
#define ERR_EXIT(m) \\
do{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
// 命名管道文件名
#define FIFO_NAME "./myfifo"
int main()
{
// 1. 创建命名管道(如果已存在,会报错,这里先判断,不存在则创建)
umask(0);
if (mkfifo(FIFO_NAME, 0644) < 0)
{
// 错误码EEXIST表示文件已存在,无需处理
if (errno != EEXIST)
ERR_EXIT("mkfifo error");
}
// 2. 以阻塞写方式打开命名管道(默认O_WRONLY,会阻塞等待读端打开)
int wfd = open(FIFO_NAME, O_WRONLY);
if (wfd < 0)
ERR_EXIT("open write error");
printf("写端成功打开命名管道,文件描述符:%d\\n", wfd);
// 3. 向管道写入数据
char buf[1024] = {0};
while (1)
{
// 从键盘读取输入
printf("请输入要发送的内容:");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s-1] = 0; // 去掉换行符
// 向管道写入数据
write(wfd, buf, strlen(buf));
// 输入quit,退出程序
if (strcmp(buf, "quit") == 0)
break;
}
}
// 4. 关闭文件描述符
close(wfd);
printf("写端关闭命名管道\\n");
return 0;
}
6.1.2 读端程序:fifo_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
// 错误处理宏
#define ERR_EXIT(m) \\
do{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
// 命名管道文件名,与写端保持一致
#define FIFO_NAME "./myfifo"
int main()
{
// 1. 以阻塞读方式打开命名管道(默认O_RDONLY,会阻塞等待写端打开)
int rfd = open(FIFO_NAME, O_RDONLY);
if (rfd < 0)
ERR_EXIT("open read error");
printf("读端成功打开命名管道,文件描述符:%d\\n", rfd);
// 2. 从管道读取数据
char buf[1024] = {0};
while (1)
{
ssize_t s = read(rfd, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s] = 0;
printf("读端读取到数据:%s\\n", buf);
// 读取到quit,退出程序
if (strcmp(buf, "quit") == 0)
break;
}
else if (s == 0)
{
// s==0表示所有写端关闭
printf("所有写端已关闭,读端退出\\n");
break;
}
else
{
ERR_EXIT("read error");
}
}
// 3. 关闭文件描述符
close(rfd);
// 删除命名管道文件(可选,也可以手动删除)
unlink(FIFO_NAME);
printf("读端关闭并删除命名管道\\n");
return 0;
}
6.1.3 编译与运行
gcc fifo_read.c -o fifo_read
- 终端 1:./fifo_read
- 终端 2:./fifo_write
6.1.4 运行结果分析
- 无论先运行读端还是写端,阻塞模式下都会相互等待,直到另一端打开,才会打印 “成功打开命名管道”;
- 写端输入的内容会通过命名管道传输到读端,实现无亲缘进程间的通信;
- 写端输入quit后,会向管道写入quit,读端读取到后退出,同时删除 FIFO 文件;
- 如果直接关闭写端终端,读端会检测到s==0(写端关闭),并退出程序。
6.2 实战案例 2:基于命名管道实现文件拷贝
需求:创建两个程序,fifo_copy_send.c(发送端)和fifo_copy_recv.c(接收端),实现发送端读取本地文件,通过命名管道发送给接收端,接收端将数据写入新文件,完成文件拷贝,验证命名管道的大数据传输能力。
6.2.1 发送端程序:fifo_copy_send.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#define ERR_EXIT(m) \\
do{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
#define FIFO_NAME "./copy_fifo"
#define SRC_FILE "./src.txt" // 要拷贝的源文件
int main()
{
// 1. 创建命名管道
umask(0);
if (mkfifo(FIFO_NAME, 0644) < 0)
{
if (errno != EEXIST)
ERR_EXIT("mkfifo error");
}
// 2. 打开源文件和命名管道
int src_fd = open(SRC_FILE, O_RDONLY);
if (src_fd < 0)
ERR_EXIT("open src file error");
int wfd = open(FIFO_NAME, O_WRONLY);
if (wfd < 0)
ERR_EXIT("open fifo write error");
printf("发送端准备就绪,开始拷贝文件…\\n");
// 3. 读取源文件,写入命名管道
char buf[1024] = {0};
ssize_t n = 0;
while ((n = read(src_fd, buf, sizeof(buf))) > 0)
{
write(wfd, buf, n);
}
// 4. 关闭文件描述符
close(src_fd);
close(wfd);
unlink(FIFO_NAME); // 可选删除
printf("文件拷贝完成,发送端退出\\n");
return 0;
}
6.2.2 接收端程序:fifo_copy_recv.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \\
do{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
#define FIFO_NAME "./copy_fifo"
#define DST_FILE "./dst.txt" // 拷贝后的目标文件
int main()
{
// 1. 打开命名管道和目标文件
int rfd = open(FIFO_NAME, O_RDONLY);
if (rfd < 0)
ERR_EXIT("open fifo read error");
// 创建目标文件,权限0644,存在则截断
int dst_fd = open(DST_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dst_fd < 0)
ERR_EXIT("open dst file error");
printf("接收端准备就绪,开始接收数据…\\n");
// 2. 从命名管道读取数据,写入目标文件
char buf[1024] = {0};
ssize_t n = 0;
while ((n = read(rfd, buf, sizeof(buf))) > 0)
{
write(dst_fd, buf, n);
}
// 3. 关闭文件描述符
close(rfd);
close(dst_fd);
printf("文件接收完成,目标文件:%s\\n", DST_FILE);
return 0;
}
6.2.3 测试步骤
6.2.4 案例核心亮点
- 利用命名管道的字节流特性,实现了任意大小文件的拷贝(只要内存足够);
- 读写缓冲区设置为 1024 字节,小于 PIPE_BUF(4096),保证了写入的原子性;
- 发送端读取文件的返回值直接作为写入管道的长度,保证了数据的完整性;
- 完美体现了 “一切皆文件” 的思想:文件和管道的操作接口完全一致,仅需替换文件描述符。
6.3 实战案例 3:基于命名管道实现 Server-Client 通信
需求:实现一个简单的服务端(Server)- 客户端(Client)通信模型,服务端持续监听命名管道,客户端向服务端发送消息,服务端实时打印客户端发送的内容,支持客户端退出后服务端继续等待新的客户端连接,这是命名管道在实际开发中的典型应用场景。
6.3.1 服务端程序:serverPipe.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define ERR_EXIT(m) \\
do{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
#define FIFO_NAME "./server_fifo"
int main()
{
// 1. 创建命名管道,服务端作为管道的创建者
umask(0);
if (mkfifo(FIFO_NAME, 0644) < 0)
{
if (errno != EEXIST)
ERR_EXIT("mkfifo error");
}
printf("服务端启动,等待客户端连接…\\n");
while (1)
{
// 2. 以阻塞读方式打开管道,客户端退出后,重新打开等待新客户端
int rfd = open(FIFO_NAME, O_RDONLY);
if (rfd < 0)
ERR_EXIT("open read error");
char buf[1024] = {0};
while (1)
{
ssize_t s = read(rfd, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s-1] = 0; // 去掉换行符
printf("客户端消息:%s\\n", buf);
}
else if (s == 0)
{
// 客户端关闭写端,服务端关闭当前读端,重新等待
printf("客户端断开连接,等待新客户端…\\n");
close(rfd);
break;
}
else
{
ERR_EXIT("read error");
close(rfd);
break;
}
}
}
// 实际不会执行到这里,服务端持续运行
close(rfd);
unlink(FIFO_NAME);
return 0;
}
6.3.2 客户端程序:clientPipe.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define ERR_EXIT(m) \\
do{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
#define FIFO_NAME "./server_fifo"
int main()
{
// 1. 以阻塞写方式打开命名管道,连接服务端
int wfd = open(FIFO_NAME, O_WRONLY);
if (wfd < 0)
ERR_EXIT("open write error");
printf("客户端成功连接服务端,请输入消息(输入quit退出):\\n");
// 2. 向服务端发送消息
char buf[1024] = {0};
while (1)
{
printf("> ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0)
{
write(wfd, buf, s);
if (strncmp(buf, "quit", 4) == 0)
{
printf("客户端退出\\n");
break;
}
memset(buf, 0, sizeof(buf));
}
}
// 3. 关闭写端
close(wfd);
return 0;
}
6.3.3 编译与运行
all:serverPipe clientPipe
serverPipe:serverPipe.c
gcc -o $@ $^
clientPipe:clientPipe.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f serverPipe clientPipe
6.3.4 案例核心亮点
- 服务端通过外层死循环,实现了持续监听,客户端退出后,重新打开命名管道,等待新的客户端连接;
- 利用写端关闭后读端 read 返回 0的特性,服务端能精准检测到客户端的断开连接;
- 实现了一对多的通信雏形,一个服务端可以为多个客户端提供通信服务;
- 代码简洁,核心逻辑清晰,可直接扩展为更复杂的通信模型(如添加消息解析、指令处理等)。
七、命名管道的核心特点与使用场景
结合前面的原理和实战,我们总结命名管道的核心特点,并梳理其典型的使用场景,帮助你在实际开发中快速判断是否适合使用命名管道。
7.1 命名管道的核心特点
7.2 命名管道的典型使用场景
命名管道适用于简单、低并发、单向 / 双向字节流通信的场景,尤其是需要在无亲缘进程间传递数据的场景,典型应用包括:
7.3 命名管道的局限性
命名管道并非万能的,也存在一些局限性,在高并发、高要求的通信场景中,需要选择其他 IPC 方式:
7.4 命名管道与其他 IPC 方式的对比
为了方便在实际开发中选择合适的 IPC 方式,我们将命名管道与常见的 IPC 方式(匿名管道、共享内存、消息队列、Socket)进行核心对比:
| 匿名管道 | 有 | 半双工 | 字节流 | 否 | 一般 | 简单、轻量、亲缘进程专用 |
| 命名管道 | 无 | 半双工 | 字节流 | 否 | 一般 | 无亲缘限制、接口统一 |
| 共享内存 | 无 | 全双工 | 自定义 | 否 | 极高 | 最快的 IPC 方式 |
| 消息队列 | 无 | 全双工 | 消息 | 否 | 较好 | 有消息边界、自带格式 |
| Socket(本地) | 无 | 全双工 | 字节流 / 数据包 | 是 | 较好 | 跨主机、功能强大 |
选择建议:
- 亲缘进程间简单通信:匿名管道;
- 无亲缘进程间本地简单通信:命名管道;
- 本地高速度大数据传输:共享内存(需手动实现同步互斥);
- 本地需要消息边界的通信:消息队列;
- 跨主机通信或复杂的本地通信:Socket。
八、命名管道开发的避坑指南
在命名管道的实际开发中,新手很容易踩坑,导致程序出现阻塞、崩溃、数据丢失等问题。本节梳理开发中最常见的坑,并给出对应的解决方案,帮助你避开陷阱,写出健壮的代码。
8.1 坑 1:未处理 SIGPIPE 信号,导致进程意外退出
问题:所有读端关闭后,写端继续写入数据,内核会发送 SIGPIPE 信号,默认处理方式是终止进程,导致程序意外退出。
解决方案:在写端程序中,捕获并忽略 SIGPIPE 信号:
#include <signal.h>
// 忽略SIGPIPE信号
signal(SIGPIPE, SIG_IGN);
8.2 坑 2:非阻塞模式下,错误判断不严谨
问题:非阻塞模式下,read()/write()返回 – 1 时,直接认为是程序错误,实际上可能是EAGAIN(资源暂时不可用),属于正常情况。
解决方案:判断返回值为 – 1 时,结合errno进行判断,忽略EAGAIN:
ssize_t s = read(rfd, buf, sizeof(buf));
if (s < 0)
{
if (errno == EAGAIN)
{
// 非阻塞模式下,无数据,可重试
continue;
}
else
{
// 真正的错误,处理
perror("read error");
break;
}
}
8.3 坑 3:多进程写管道时,数据混乱
问题:多个进程同时向命名管道写入数据,且写入的数据量大于 PIPE_BUF,导致数据交错,出现混乱。
解决方案:保证每个进程的写入数据量不大于 PIPE_BUF,利用原子写入特性,避免数据交错;如果需要写入大数据,可将数据拆分为多个 PIPE_BUF 大小的块,依次写入。
8.4 坑 4:双向通信时,只创建一个管道,导致死锁
问题:实现双向通信时,只创建一个命名管道,两个进程同时以读 + 写方式打开,导致双方相互阻塞,出现死锁。
解决方案:双向通信必须创建两个命名管道,分别负责 A→B 和 B→A 的数据流,每个进程对一个管道读,对另一个管道写。
8.5 坑 5:FIFO 文件未删除,导致程序重启时 mkfifo 报错
问题:程序退出后,未删除 FIFO 文件,再次启动程序时,mkfifo()会因文件已存在而报错(errno=EEXIST)。
解决方案:
8.6 坑 6:阻塞模式下,进程卡死,无法退出
问题:阻塞模式下,进程打开管道后,另一端一直未打开,导致进程卡死在open()调用,无法退出。 解决方案:
总结
命名管道的学习,不仅让我们掌握了一种实用的 IPC 方式,更让我们加深了对 Linux “一切皆文件” 设计思想的理解。在实际开发中,命名管道虽然不如 Socket 功能强大,不如共享内存速度快,但它以轻量、简单、易用的特点,在本地跨进程通信的场景中占据着重要的位置。
当然,命名管道也只是 Linux IPC 家族的一员,后续还可以继续学习共享内存(最快的 IPC)、消息队列(有消息边界的通信)、Socket(跨主机通信)等方式,构建完整的 Linux 进程间通信知识体系。但无论学习哪种 IPC 方式,命名管道的基础知识和设计思想,都是重要的铺垫。
希望本文能帮助你彻底吃透命名管道,在实际开发中灵活运用,实现高效的进程间通信!
网硕互联帮助中心





评论前必须登录!
注册