跳到主要内容
Linux 进程间通信:命名管道(FIFO)核心原理与实战 | 极客日志
C++
Linux 进程间通信:命名管道(FIFO)核心原理与实战 Linux 进程间通信中,命名管道(FIFO)通过文件系统路径打破亲缘进程限制,实现任意进程间的双向数据传输。其核心特性、创建方式及打开规则,重点解析读写端同步阻塞机制。结合文件拷贝与 C/S 模型实战案例,演示 mkfifo 与 open 系统调用的具体应用,并总结权限设置、原子性保证等避坑要点,帮助开发者掌握高效可靠的跨进程通信方案。
Ne0 发布于 2026/3/26 更新于 2026/5/24 14 浏览Linux 进程间通信:命名管道(FIFO)核心原理与实战
前言
在 Linux IPC(进程间通信)体系中,匿名管道解决了亲缘进程间的通信问题,但存在一个核心限制——仅支持具有共同祖先的进程通信。而命名管道(FIFO)作为匿名管道的扩展,通过文件系统中的'命名'标识,打破了亲缘关系的束缚,实现了无关联进程间的双向数据传输。本文将从命名管道的核心概念、创建方式、通信原理和实战案例出发,带你吃透这一实用的跨进程通信技术。
一、命名管道核心概念:什么是 FIFO?
1.1 命名管道的定义
命名管道(Named Pipe),又称 FIFO(First In First Out),是一种特殊的文件系统对象(类型为 p)。其核心本质与匿名管道一致——内核中的一块缓冲区,但通过文件路径作为标识,让任意进程都能通过该路径访问管道,实现跨进程通信。
与匿名管道相比,命名管道的核心差异在于'命名':
匿名管道 :无文件路径,仅通过 pipe() 创建的文件描述符在亲缘进程间共享;
命名管道 :有明确的文件路径(如 /tmp/myfifo),任意进程可通过 open() 打开该路径,实现通信。
1.2 命名管道的核心特性
跨进程通信 :支持无亲缘关系的进程(如两个独立的应用程序)通信,突破匿名管道的亲缘限制;
半双工通信 :数据单向流动,如需双向通信需创建两个命名管道;
基于文件操作 :遵循 Linux'一切皆文件'思想,通过 open()/read()/write()/close() 等标准文件接口操作;
生命周期随内核 :命名管道创建后,即使创建进程退出,管道文件仍存在于文件系统中,需手动删除(unlink() 或 rm 命令);
同步与互斥 :内核自动保证管道操作的同步(如读阻塞、写阻塞)和互斥(同一时间仅允许一个进程写)。
1.3 命名管道和匿名管道的区别与联系
两者唯一的区别在于创建与打开的方式不同,一旦工作完成,它们具有相同的语义。为了更清晰地理解命名管道的定位,整理两者的核心差异对比如下表所示:
定义 一种半双工通信通道,通常用于具有亲缘关系的进程之间(如父子进程)。 一种特殊的文件(存在于文件系统中),允许无亲缘关系的进程间通信。 创建方式 通过 pipe() 系统调用创建,返回两个文件描述符(读端和写端)。 通过 mkfifo() 或 mknod() 创建,在文件系统中具有路径名。 标识 无名,仅通过文件描述符引用。 有名,通过文件系统中的路径名标识。 进程关系 仅适用于有共同祖先(如父子进程)的进程。 适用于任意进程,无论是否有亲缘关系。 通信方向 单向(半双工),数据只能从一个方向流动。 通常也是单向(半双工),但可通过打开两个管道实现双向通信。 持久性 随进程存在而存在,所有相关进程关闭管道后自动销毁。 随文件系统存在,可显式删除(unlink),即使没有进程打开也不会消失。 打开方式 无需显式打开,创建时直接获得文件描述符。 必须像普通文件一样用 open() 打开,使用路径名。 数据模型 字节流,无消息边界。 字节流,无消息边界。 阻塞行为 默认阻塞:读空管道或写满管道会使进程阻塞(可设置非阻塞)。 默认阻塞:读空 FIFO 或写满 FIFO 会使进程阻塞(可设置非阻塞)。 典型应用场景 父子进程间的简单通信,如 shell 中的命令管道。 无关联进程间通信(如 C/S 模型)。
通信机制 :两者都是操作系统提供的进程间通信(IPC)方式,基于内核缓冲区实现数据传输。
数据特性 :都提供可靠的字节流服务,数据写入和读取的顺序一致,无消息边界。
行为相似 :默认情况下,读写操作具有相似的阻塞语义(读空阻塞、写满阻塞),并支持非阻塞标志。
单向性 :本质上都是单向通信管道(半双工),若要双向通信需创建两个管道。
原子性 :在 PIPE_BUF 限制内,写入操作具有原子性(多个进程同时写时数据不会交错)。
二、命名管道的创建方式 命名管道有两种创建方式:命令行创建和代码创建,本质都是在文件系统中生成一个 FIFO 类型的文件。
2.1 命令行创建(mkfifo 命令) 直接通过 mkfifo 命令创建命名管道,语法简单,适合快速测试:
mkfifo myfifo
ls -l myfifo
我们可以发现,这里创建的管道文件(最前面的标识是 p 开头),就算我们不停地往里面进行写操作,它的文件大小是一直不变的,验证了它不是一个普通文件。另外当我们直接终止读端的时候,这个时候的情况就相当于读端关闭,写端还在一直写,所以操作系统直接杀死了我们的进程。
2.2 代码创建(mkfifo 函数) 通过 mkfifo() 系统调用在代码中创建命名管道,需指定管道路径和权限,原型如下:
#include <sys/stat.h>
#include <sys/types.h>
int mkfifo (const char * pathname, mode_t mode) ;
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#define FIFO_PATH "./myfifo"
int main () {
int ret = mkfifo (FIFO_PATH, 0644 );
if (ret == -1 ) {
if (errno != EEXIST) {
perror ("mkfifo error" );
return 1 ;
}
printf ("命名管道已存在\n" );
} else {
printf ("命名管道创建成功\n" );
}
return 0 ;
}
三、命名管道的打开规则(关键!) 命名管道的打开(open())行为与普通文件不同,核心是'读端与写端的同步'——仅当管道的读端和写端都被打开后,通信才能正常进行,具体规则如下:
打开方式 行为描述 读方式打开(O_RDONLY) - 若管道无写端打开:阻塞,直到有进程以写方式打开该管道; - 若指定 O_NONBLOCK:不阻塞,直接返回成功 写方式打开(O_WRONLY) - 若管道无读端打开:阻塞,直到有进程以读方式打开该管道; - 若指定 O_NONBLOCK:不阻塞,返回失败(errno=ENXIO) 读写方式打开(O_RDWR) 不阻塞,直接打开(同时具备读和写权限,可实现单向通信的'自我循环')
注意 :实际开发中,建议读端以 O_RDONLY 打开,写端以 O_WRONLY 打开,避免使用 O_RDWR(可能导致通信逻辑混乱)。
四、命名管道实战案例 下面通过两个独立的程序(writer.c 写端、reader.c 读端)演示命名管道的跨进程通信,实现'写端输入数据,读端接收并打印'的功能。
4.1 案例 1:命名管道实现文件拷贝
file_writer.c:读取本地文件,将内容写入命名管道;
file_reader.c:从命名管道读取内容,写入目标文件。
4.1.1 写端程序(file_writer.c) #include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#define FIFO_PATH "./tp"
#define BUF_SIZE 1024
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main () {
int ret = mkfifo(FIFO_PATH, 0644 );
if (ret == -1 && errno != EEXIST) {
ERR_EXIT("mkfifo error" );
}
int infd = open("abc" , O_RDONLY);
if (infd == -1 ) {
ERR_EXIT("open source file error" );
}
int outfd = open(FIFO_PATH, O_WRONLY);
if (outfd == -1 ) {
ERR_EXIT("open fifo error" );
}
char buf[BUF_SIZE];
int n;
while ((n = read(infd, buf, BUF_SIZE)) > 0 ) {
if (write(outfd, buf, n) != n) {
ERR_EXIT("write to fifo error" );
}
}
close(infd);
close(outfd);
printf ("文件内容写入管道完成\n" );
return 0 ;
}
4.1.2 读端程序(file_reader.c) #include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#define FIFO_PATH "./tp"
#define BUF_SIZE 1024
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main () {
int infd = open(FIFO_PATH, O_RDONLY);
if (infd == -1 ) {
ERR_EXIT("open fifo error" );
}
int outfd = open("abc.bak" , O_WRONLY | O_CREAT | O_TRUNC, 0644 );
if (outfd == -1 ) {
ERR_EXIT("open target file error" );
}
char buf[BUF_SIZE];
int n;
while ((n = read(infd, buf, BUF_SIZE)) > 0 ) {
if (write(outfd, buf, n) != n) {
ERR_EXIT("write to target file error" );
}
}
close(infd);
close(outfd);
unlink(FIFO_PATH);
printf ("文件拷贝完成\n" );
return 0 ;
}
4.1.3 编译与运行
gcc file_writer.c -o writer
gcc file_reader.c -o reader
./reader
./writer
写端读取 abc 文件内容,写入命名管道 tp;
读端从 tp 读取内容,写入 abc.bak;
拷贝完成后,读端删除管道文件,两个程序退出。
4.2 案例 2:命名管道实现 Server-Client 通信
server.c:作为服务端,监听管道,接收客户端消息并打印;
client.c:作为客户端,向管道发送消息,实现双向交互。
4.2.1 前置准备(Makefile && comm.h)
4.2.2 服务端程序(server.c) #include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "comm.h"
int main () {
umask (0 );
int n = mkfifo (fifoname.c_str (), 0666 );
if (n < 0 ) {
perror ("mkfifo" );
return 1 ;
}
int rfd = open (fifoname.c_str (), O_RDONLY);
if (rfd < 0 ) {
perror ("open" );
return 2 ;
}
char inbuffer[1024 ];
while (true ) {
ssize_t n = read (rfd, inbuffer, sizeof (inbuffer) - 1 );
if (n > 0 ) {
inbuffer[n] = 0 ;
std::cout << "client say# " << inbuffer << std::endl;
} else if (n == 0 ) {
break ;
} else {
perror ("read" );
break ;
}
}
close (rfd);
unlink (fifoname.c_str ());
return 0 ;
}
4.2.3 客户端程序(client.c) #include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.h"
int main () {
int wfd = open (fifoname.c_str (), O_WRONLY);
if (wfd < 0 ) {
perror ("open" );
return 1 ;
}
std::string outstring;
while (true ) {
std::cout << "Please Enter@ " ;
std::cin >> outstring;
write (wfd, outstring.c_str (), outstring.size ());
}
close (wfd);
return 0 ;
}
4.2.4 编译与运行
提示 :必须先打开服务端再打开客户端,大家可以自己去尝试一下。如果顺序反了,客户端会因为无法打开管道而阻塞。
我们可以加一点代码更好的去验证一下观察上面的现象啥的。
五、命名管道使用避坑指南和总结
必须手动删除管道文件 :命名管道创建后会残留于文件系统,若不删除,下次创建会报错(errno=EEXIST),建议在通信结束后用 unlink() 删除;
避免单进程同时读写 :虽然可通过 O_RDWR 打开管道实现单进程读写,但会破坏半双工特性,容易导致数据混乱;
处理阻塞场景 :读端未打开时写端会阻塞,写端未打开时读端会阻塞,若需非阻塞操作,可在 open() 时添加 O_NONBLOCK 标志;
数据完整性保证 :当写入数据量 ≤ PIPE_BUF(默认 4096 字节)时,内核保证写入原子性;超过则不保证,需在应用层处理分包;
权限设置合理 :创建管道时权限需开放给通信进程(如 0664 允许同组进程访问),避免因权限不足导致 open() 失败。
总结 :命名管道(FIFO)是匿名管道的重要扩展,其核心价值在于突破了亲缘进程的限制,通过文件系统路径实现任意进程间的通信,且沿用了 Linux 标准的文件操作接口,上手成本低。
命名管道是带文件路径的内核缓冲区,支持跨进程通信
需通过 mkfifo() 或 mkfifo 命令创建,open() 时需遵循读 / 写端同步规则;
实战中可实现文件拷贝、C/S 通信等场景,配合 read()/write() 即可完成数据传输;
与匿名管道相比,命名管道的核心优势是通信范围无限制,生命周期随内核。
结语 本文从概念、创建、打开规则到实战案例,全面覆盖了命名管道的核心知识点。命名管道适合对通信效率要求不极致、需跨进程交互的场景(如日志收集、简单命令交互)。如果需要更高效率的跨进程通信(如高频数据传输),可后续学习共享内存;若需消息结构化传输,可了解消息队列。后续会持续更新 Linux IPC 系列(共享内存、消息队列、信号量),带你从底层吃透进程间通信技术。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online