从零开始学习管道:进程通信的概念,特点和示例

在这里插入图片描述

📟作者主页:慢热的陕西人

🌴专栏链接:Linux

📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言

本博客主要内容通过进程通信的概念,引入管道,实操了管道的五种特性和四种场景,以及对应的管道的特点最后我们写了一个例子让我们对于管道,重定向等的只是更加的印象深刻

文章目录

    • 1.进程通信的介绍
      • 1.1进程通信目的
      • 1.2进程间通信发展
      • 1.3进程通信的分类
    • 2.管道
      • 2.1什么是管道
      • 2.2实操一下见见管道
      • 2.3管道的原理
      • 2.4直接编写样例代码
      • 2.5做实验,推导出管道
        • a.五种特性
        • b.四种场景:
      • 2.6管道的特点
      • 2.7添加一点设计,来完成一个基本的多进程控制代码

1.进程通信的介绍

1.1进程通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

1.2进程间通信发展

  • 管道

  • System V进程间通信

    System V 是一种基于本地版的进程通信,它是不能跨网络的,因为它只能本主机的内部进行进程间的通信,所以这也是它为什么会现在被边缘化的原因。关于System V我们只需要了解一个共享内存即可.

  • POSIX进程间通信

    POSIX 是一种基于网络版的进程通信。

    System V 和 POSIX相当于是进程间的通信的两套标准。

1.3进程通信的分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

①首先进程是具有独立性的—无疑增加了通信的成本

②要让两个不同的进程通信,进行通信,前提条件是:先让两个进程,看到同一份“资源”。(进程的本质和前提)

③任何进程通信手段都需要遵循如下的原则解决进程间的通信问题:

我们在操作系统内创建一份公共的资源,例如一段缓冲区,它既不属于进程A,也不属于进程B,那么我们这一份资源既可以被进程A看到,也可以被进程B看到。所以我们可以把进程A产生的数据放到缓冲区中,然后进程B就可以从缓冲区中拿到这部分数据从而完成了进程间的通信!

综上:

​ a.想办法,先让不同的进程,看到同一份资源。

​ b.让一方写入,一方读取,完成通信过程,至于,通信目的与后续工作需要结合具体场景。

2.管道

2.1什么是管道

  • 管道是Unix中最古老的进程间通信的形式
  • 我们把从一个进程连接到另一个进程的一个数据流成为一个“管道”

2.2实操一下见见管道

who命令:查看当前Linux系统中有用户登录信息

image-20231123212353868

who | wc -l其中的wc -l表示统计输出结果的行数然后输出,那么整体的运行结果就表示的是当前Linux系统中有多少个用户登录

image-20231123212546537

再看一个例子

 //其中的&代表让当前的命令在后台执行[mi@lavm-5wklnbmaja lesson12]$ sleep 10000 | sleep 20000 | sleep 30000 &
[1] 24952
//我们可以查看到三个sleep进程,那么这三个进程被称为我们的兄弟进程,父进程都是我们的bash
[mi@lavm-5wklnbmaja lesson12]$ ps axj | head -1 && ps axj | grep sleepPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
22692 24950 24950 22692 pts/0    24995 S     1000   0:00 sleep 10000
22692 24951 24950 22692 pts/0    24995 S     1000   0:00 sleep 20000
22692 24952 24950 22692 pts/0    24995 S     1000   0:00 sleep 30000
22692 24996 24995 22692 pts/0    24995 S+    1000   0:00 grep --color=auto sleep

那么当我们使用管道执行命令的时候他是帮我们直接创建了这几个进程:比如上面例子中的sleep 10000 | sleep 20000 | sleep 30000who | wc -l都是如此,那么它是如何实现进程之间的数据进行通信的?

以上为例,首先管道也是文件,所以我们对应的who进程以写的形式打开管道,并且将自己的标准输出重定向管道

对应的wc -l进程就以读的形式打开管道,并且将自己的标准输入重定向到管道。

image-20231123213537515

2.3管道的原理

管道就是一个操作系统提供的纯内存级文件!

①父进程曾经打开的文件是不需要复制给子进程的,原因是子进程拷贝了一份父进程的struct files_struct,其中包含了父进程文件的描述符对应的数组,子进程可以通过这个数组依旧指向父进程的原来打开的文件,而不需要再打开一份造成资源的浪费。

②所以创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象,不会复制父进程曾经打开的文件对象!

现象:这就是为什么fork之后,父子进程都printf,cout,都会向同一个显示器终端打印数据的原因!

因此我们的子进程也可以看到父进程创建的管道文件!完成了进程间通信的前提,让不同的进程看到了同一份资源!

这种管道只支持单向通信!!

③确定数据流向,关闭不需要的fd

所以我们在子进程关闭对应的读端,和父进程对应的写端,所以我们就可以通过管道将子进程的数据流向父进程。就可以进行正常的进程间的通信!

那么管道为什么是单向的?

原因是管道是基于文件进行通信的,文件读写缓冲区是分开的,当这种通信技术被发明出来的时候,我们发现进程的通信只能是单向的。首先作为父进程它打开管道是需要以读写方式打开,创建子进程之后才能关闭对应的读或者写,要不然子进程继承不到对应的读写方式打开文件,就不能进行一个写一个读了!

管道原理图

2.4直接编写样例代码

这里我们需要用到pipe接口:创建管道文件

其中它的参数int pipefd[2]被称为输出型参数,pipe接口将对应的读和写文件描述符写到这个数组里。

成功返回0,否则返回-1.

image-20231124150856768

#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<assert.h>
#include<string.h>
#include<string>using namespace std;int main()
{//让不同的进程看到同一份资源!//任何一种任何一种进程间的通信中//,一定要 先 保证不同的进程之间看到同一份资源int pipefd[2] = { 0 };//1.创建管道int ret = pipe(pipefd);//创建失败if(ret < 0){cout << "pipe error," << errno << ": " << strerror(errno) << endl;return 1;}//打印对应的pipefdcout << "pipefd[0]: " << pipefd[0] << endl;  //读端cout << "pipefd[1]: " << pipefd[1] << endl;  //写端//2.创建子进程pid_t id = fork();assert(id != -1); //正常应该用判断//意料之外用if,意料之内用assertif(id == 0){//子进程//关闭对应的读端close(pipefd[0]);//4.开始通信---结合场景const string msg = "hello, 我是子进程";int cnt = 0;char buffer[1024];while(true){snprintf(buffer, sizeof(buffer), "%s, 计数器:%d, 我的PID: %d", msg.c_str(), cnt, getpid());write(pipefd[1], buffer, strlen(buffer));sleep(1);cnt++;}close(pipefd[1]);exit(0);}//父进程//3.关闭不需要的文件描述符,父读子写,关闭对应的写端close(pipefd[1]);//4.开始通信---结合场景char buffer[1024];while(true){//sleep(1);int n = read(pipefd[0], buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = '\0';cout << "我是父进程,我收到了子进程发给我的消息:" << buffer << endl;}}close(pipefd[0]);return 0;
}

image-20231124211108964

2.5做实验,推导出管道

a.五种特性

​ 1.单向通信

​ 2.管道的本质是文件,因为fd的声明周期随进程,管道的声明周期是随进程的

​ 3.管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用于父子通信—pipe打开管道,并不清楚管道的名字,匿名管道。

​ 4.在管道通信中,写入的次数,和读取的次数,不是严格匹配的,读写次数的多少没有强相关—表现—字节流

​ 5. 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信—自带同步机制

b.四种场景:

1.如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待

2.如果我们writer端管道写满了,我们还能写吗?不能!

我们每次写入四个字节,写入了65535次管道就满了!

snprintf(buffer, 4, "s");

image-20231124211820571

3.如果关闭了写端,读取完毕,在读,read就返回0,表明读到了文件结尾。

image-20231124191750420

4.写端一直写,读端关闭,会发生什么?没有意义!OS不会维护无意义,低效率,或者浪费资源的事情,OS会杀死一直在写入的进程!OS会通过信号来终止进程,13)SIGPIPE

我们收一条指令之后五秒之后关闭父进程对应的读端,发现子进程也退出了!

image-20231124214343431

2.6管道的特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

2.7添加一点设计,来完成一个基本的多进程控制代码

需求:父进程有多个子进程,父进程和这些子进程之间都有管道,父进程可以通过向子进程写入特定的消息,唤醒子进程,甚至让子进程执行某种任务。

首先搭建大体的框架:

#include<iostream>
#include<string>
#include<unistd.h>
#include<assert.h>
#include<vector>using namespace std;const int gnum = 5; //表示子进程数int main()
{//1.先进行构建控制结构,父进程写,子进程读for(int i = 0; i < gnum; ++i){//1.1创建管道int pipefd[2] = {0};int ret = pipe(pipefd);assert(ret == 0); //0正常 -1不正常(void)ret;//1.2创建进程pid_t id = fork();assert(id != -1);if(id == 0){//子进程//1.关闭对应的fd,也就是写close(pipefd[1]);close(pipefd[0]);exit(0);}//父进程//1.3关闭不要的fdclose(pipefd[0]);}return 0;
}

但是我们应该理解到,父进程有这么多的子进程,我们父进程在操作的时候,怎么分的清对应的子进程是哪一个?所以这时候我们应该将这些子进程组织起来,这就要利用到我们操作系统内部一直在践行的:先描述再组织

创建一个对应的结构体,然后用vector将其组织起来

//先描述
class EndPoint
{
public:pid_t _child; //子进程pidint _write_fd;//对应的文件描述符public://构造EndPoint(pid_t id, int fd) :_child(id), _write_fd(fd){}//析构~EndPoint(){}
};//在组织vector<EndPoint> end_points;

然后我们将以上的步骤封装成一个函数:

void creatProcesses(vector<EndPoint>& end_points)
{//1.先进行构建控制结构,父进程写,子进程读for(int i = 0; i < gnum; ++i){//1.1创建管道int pipefd[2] = {0};int ret = pipe(pipefd);assert(ret == 0); //0正常 -1不正常(void)ret;//1.2创建进程pid_t id = fork();assert(id != -1);if(id == 0){//子进程//1.3关闭不要的fdclose(pipefd[1]);//我们期望,所有的子进程读取“指令”的时候,都从标准输入读取//1.3.1所以我们进行输入重定向dup2(pipefd[0], 0);//1.3.2子进程开始等待获取命令WaitCommend();close(pipefd[0]);exit(0);}//父进程//1.3关闭不要的fdclose(pipefd[0]);//1.4将新的子进程和他的管道写端构建对象。end_points.push_back(EndPoint(id, pipefd[1]));}
}

主函数这样写:先让程序跑起来

int main()
{//在组织vector<EndPoint> end_points;creatProcesses(end_points);//2.那么我们这里就可以得到了五个子进程的id和对应的写端while(true){sleep(1);}return 0;
}

我们运行了之后用ps去监视查看进程确实生成了我们对应了五个子进程:

image-20231124231658337

设计 WaitCommend();函数

这个函数就是让子进程去一直去读取管道中信息,读取到之后执行对应的任务。

void WaitCommend()
{while (true){int command;int n = read(0, &command, sizeof(int));if (n == sizeof(int)) // 读取成功{t.Execute(command);}else if (n == 0) // 表示链接已经关闭{// 则不需要去读了break;}else // 读取错误{break;}}
}

主函数内部父进程调度发配任务:

主要分为三步:

  • 确定任务
  • 确定执行任务的子进程
  • 执行任务
int main()
{// 在组织vector<EndPoint> end_points;creatProcesses(end_points);// 2.那么我们这里就可以得到了五个子进程的id和对应的写端while (true){//1.确定任务int command = COMMAND_LOG;//2.确定执行任务的子进程int child = rand() % end_points.size();//3.执行任务write(end_points[child]._write_fd, &command, sizeof(command));sleep(1);}return 0;
}

运行结果:

image-20231125121913303

所有源码:

Task.hpp

#pragma once#include<iostream>
#include<vector>
#include<unistd.h>using namespace std;typedef void (*fun_t)(); //函数指针//任务对应的操作码,约定每一个command是四个字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2//三个任务
void PrintLog()
{cout <<"进程的PID:" <<getpid()<< "打印日志任务,正在被执行" << endl;
}void InsertMySQL()
{cout << "执行数据库任务,正在被执行" << endl;
}void NetRequest()
{cout << "执行网络请求任务,正在被执行" << endl;
}class Task
{
public:Task(){funcs.push_back(PrintLog);funcs.push_back(InsertMySQL);funcs.push_back(NetRequest);}//任务执行函数void Execute(int command){if(command >= 0 && command < funcs.size()) funcs[command]();}~Task(){}public:vector<fun_t> funcs;};

myprocess.cc

#include <iostream>
#include <string>
#include <unistd.h>
#include <assert.h>
#include <vector>#include "Task.hpp"using namespace std;const int gnum = 5; // 表示子进程数// 定义对应的任务对象
Task t;// 先描述
class EndPoint
{
public:pid_t _child;  // 子进程pidint _write_fd; // 对应的文件描述符public:// 构造EndPoint(pid_t id, int fd): _child(id), _write_fd(fd){}// 析构~EndPoint(){}
};void WaitCommend()
{while (true){int command;int n = read(0, &command, sizeof(int));if (n == sizeof(int)) // 读取成功{t.Execute(command);}else if (n == 0) // 表示链接已经关闭{// 则不需要去读了break;}else // 读取错误{break;}}
}void creatProcesses(vector<EndPoint> &end_points)
{// 1.先进行构建控制结构,父进程写,子进程读for (int i = 0; i < gnum; ++i){// 1.1创建管道int pipefd[2] = {0};int ret = pipe(pipefd);assert(ret == 0); // 0正常 -1不正常(void)ret;// 1.2创建进程pid_t id = fork();assert(id != -1);if (id == 0){// 子进程// 1.3关闭不要的fdclose(pipefd[1]);// 我们期望,所有的子进程读取“指令”的时候,都从标准输入读取// 1.3.1所以我们进行输入重定向dup2(pipefd[0], 0);// 1.3.2子进程开始等待获取命令WaitCommend();close(pipefd[0]);exit(0);}// 父进程// 1.3关闭不要的fdclose(pipefd[0]);// 1.4将新的子进程和他的管道写端构建对象。end_points.push_back(EndPoint(id, pipefd[1]));}
}
int main()
{// 在组织vector<EndPoint> end_points;creatProcesses(end_points);// 2.那么我们这里就可以得到了五个子进程的id和对应的写端while (true){//1.确定任务int command = COMMAND_LOG;//2.确定执行任务的子进程int child = rand() % end_points.size();//3.执行任务write(end_points[child]._write_fd, &command, sizeof(command));sleep(1);}return 0;
}

到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

在这里插入图片描述

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

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

相关文章

3 Unsupervised learning recommenders reinforcement learning

文章目录 Week1Unsupervised LearningClusteringK-meansprincipleOptimization objectiveInitializing K-meanschose the number of clusters Anomaly DetectionFind unusual eventsAlgorithmchose epsilonAnomally Detection vs Supervised learningfeatures Week2Recommender…

华为ensp:单臂路由

通过单臂路由实现vlan之间的互通 将vlan和trunk配置好&#xff0c;我直接就在r1上演示单臂路由 我们要在r1的e0/0/0上面随便配置个ip&#xff0c;如果你不在接口上配置ip那就无法开启协议 R1 interface e0/0/0 进入真实接口随便配置个ip ip add 192.168.10.1 24 再进入子接…

掌握3个Mock工具,轻松玩转单元测试

公司要求提升单元测试的质量&#xff0c;提高代码的分支覆盖率和行覆盖率&#xff0c;安排我研究单元测试&#xff0c;指定方案分享并在开发部普及开。 单元测试中的Mock的目的 Mock的主要目的是让单元测试Write Once, Run Everywhere. 即编写一次后&#xff0c;可以在任意时…

C语言SO EASY(ZZULIOJ1220: SO EASY)

题目描述 Superbin最近在研究初等数论&#xff0c;初等数论 是研究数的规律&#xff0c;特别是整数性质的数学分支。它是数论的一个最古老的分支。它以算术方法为主要研究方法&#xff0c;主要内容有整数的整除理论、同余理论、连分数理论和某些特殊不定方程。 是定义在正整数…

香蕉派BPI-M4 Zero单板计算机采用全志H618,板载2GRAM内存

Banana Pi BPI-M4 Zero 香蕉派 BPI-M4 Zero是BPI-M2 Zero的最新升级版本。它在性能上有很大的提高。主控芯片升级为全志科技H618 四核A53, CPU主频提升25%。内存升级为2G LPDDR4&#xff0c;板载8G eMMC存储。它支持5G WiFi 和蓝牙, USB接口也升级为type-C。 它具有与树莓派 …

链表经典面试题

1 回文链表 1.1 判断方法 第一种&#xff08;笔试&#xff09;&#xff1a; 链表从中间分开&#xff0c;把后半部分的节点放到栈中从链表的头结点开始&#xff0c;依次和弹出的节点比较 第二种&#xff08;面试&#xff09;&#xff1a; 反转链表的后半部分&#xff0c;中间节…

批量按顺序1、2、3...重命名所有文件夹里的文件

最新&#xff1a; 最快方法&#xff1a;先用这个教程http://文件重命名1,2......nhttps://jingyan.baidu.com/article/495ba841281b7079b20ede2c.html再用这个教程去空格&#xff1a;利用批处理去掉文件名中的空格-百度经验 (baidu.com) 以下为原回答 注意文件名有空格会失败…

html实现我的故乡,城市介绍网站(附源码)

文章目录 1. 我生活的城市北京&#xff08;网站&#xff09;1.1 首页1.2 关于北京1.3 北京文化1.4 加入北京1.5 北京景点1.6 北京美食1.7 联系我们 2.效果和源码2.1 动态效果2.2 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43…

C语言猜素数(ZZULIOJ1292:猜素数)

题目描述 Lx给Xp出了一道难题&#xff0c;随便在0和1000000之间抽出两个数&#xff0c;估计在这两个数之间的素数的个数&#xff0c;如果猜测的结果和正确结果一样&#xff0c;Xp就可以得到Lx的一件礼物&#xff0c;你能猜对吗&#xff1f;编程实现一下吧&#xff01; 输入&…

面试必问:如何快速定位BUG?BUG定位技巧及N板斧!

01 定位问题的重要性 很多测试人员可能会说&#xff0c;我的职责就是找到bug&#xff0c;至于找原因并修复&#xff0c;那是开发的事情&#xff0c;关我什么事&#xff1f; 好&#xff0c;我的回答是&#xff0c;如果您只想做一个测试人员最基本最本分的事情&#xff0c;那么可…

BTP-K710自定义纸张大小

进入控制面板 输入 control 打开打印机 点击查看设备和打印机 打开打印机属性 鼠标右击 -> 点击打开打印机属性 如果上面没有你想要的尺寸 点标签库 -> 选中Custom Paper-BPLZ -> 然后修改宽度和高度 -> 点击应用 选择Custom Paper-BPLZ 最后点击确定 这样…

什么是AWS CodeWhisperer?

AWS CodeWhisperer https://aws.amazon.com/cn/codewhisperer/ CodeWhisperer 经过数十亿行代码的训练&#xff0c;可以根据您的评论和现有代码实时生成从代码片段到全函数的代码建议。 ✔ 为您量身定制的实时 AI 代码生成器 ✔ 支持热门编程语言和 IDE ✔ 针对 AWS 服务的优…