Linux -- 进程间通信之匿名管道

在这里插入图片描述
博客中涉及代码已全部上传至Gitee,有需要请自行下载

目录

  • 前言
  • 通信基础
    • 管道
  • 匿名管道
    • 第一步:创建管道
    • 第二步:创建子进程
    • 第三步:开始通信
    • 第四步:结束通信
  • 匿名管道通信代码实现
    • 四种特殊情景
  • 基于匿名管道的多进程控制
    • 对象管理
    • 体系构建
  • 总结


前言

通信是指人与人或人与自然之间通过某种行为或媒介进行的信息交流与传递,从广义上指需要信息的双方或多方在不违背各自意愿的情况下采用任意方法、任意媒质,将信息从某方准确安全地传送到另方。在生活中,我们无时无刻都在于他人进行通信,通信的本质就是将我们的信息进行相互传递共享,让我们可以共同来配合做某一些事情来提高效率。

在这里插入图片描述

因此,我们的进程间也应该要进行相互的通信,相互的配合着做一些事情。

进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。 IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享内存、Socket(套接字)等,其中 Socket和支持不同主机上的两个进程IPC。本篇文章介绍的是管道通信。

通信基础

我们知道进程与进程间是相互独立,互不影响的,但现在我们想让两个互不干扰的进程进行通信,首先就要打破这个性质,让两个进程间建立起一座通信的桥梁,而这个桥梁就是我们的管道。

管道

管道是一种半双工的通信方式,只能用于父进程与子进程的通信,或者同祖先的子进程(具有血缘关系的进程)之间的通信。在Linux中,管道是UNIX环境中历史最悠久的进程间通信方式之一。

在这里插入图片描述

而我们利用管道通信的本质就是打破进程间相互独立从而看不到同一份资源的窘境,管道的两端分别链接两个进程,一个进程将自己要发送的消息或者资源放到管道文件中,另一个进程就可以从管道文件中将消息和资源读取出来。这样一放一取的过程,我们就完成了两个进程间的通信。✌

匿名管道

第一步:创建管道

匿名管道,顾名思义就是没有名字的管道文件,匿名管道在系统中没有实名,它只是进程的一种资源,会随着进程的结束而被系统清除。

创建匿名管道我们可以使用系统给我们提供的系统调用接口pipe,接口的返回值是int类型,如果创建成功就返回0,失败就返回-1,并设置错误码。pipe接口只有一个输出型参数,是一个数组,给我们返回管道的文件描述符,pipefd[0]表示读端,pipefd[1]表示写端。之后我们就可以拿这两个文件描述符来做文章了。

在这里插入图片描述
在这里插入图片描述

第二步:创建子进程

通过创建子进程,子进程会继承了父进程的各种属性(包括文件描述符表),从而子进程和父进程就看到了同一个管道文件,也是是看到了同一份资源!因为管道只支持单向通信,所以我们还需要关闭父子进程多余的读写端,让一个进程专门发送数据,一个进程专门读取数据。

在这里插入图片描述

第三步:开始通信

在完成了前面两步之后,我们就已经让两个彼此没有交集的进程有了一丝丝的牵连,接下来我们就可以进行愉快的通信了。👏(额…单向的通信也算通信吗?)
在这里插入图片描述

第四步:结束通信

在通信结束后,我们还需要将自己相应的端口关闭,当管道文件的读写两端都没有进程链接时,管道文件会自动销毁,不需要我们去手动释放空间。在此我们还需要学习一个系统调用接口close

close 函数的作用是关闭一个打开的文件描述符,释放对应的资源,包括操作系统中的文件表项等。如果文件描述符是打开的,它会被关闭;如果文件描述符已经关闭,调用 close 函数将没有任何影响,也不会报错。

在使用close函数时,我们只需要传入我们打开的文件描述符即可,当返回值为0时表示关闭成功,-1表示关闭失败,并设置错误码。

在这里插入图片描述

匿名管道通信代码实现

在上述一系列分析之后,成功搞清楚了匿名管道通信的具体流程,现在我们可以来设计这样一个程序,我们创建一个管道,在拿到相应的文件描述符后在创建一个子进程,关掉父进程的写端和子进程的读端。让父进程进行从管道读取,子进程向管道写入,在子进程写入5条消息后子进程退出,父进程读取完毕后也退出。

以下是这个简单设计的代码实现:

```cpp
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <string.h>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>int main()
{// 先让不同的进程看到同一份资源// 创建管道int pipefd[2];int ret = pipe(pipefd);if (ret == -1){std::cout << "pipe error" << errno << " : " << strerror(errno) << std::endl;}// 创建子进程pid_t pid = fork();if (pid == -1) // 创建失败{std::cout << errno << " : " << strerror(errno) << std::endl;exit(1);}// 创建子进程成功// 让子进程写,父进程读if (pid == 0){// 子进程// 关闭不需要的fdclose(pipefd[0]);// 开始通信 -- 向父进程写入数据for (int i = 0; i < 5; i++){std::cout << "我正在发送第 " << i + 1 << " 条数据" << std::endl;char buffer[] = "i am father";write(pipefd[1], buffer, sizeof(buffer) - 1);     sleep(1);}close(pipefd[1]);std::cout << "发送完成,我退出了" << std::endl;exit(0);}// 父进程// 关闭不需要的fdclose(pipefd[1]);// for(int i = 0; i < 5; i++)while (true){char buffer[1024];ssize_t ret = read(pipefd[0], buffer, sizeof(buffer) - 1);buffer[ret] = '\0';if (ret > 0){std::cout << "这是我读到的数据 : " << buffer << std::endl;}else if (ret == 0){std::cout << "通信完毕,退出!" << std::endl;break;}else{std::cout << errno << "读取错误 : " << strerror(errno) << std::endl;exit(1);}sleep(1);}close(pipefd[0]);int status = 0;waitpid(pid, &status, 0);std::cout << "sig : " << (status & 0x7F) << std::endl;return 0;
}

运行结果正如我们设想的一样,子进程在发送完5条消息后退出,父进程也在读取完毕5条消息后退出。

在这里插入图片描述

四种特殊情景

  1. 读端一直读取,写端直接退出

如果管道的写端直接退出而读端仍在不断尝试读取数据时,如果管道中还有数据没有读取完毕,读端会继续读取管道中的数据,直到管道为空。当管道中写段关闭并且管道中没有数据时,read会返回0,我们可以从返回值判断通信的状况。

  1. 写端一直写入,读端直接退出

如果管道的写段一直在写入,而读端直接退出,写入的进程会收到OS发送的SIGPIPE信号从而终止进程。

  1. 读端快速读取,写端慢速写入

当读端读取速度大于写段的写入速度时,那么每一次读取会将上一次写端写入的所有数据一次性读取完毕。

  1. 读端慢速读取,写段快速写入

当写端的写入速度小于读端的读取速度时,在它将管道中的数据读取完后,下一次读取时会阻塞等待,一直到写端的下一次写入数据完毕时才会进行读取。

基于匿名管道的多进程控制

我们在上述代码中让子进程写入,父进程读取,那当然我们也可以让父进程写入,子进程进行读取。我们先创建一批管道和一批进程管道和进程一一对应,在记录下子进程的PID和对应写入的文件描述符后,当我们想让哪个子进程处理什么样的任务时,我们只需要向对应的文件描述符写入对应的任务号后,子进程就可以去执行对应的任务。这样我们就完成了基于匿名管道的多进程控制。

对象管理

我们可以用类来对子进程的PID和对应文件描述符进行封装,如下代码:

class EndPoint
{
public:pid_t _pid;int _fd;public:EndPoint(pid_t pid, int fd) : _pid(pid), _fd(fd){};~EndPoint(){};
};

然后在对需要执行的一批任务进行结构化管理,这里只是进行简单试验,所以任务只创建3个并只是简单的打印区分,如下代码:

using func = std::function<void(void)>;
void Mysql()
{std::cout << "PID : " << getpid() <<  " MySQL is running......." << std::endl;
}void Print()
{std::cout << "PID : " << getpid() << " Printing........." << std::endl;
}void Calculate()
{std::cout << "PID : " << getpid() << " Calculate........." << std::endl;
}
class Task
{
public:Task(){funcs.push_back(Mysql);funcs.push_back(Print);funcs.push_back(Calculate);}void Excute(int taskid){if (taskid >= 0 && taskid < funcs.size())funcs[taskid]();}~Task() {}public:std::vector<func> funcs;
};

体系构建

在将对应的任务和子进程管理好后,我们就可以对整个框架进行搭建,首先我们需要创建出一批管道和进程。关闭掉相应的读写端后将子进程的PID和写入端文件描述符构建出一个对象,再将这个对象加入到顺序表中进行统一管理。再继续就可以选取对应的进程和相应的任务,将这个任务号写入到匿名管道中让子进程进行读取执行。就此,我们可以写出如下的代码:

void CreateProccess(std::vector<EndPoint> &ep)
{std::vector<int> fds;for (int i = 0; i < PROCCESS_NUM; i++){// 创建管道int pipefd[2];int ret = pipe(pipefd);if (ret == -1){std::cout << errno << " : " << strerror(errno) << std::endl;exit(1);}// 创建子进程pid_t id = fork();assert(id >= 0);if (id == 0){for (auto &e : fds)close(e);// 子进程//  关闭多余文件描述符close(pipefd[1]);// 处理WaitTask(pipefd[0]);// 关闭剩下文件描述符close(pipefd[0]);exit(0);}// 父进程//  关闭多余文件描述符close(pipefd[0]);ep.push_back({id, pipefd[1]});fds.push_back(pipefd[1]);}
}void WaitTask(int fd)
{while (1){int taskid;ssize_t ret = read(fd, &taskid, sizeof(taskid));if (ret == sizeof(int)){t.Excute(taskid);}else if (ret == 0){std::cout << "exit........" << std::endl;break;}else{std::cout << "error" << std::endl;break;}}
}int Enum()
{int ret;std::cout << "---------------------------------" << std::endl;std::cout << "---0.数据库任务------1.打印任务---" << std::endl;std::cout << "---2.计算任务--------3.退出------" << std::endl;std::cout << "---------------------------------" << std::endl;std::cout << "---------------------------------" << std::endl;std::cout << "请输入你的选择 # " << std::endl;std::cin >> ret;return ret;
}void WaitProccess(std::vector<EndPoint> &ep)
{// 关闭父进程写端// wait子进程for (int i = ep.size() - 1; i >= 0; i--){close(ep[i]._fd);waitpid(ep[i]._pid, nullptr, 0);}
}int main()
{std::vector<EndPoint> ep; Task task;                // 任务列表// 创建一批进程CreateProccess(ep);while (1){// 选取任务int ret = Enum();if (ret == 3)break;if (ret < 0 || ret > 3)continue;// int taskid = rand() % task.funcs.size();//  选择进程int pid = rand() % PROCCESS_NUM;// 发送给相应的进程执行write(ep[pid]._fd, &ret, sizeof(ret));}WaitProccess(ep);return 0;
}

最后,我们就可以根据提示输入相应的任务号码让系统随机的选择进程去执行任务。

在这里插入图片描述

我们也可以进行修改最后让我们自己指定进程去执行,这个有兴趣的话也可以试试。

总结

匿名管道是一种在进程间进行通信的机制,通常用于在父子进程或者兄弟进程之间传递数据。以下是匿名管道的一些主要特性:

  • 单向通信:匿名管道是单向的,分为读端和写端。一个进程可以写入数据到管道的写端,而另一个进程可以从管道的读端读取数据。这种单向性能确保了数据在两个进程之间的有序传递。

  • 进程间通信:匿名管道通常用于父子进程或者兄弟进程之间的通信。父进程创建管道并传递给子进程,或者两个独立的进程可以通过继承同一个管道来进行通信。

  • 基于文件描述符:匿名管道使用文件描述符来表示,通常通过系统调用 pipe() 来创建。一个管道有两个文件描述符,一个用于读取(管道的读端),另一个用于写入(管道的写端)。

  • 阻塞式写入和读取:当管道的缓冲区满时,写入进程将被阻塞,直到有足够的空间可以写入数据。同样,如果管道为空,读取进程将被阻塞,直到有数据可供读取。

  • 匿名性质:匿名管道不关联具体的文件系统路径,因此在进程间通信时,它们通常不需要文件系统中的实际文件。这种匿名性质使得进程可以轻松地进行通信而不必担心文件路径的问题。

  • 生命周期:匿名管道通常在创建进程时被创建,而且只在相关进程存在时才有效。当相关进程终止时,管道也会被自动关闭和销毁。

  • 数据传输有序:匿名管道用于有序的数据传输,保证写入管道的数据按顺序被读取。这是因为管道是一个先进先出(FIFO)的数据结构。

  • 有限容量:匿名管道通常具有有限的缓冲区容量,一旦管道的缓冲区满了,进一步的写入操作将被阻塞,直到有读取进程将数据读取出来,腾出空间。

总的来说,匿名管道是一种用于进程间通信的轻量级、有序的机制,常用于父子进程或者兄弟进程之间的数据传递。然而,匿名管道只支持单向通信,且容量有限,因此在某些情况下可能需要考虑其他进程间通信方式,如命名管道、消息队列或共享内存等。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/142845.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是城市坐标系,与国家坐标系的区别?

文章目录 先说国家坐标系什么是城市坐标系城市坐标系建设规范常见的城市坐标系 先说国家坐标系 先1954年我国建立了第一代国家大地坐标系统&#xff0c;即北京54坐标系&#xff0c;英文缩写BJ54&#xff0c;坐标原点在苏联&#xff0c;椭球参数直接拿苏联的。第一代坐标系的椭…

【C++杂货铺】一颗具有搜索功能的二叉树

文章目录 一、二叉搜索树概念二、二叉搜索树的操作2.1 二叉搜索树的查找2.2 二叉搜索树的插入2.3 二叉搜索树的删除 三、二叉搜索树的实现3.1 BinarySearchTreeNode&#xff08;结点类&#xff09;3.2 BinarySearchTree&#xff08;二叉搜索树类&#xff09;3.2.1 框架3.2.2 in…

Linux系统100条命令:关于Ubuntu和 CentOS 7 相同功能的不同的终端操作命令

安装软件包&#xff1a; Ubuntu&#xff1a;apt-get install package_name CentOS 7&#xff1a;yum install package_name 更新软件包列表&#xff1a; Ubuntu&#xff1a;apt-get update CentOS 7&#xff1a;yum update 卸载软件包&#xff1a; Ubuntu&#xff1a;apt-…

Diffusion Autoencoders: Toward a Meaningful and Decodable Representation

Diffusion Autoencoders: Toward a Meaningful and Decodable Representation (Paper reading) Konpat Preechakul, VISTEC, Thailand, CVPR22 Oral, Cited:117, Code, Paper 1. 前言 扩散概率模型 (DPM) 在图像生成方面取得了显着的质量&#xff0c;可与 GAN 相媲美。但是与…

C++ - 红黑树 介绍 和 实现

前言 前面 学习了 AVL树&#xff0c;AVL树虽然在 查找方面始终拥有 O(log N &#xff09;的极高效率&#xff0c;但是&#xff0c;AVL 树在插入 ,删除等等 修改的操作当中非常的麻烦&#xff0c;尤其是 删除操作&#xff0c;在实现当中细节非常多&#xff0c;在实现上非常难掌控…

如何套用模板制作大屏?

在山海鲸可视化的资源中心里内置了大量的二维、三维大屏模板&#xff0c;大家可以根据需要找到自己想要的模板&#xff0c;然后点击下载直接进行使用。 有需要可自行前往哔哩哔哩账号中观看相关内容的视频教程↓↓↓ 山海鲸可视化的个人空间-山海鲸可视化个人主页-哔哩哔哩视频…

cocos2dx查看版本号的方法

打开文件&#xff1a;项目根目录\frameworks\cocos2d-x\docs\RELEASE_NOTES.md 知道引擎版本号的意义&#xff1a; 1.面试中经常被问到(面试官想知道你会不会查版本号&#xff0c;你会查也不一定会去看&#xff0c;如果你去看了说明你是一个有心人&#xff0c;或者想深入研究下…

Java内存泄漏知识(软引用、弱引用等)

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 未经允许不得转载 目录 一、导读二、概览三、相关知识3.1 内存…

工作中Git管理项目和常见问题处理

工作中Git管理项目和常见问题处理 Git仓库的管理方式为什么会出现无法push到线上处理方法 Git仓库的管理方式 共用统一仓库,不同开发人员使用不同分支 步骤 下载代码 git clone <url>查看分支 git branch创建并切换分支 git checkout -b dev分支名称保持和远程分支一…

计算机视觉与深度学习-图像分割-视觉识别任务03-实例分割-【北邮鲁鹏】

目录 参考定义Mark R-CNN结构思路Mask R-CNN训练阶段使用的Mask样例Mask R-CNN实例分割结果Mask R-CNN检测姿态 参考 论文题目&#xff1a;Mask R-CNN 论文链接&#xff1a;论文下载 论文代码&#xff1a;Facebook代码链接&#xff1b;Tensorflow版本代码链接&#xff1b; K…

微信里怎么添加阅读付费链接

在微信中添加阅读付费链接为主题&#xff0c;首先需要开通微信支付商户号&#xff0c;然后创建自定义菜单&#xff0c;并设置跳转到付费链接的逻辑。以下是详细步骤&#xff1a; 注册并开通微信支付商户号 在微信开放平台上注册并开通微信支付商户号。这一步需要营业执照、法…

出现 conda虚拟环境默认放在C盘 解决方法

目录 1. 问题所示2. 原理分析3. 解决方法3.1 方法一3.2 方法二1. 问题所示 通过conda配置虚拟环境的时候,由于安装在D盘下,但是配置的环境默认都给我放C盘 通过如下命令:conda env list,最后查看该环境的确在C盘下 2. 原理分析 究其根本原因,这是因为默认路径没有足够的…

go sync.Map包装过的对象nil值的判断

被sync.Map包装过的nil 对象&#xff0c;是不能直接用if xxx nil的方式来判断的 func testnil() *interface{} {return nil }func main() {var ptr *interface{}test : testnil()//p &Person{}fmt.Printf("ptr 的值为 : %v\n", ptr)fmt.Printf("ptr 的值…

uni-app:实现元素在屏幕中的居中(绝对定位absolute)

一、实现水平居中 效果 代码 <template><view><view class"center">我需要居中</view></view> </template><style>.center {position: absolute;left:50%;transform: translateX(-50%);border:1px solid black;} </s…

20个提升效率的JS简写技巧,告别屎山!

JavaScript 中有很多简写技巧&#xff0c;可以缩短代码长度、减少冗余&#xff0c;并且提高代码的可读性和可维护性。本文将介绍 20 个提升效率的 JS 简写技巧&#xff0c;助你告别屎山&#xff0c;轻松编写优雅的代码&#xff01; 移除数组假值 可以使用 filter() 结合 Bool…

智能机器学习:人工智能的下一个巨大飞跃

文章目录 第1节&#xff1a;智能机器学习的背景1.1 传统机器学习1.2 人工智能 第2节&#xff1a;智能机器学习的定义2.1 智能机器学习的原理2.1.1 自主学习2.1.2 强化学习2.1.3 自适应性 2.2 智能机器学习的关键技术2.2.1 深度学习2.2.2 强化学习算法2.2.3 自然语言处理&#x…

Stable Diffusion 参数介绍及用法

大模型 CheckPoint 介绍 作用&#xff1a;定调了作图风格&#xff0c;可以理解为指挥者 安装路径&#xff1a;models/Stable-diffusion 推荐&#xff1a; AnythingV5Ink_v32Ink.safetensors cuteyukimixAdorable_midchapter2.safetensors manmaruMix_v10.safetensors counterf…

线性约束最小方差准则(LCMV)波束形成算法仿真

常规波束形成仅能使得主波束对准目标方向&#xff0c;从而在噪声环境下检测到目标&#xff0c;但无法对复杂多变的干扰做出响应&#xff0c;所以不能称之为真正意义上的自适应滤波。自适应阵列处理指的是采用自适应算法对空间阵列接收的混合信号进行处理&#xff0c;又可称为自…

晨控CK-FR08系列读写器与LS可编程逻辑控制器MODBUSRTU连接手册

晨控CK-FR08系列读写器与LS可编程逻辑控制器MODBUSRTU连接手册 晨控CK-FR08是一款基于射频识别技术的高频RFID标签读卡器&#xff0c;读卡器工作频率为13.56MHZ&#xff0c;支持对I-CODE 2、I-CODE SLI等符合ISO15693国际标准协议格式标签的读取。读卡器内部集成了射频部分通信…

【Linux】生产者和消费者模型

生产者和消费者概念基于BlockingQueue的生产者消费者模型全部代码 生产者和消费者概念 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。 生产者和消费者彼此之间不直接通讯&#xff0c;而通过这个容器来通讯&#xff0c;所以生产者生产完数据之后不用等待…