本文承接上文匿名管道:【Linux】深度解析Linux进程间通信:匿名管道原理、实战与高频问题排查-CSDN博客
深入探讨Linux进程间通信(IPC),以匿名管道为核心,详细阐述其通信目的、实现前提及机制。涵盖数据传输、资源共享等核心目的,说明共享OS资源与系统调用支持的实现前提。重点解析匿名管道和命名管道的机制、特征,包括内核缓冲区管理、文件描述符作用及进程通信的四种关键情况和五大特征。实战应用部分展示进程池、命令行管道的实现及代码示例,同时剖析关键技术细节与常见问题解决方案,为开发者提供全面的技术指导。
目录
deepseek 总结全文:
命名管道
原理:
匿名管道与命名管道的区别
命名管道的打开规则
命名管道的底层实现:
1.创建,删除命名管道:namedPipe.hpp
2.打开管道
函数传参规则
3.读写管道
4.开始通信
让服务端与写入端进行调用:
写入端关闭,读取端也应该关闭
读取端关闭,写入端再写入时被关闭 -- 同匿名管道
deepseek 总结全文:
- 命名管道(FIFO)的核心概念
- 允许无亲缘关系的进程通过文件系统路径访问同一内核缓冲区进行通信。
- 本质是特殊文件,数据不落盘,仅在内核缓冲区中传输。
- 与匿名管道的区别:命名管道通过
mkfifo
创建并需显式打开,匿名管道通过pipe
函数创建。命名管道的操作规则
- 打开规则:
读端阻塞直到有写端打开(非阻塞模式直接返回)。
写端阻塞直到有读端打开(非阻塞模式返回
ENXIO
错误)。读写规则:
读端关闭后,写端会收到
SIGPIPE
信号(进程终止)。写端关闭后,读端
read
返回0,触发退出逻辑。底层实现与代码实战
C++封装类:通过
NamePiped
类管理命名管道的生命周期(创建、打开、读写、删除)。服务端与客户端设计:
服务端:创建管道、以读模式打开、循环读取数据。
客户端:以写模式打开管道、接收用户输入并发送。
同步机制:服务端需等待客户端打开管道后才能继续执行,实现隐式进程同步。
关键代码逻辑
创建与删除:
mkfifo
和unlink
系统调用。读写实现:基于
read
和write
系统函数,缓冲区大小为4KB。异常处理:对
SIGPIPE
信号的容错(如服务端检测到写端关闭后主动退出)。实际运行效果
验证了命名管道在不同场景下的行为(如读写端关闭的响应)。
通过终端截图展示了通信过程及错误处理逻辑。
命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件
原理:
既然是要支持两个毫无关系的进程能够进行通信,那么就需要保证他们打开同一个文件,那么他们就需要在同一个文件内核缓冲区即管道当中写入和读取内容,一个管道属于一个文件,想要找到一个文件就可以通过这个唯一性文件路径进行寻找。而通信时不需要将写入端写入的内容刷新到所打开的文件当中,因此这个文件属于一种特殊文件--->命名管道 ---> 与管道一样是内核级的通信。
文件系统的内容可以看这篇博客:深入理解Linux文件系统:从磁盘结构到inode与挂载-CSDN博客
如图:使用mkfifo指令创建文件,也可以使用mkfifo函数创建文件
如图:可以一直写入
while :;do sleep 1;echo "hello named pipe"; done >> myfifo
可以一直打印
如果先关闭读端(右边),那么左边的写端就会直接崩掉,因为echo是一个内建命令,内建命令是属于是bash在写,操作系统就会直接干掉bash,就会导致这个终端直接退出:
如图:
无论在myfifo中写入多少内容,他的文件大小始终是0
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
命名管道的底层实现:
需要准备四个文件:
namedPipe.hpp --->用于管理命名管道 server.cc client.cc
server
和client
是两个常见的角色名称,这种命名方式是为了模拟一种典型的客户端-服务器(Client-Server)架构
Server(服务器):通常是指提供服务的一方,它等待客户端的请求,并根据请求提供相应的服务。
Client(客户端):是指请求服务的一方,它主动发起请求,等待服务器的响应。
Makefile:
.PHONY: all all: client server client: client.cc g++ -o $@ $^ -std=c++11 server: server.cc g++ -o $@ $^ -std=c++11 .PHONY: clean clean: rm -rf client server
1.创建,删除命名管道:namedPipe.hpp
#include<iostream> #include<cstdio> #include<string> #include<cerrno> //mkfifo #include <sys/types.h> #include <sys/stat.h> const std::string comm_path = "./myfifo"; int CreateNamePipe(const std::string &path) { // int mkfifo(const char *pathname--->文件路径名,缺省权限:mode); int res = mkfifo(path.c_str(), 0666); if(res != 0) { perror("mkfifo"); } return res; } int RemovedNamePipe(const std::string &path) { int res = unlink(path.c_str()); if(res != 0) { perror("unlink"); } return res; }
在
server.cc中进行调用CreateNamePipe,运行结果:创建成功
主要用来删除特殊文件:unlink ---> 删除指定目录下的文件:成功时为0,错误错误码被设置
#include <unistd.h> int unlink(const char *pathname);
在
server.cc中进行调用CreateNamePipe、
RemovedNamePipe,运行结果:删除成功
做好以上的准备工作:
现在我们需要让客户端client进行写入,服务端server进行读取,并且服务端server管理管道命名管道的整个生命周期,修改namedPipe.hpp对管道进行封装:
const std::string comm_path = "./myfifo";
class NamePiped
{
public:
NamePiped(const std::string path)
: _fifo_path(path)
{
int res = mkfifo(path.c_str(), 0666);
if (res != 0)
{
perror("mkfifo");
}
}
~NamePiped()
{
sleep(10);//十秒后关闭管道
int res = unlink(_fifo_path.c_str());
if (res != 0)
{
perror("unlink");
}
}
private:
const std::string _fifo_path;
};
2.打开管道
定义使用者(客户端)和创建者(服务端)的id,文件的fd,只读只写的宏
#define DefaultFd -1 //文件的fd
#define Creater 1
#define User 2
#define Read O_RDONLY //只读
#define Write O_WRONLY //只写
private:
const std::string _fifo_path;
int _id;
int _fd;
- 打开文件的方法不想被看到,给外界提供调用,告诉外界的服务端和客户端文件是否打开成功
- 创建者才需要创建管道,使用者只需要初始化管道
- 析构时服务端关闭管道和文件描述符 , 客户端只关闭文件描述符
class NamePiped
{
private:
//打开文件的方法不想被看到,给外界提供调用,告诉外界的服务端()和客户端是否打开成功
bool OpenNamedPipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0)
return false;
return true;
}
public:
NamePiped(const std::string path, int who)
: _fifo_path(path), _id(who), _fd(DefaultFd)
{
// 是创建者才需要创建管道,使用者只需要初始化管道就可以
if (_id == Creater)
{
int res = mkfifo(path.c_str(), 0666);
if (res != 0)
{
perror("mkfifo");
}
std::cout << "creater create named pipe" << std::endl;
}
}
bool OpenForRead()
{
return OpenNamedPipe(Read);
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);
}
~NamePiped()
{
// 是创建者才可以删除管道,使用者不用管
if (_id == Creater)
{
int res = unlink(_fifo_path.c_str());
if (res != 0)
{
perror("unlink");
}
std::cout << "creater free named pipe" << std::endl;
}
if(_fd != DefaultFd) close(_fd);
}
private:
const std::string _fifo_path;
int _id;
int _fd;
};
服务端以读的方式打开文件 ,客户端以写的方式打开文件
//服务端
int main()
{
NamePiped fifo(comm_path, Creater);
// 服务端以读的方式打开文件
fifo.OpenForRead();
return 0;
}
// 客户端
int main()
{
NamePiped fifo(comm_path, User);
// 客户端以写的方式打开文件
fifo.OpenForWrite();
return 0;
}
函数传参规则
const &: const std::string &XXX 纯输入
* : std::string * 纯输出
& : std::string & 输入输出
3.读写管道
通信基本大小4KB
#define BaseSize 4096
int ReadNamedPipe(std::string *out)
{
char buffer[BaseSize];
//将_fd文件的内容读取到buffer
int n = read(_fd, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
//写入管道
int WriteNamedPipe(const std::string &in)
{
// 向_fd文件写入内容,写in, 写入in的大小
return write(_fd, in.c_str(), in.size());
}
4.开始通信
让服务端与写入端进行调用:
//服务端 int main() { NamePiped fifo(comm_path, Creater); // 服务端以读的方式打开文件 if (fifo.OpenForRead()) { std::cout << "server open named pipe done" << std::endl; sleep(3); while (true) { std::string message; int n = fifo.ReadNamedPipe(&message); if (n > 0) { std::cout << "Client Say> " << message << std::endl; } } } return 0; } //客户端 int main() { NamePiped fifo(comm_path, User); // 客户端以写的方式打开文件 if (fifo.OpenForWrite()) { std::cout << "client open named pipe done" << std::endl; while (true) { std::cout << "Please Enter> "; std::string message; std::getline(std::cin, message); fifo.WriteNamedPipe(message); } } return 0; }
运行结果:
对于读取端服务端而言,已经将文件打开,但在写入端客户端未打开写入端时,是处于等待状态的,被阻塞在OpenForRead()调用中。直到客户端编译,打开了管道的写入端,服务端再继续执行,打开读入端。这是一种变相的进程同步。(与匿名管道的区别)
在
由上:客户端和服务端没有任何关系,实现了通信
写入端关闭,读取端也应该关闭
当写入端关闭后,读取端因为read的返回值为0,应该会退出;因此还需要修改服务端代码
int main() { NamePiped fifo(comm_path, Creater); if (fifo.OpenForRead()) { std::cout << "server open named pipe done" << std::endl; sleep(3); while (true) { std::string message; int n = fifo.ReadNamedPipe(&message); if (n > 0) { std::cout << "Client Say> " << message << std::endl; } else if(n == 0) { std::cout << "Client quit, Server Too!" << std::endl; break; } else { std::cout << "fifo.ReadNamedPipe Error" << std::endl; break; } } } return 0; }
运行结果:写入端关闭,读取端也被关闭
读取端关闭,写入端再写入时被关闭 -- 同匿名管道
这是因为在写入端再次要写入内容的时候,OS判断出读取端已关闭检测出了异常,对写入端发送了13号SIGPIPE的信号,杀掉了写入端。
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。