ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

《从零入门Linux系统篇(三十八):进程间通信篇·三——命名管道详解:从FIFO创建到多进程通信实战》

2026/9/5 7:58:42 拓冰建站 浏览量
《从零入门Linux系统篇(三十八):进程间通信篇·三——命名管道详解:从FIFO创建到多进程通信实战》 上一篇我们把匿名管道扒了个底朝天。东西是好东西但一条硬伤实在太刺眼它只认血缘关系。想用匿名管道通信可以前提是你们得是父子、兄弟。换句话说要么是亲生的要么是一个娘胎里出来的。但现实开发里我们往往需要让两个八竿子打不着的进程聊上几句一个服务器进程一个客户端进程谁也不是谁生的却要频繁互传数据。这时候匿名管道就像个只让亲戚串门的门卫直接把你拦在外面。于是命名管道登场了。命名管道专治“没血缘”。它支持两个毫不相干的进程进行通信。名字怎么来的“命名”就是因为它不再像匿名管道那样神龙见首不见尾而是在文件系统里有个真实的名字。两个进程只要知道这个名字就能各自打开管道的一端开始通信跟它是谁生的、归谁管一点关系都没有。废话不多说我们直接开讲。目录一、认识命名管道从匿名管道到FIFO二、创建命名管道2.1 使用命令行创建命名管道实现最简单的通信2.2 使用系统调用创建命名管道三、命名管道通信实战3.1 实战一模拟“打电话”的通信过程3.1.1 运行测试与通信验证3.2 实战二利用命名管道传输文件内容一、认识命名管道从匿名管道到FIFO命名管道是怎么想出来的归根到底还是那句老话进程间通信必须让两个进程看到同一块资源。而这回资源依然是文件只不过不再是那个藏在内存里的匿名管道文件了。两个进程只要打开同一个路径下的同一个文件就像两个人盯着同一块屏幕一样一个往上写一个从上读就这么简单。文件有唯一的路径有正经的名字我们便给这种通信方式起了个统一的名号命名管道。接下来可能有两个疑问会冒出来。疑问一两个进程打开同一个文件这个文件不会在内存里被加载两次吗不会。内核不会干那种又浪费空间又浪费时间的蠢事。同一个文件被多次打开内核心里有数该共享的东西它绝不多存一份。疑问二文件在内核里的映射到底是什么结构这块要稍微抠一下字眼。一个文件在内核中的映射其实并不包括struct file只有inode ops读写接口和缓冲区。换句话说内核为文件维护的核心是“怎么读写”和“数据暂存在哪”至于“谁正开着它、读写到哪儿了”这些状态是另一层结构在管。这个概念以后讲文件系统的时候还会反复碰到现在先混个脸熟。二、创建命名管道命名管道这文件跟普通文件不是一个物种。它只进不出——数据进去了就待在内存里绝不需要刷回磁盘。而且它天生自带“排队”属性谁先写谁后写井井有条不会乱序。这些本事普通文件一个都做不到。2.1 使用命令行创建命名管道实现最简单的通信创建命名管道命令就一个mkfifo 文件名删除它也简单unlink 文件名别小看这两条命令。mkfifo一执行文件系统里就多了一个特殊的“管道文件”。它长得像个普通文件身份却完全不同它只是个通信入口不是用来存数据的。来我们直接上命令行动手玩一把。先在终端A里创建管道并往里写点东西2.2 使用系统调用创建命名管道命令行里用mkfifo建管道不过是浅尝辄止。真要写程序还得请出对应的系统调用#include sys/types.h #include sys/stat.h int mkfifo(const char *pathname, mode_t mode);参数一眼就懂pathname带路径的管道文件名。这是命名管道区别于匿名管道的灵魂——它得有个正经名字。mode权限掩码。管道文件也是文件该有的权限一样不少谁读谁写这里说了算。返回值照旧成功返回0失败返回-1并设置errno告诉你错在哪。光看接口不过瘾我们直接上两个实战例子看看命名管道在真实开发里到底是怎么干活的。一个只管读一个只管写两个互不认识的程序靠同一根管道牵上线。三、命名管道通信实战理论讲完上硬菜。我们直接用一个“打电话”的实战案例看看命名管道到底是怎么在两个陌生进程之间搭起通话线路的。3.1 实战一模拟“打电话”的通信过程两个进程一个叫张三负责“说话”一个叫李四负责“接听”。张三把消息写进管道李四从管道里读出来一来一回像极了两个人隔着电话线聊天。Makefile:先把编译流程管起来两个程序一起编ALL : Mr_List Mr_Zhang Mr_List : Mr_List.cpp g -o $ $^ Mr_Zhang : Mr_Zhang.cpp g -o $ $^ .PHONY : clean clean : rm -f Mr_List Mr_Zhang fifo共享头文件 Common.hpp两个程序都要用到的东西抽出来放一起。最关键的是那根管道的路径两个人得约定好不然各找各的电话线就接不上了。#ifndef __COMMON_NAMEDPIPE_ #define __COMMON_NAMEDPIPE_ #include iostream #include unistd.h #include sys/types.h #include sys/stat.h #include sys/wait.h #include errno.h #include cstdlib #include string #include fcntl.h #include cstring #define PIPE_FILE ./fifo #define ERR_FIFO(m) do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (false) #endif这里重点说说ERR_FIFO这个宏。它把perror和exit打包成一条“组合拳”还套了个do-while(false)。这么写是为了让宏在任何if-else里都能安全展开不管前面有没有大括号都不会出语法问题。这是宏定义里的经典安全套路。另外那个行尾的\别打错。有初学者会把行延续符写成/你想想\跟注释用的//方向相反记这个就够。张三方只管写张三负责“说”。他先创建管道然后打开写端接着读你从键盘敲进去的每一句话原封不动写进管道。#include Common.hpp int main() { umask(0); int n mkfifo(PIPE_FILE, 0666); if (n -1) ERR_FIFO(mkfifo); int wfd open(PIPE_FILE, O_WRONLY); if (wfd -1) ERR_FIFO(open); else { while (true) { std::cout #PleaseEnter:; std::cout.flush(); char buffer[1024]; memset(buffer, 0, sizeof(buffer)); int m read(0, buffer, sizeof(buffer)); if (m 0) ERR_FIFO(read); else { int i write(wfd, buffer, m); if (i 0) ERR_FIFO(write); } } } return 0; }注意一个细节这里用的是read(0, ...)也就是从标准输入读数据而不是直接写死std::cin。因为系统编程里文件描述符就是最底层的语言读键盘、读管道统一都走read。李四方只管读李四负责“听”。他一启动就去打开同一个管道的读端然后循环读张三说一句他就打印一句。#include Common.hpp int main() { int rfd open(PIPE_FILE, O_RDONLY); if (rfd -1) ERR_FIFO(open); while (true) { char buffer[1024]; memset(buffer, 0, sizeof(buffer)); std::cout #PleaseWait ... std::endl; int n read(rfd, buffer, sizeof(buffer) - 1); if (n 0) ERR_FIFO(read); else if (n 0) { std::cout Mr_Zhang Hang Up The Telephone std::endl; break; } else { buffer[n] \0; std::cout Mr_Zhang say : buffer; } } return 0; }这里read返回0意味着张三那边把写端关了——电话挂断了。李四读到信号打印一句“挂电话了”然后优雅退出循环。3.1.1 运行测试与通信验证3.2 实战二利用命名管道传输文件内容这个案例比“打电话”更直接服务端把Source.txt里的内容读出来通过命名管道发出去客户端收到后写进destnation.txt。全程没有一句“聊天”就是纯粹的数据搬运。代码结构上这次换成了面向对象把管道的创建、销毁、打开、读写全塞进一个NamedPipe类里逻辑更清爽。Makefile:ALL : Client Server Client : Client.cpp g -o $ $^ Server : Server.cpp g -o $ $^ .PHONY: clean clean : rm -f Client Server公共头文件Common.hpp这里面放的就是NamedPipe类的完整实现管道的生老病死、读写操作全被封装成一个个方法。#ifndef __COMMON_NAMEDPIPE_ #define __COMMON_NAMEDPIPE_ #include iostream #include unistd.h #include sys/types.h #include sys/stat.h #include sys/wait.h #include errno.h #include cstdlib #include string #include fcntl.h #include cstring #define PIPE_FILE ./fifo #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (false) class NamedPipe { public: void Create(std::string file PIPE_FILE) { umask(0); int n mkfifo(file.c_str(), 0666); if (n -1) ERR_EXIT(mkfifo); } void Destory(std::string file PIPE_FILE) { int n unlink(file.c_str()); if (n -1) ERR_EXIT(unlink); } void OpenForRead(std::string file PIPE_FILE) { _pipefd open(file.c_str(), O_RDONLY); if (_pipefd -1) ERR_EXIT(open_read); } void OpenForWrite(std::string file PIPE_FILE) { _pipefd open(file.c_str(), O_WRONLY); if (_pipefd -1) ERR_EXIT(open_write); } void Read(int outfd) { char _buffer[1024]; while (read(_pipefd, _buffer, sizeof(_buffer)) ! 0) { int n write(outfd, _buffer, sizeof(_buffer)); if (n 0) ERR_EXIT(write); memset(_buffer, 0, sizeof(_buffer)); } } void Write(int infd) { char _buffer[1024]; while (read(infd, _buffer, sizeof(_buffer)) ! 0) { int n write(_pipefd, _buffer, sizeof(_buffer)); if (n 0) ERR_EXIT(write); memset(_buffer, 0, sizeof(_buffer)); } } private: int _pipefd; }; #endif这里值得特别说明一个设计选择Create和Destory没被塞进构造函数和析构函数。为什么因为创建管道的任务只需要通信双方的其中一方来做。如果放进构造函数那不管谁创建对象都得先造一根管道逻辑上就不灵活了。当然也不是不能做把其他方法再封装进一个opt类也能实现但为了简洁直观现在这样“显式创建、显式销毁”最清楚。服务端读文件写管道服务端先打开管道读端接着打开Source.txt然后把文件内容从管道里读出来写进destnation.txt。#include Common.hpp int main() { NamedPipe pipe; pipe.OpenForRead(PIPE_FILE); int outfd open(destnation.txt, O_WRONLY); if (outfd -1) ERR_EXIT(open); pipe.Read(outfd); return 0; }客户端读文件写管道客户端则是先创建管道再打开写端然后从Source.txt里读内容一段一段灌进管道最后销毁管道。#include Common.hpp int main() { NamedPipe pipe; pipe.Create(PIPE_FILE); pipe.OpenForWrite(PIPE_FILE); int infd open(Source.txt, O_RDONLY); if (infd -1) ERR_EXIT(open); pipe.Write(infd); pipe.Destory(); return 0; }两个案例的共同点无论是“打电话”还是“传文件”本质都是同一套玩法两个互不认识的进程靠一个共同约定的管道文件名找到彼此开始通信。打电话是“人在说话”传文件是“机器在搬数据”。通信方式没变变的只是数据的来源和去向。命名管道的价值正在于它把“通信”这件事从进程血缘里解放出来让任意两个进程都能通过一个文件名牵上线。如果这篇文章对你有帮助别忘了点个赞、点个收藏、点个关注。你的每一次反馈都是我继续硬核输出的最大动力。