Linux - 进程间通信(中)- 管道的应用场景

前言

在上篇博客当中,对Linux 当中的进程通信,做了详细阐述,主要是针对父子进程的通信来阐述的同时,也进行了模拟实现。

对于管道也有了初步了解,但是这仅仅是 进程间通信的一部分,Linux 当中关于进程间通信还有很多的内容,这篇博客将会在上篇博客的基础之上,继续阐述进程间通信。

如有疑问,请看上篇博客:
Linux - 进程间通信(上) - Linux 当中的管道-CSDN博客

管道的应用场景

我们知道,在Linux 当中,有一个命令 -- "|" ,我们也把这个命令称之为管道,那么这个命令和我们上篇博客当中,对于管道的介绍有什么关系呢?

cat text.txt | head -10 | tail-5

比如上述命令,在之前来解释的话,就是把 cat text.txt 的运行结果,通过管道,传输给 head -10 ,然后 head -10 也有有一个输出结果,又把这个输出结果,通过管道,传输给 tail -5,此时得出的结果才是上述命令的最终结果。

 而上述我们所使用的 -- "|" 命令,和 Linux 当中的 pipe 管道是有关系的

 我们拿下述 sleep 这个命令来说明上述 -- "|" 的实现:
 

sleep 6666 | sleep 7777 | sleep 8888

在执行上述命令之前,我们先用 ps 命令查看一下当中 关于 sleep 的进程,发现此时只有一个 sleep 进程: 

当我们执行上述命令之后,发现系统的当中就多出了上述的  sleep 6666 ,sleep 7777 ,sleep 8888 这个三个进程:
 

 而这  sleep 6666 ,sleep 7777 ,sleep 8888 这三个进程 的 PPID 都是 18595,也就是说,这三个进程的 PPID (父进程的PID)是一样的。

说明,这三个进程 互相直接的关系是兄弟进程。也就是具有血缘关系。

 很显然,这个18595的PID 是 BASH 的PID。

 而上述的实现也就和上篇博客当中的父子通信的原理是一样的:

首先,操作系统会先创建上述的两个管道,创建之后,在创建   sleep 6666 ,sleep 7777 ,sleep 8888 这三个进程,三个进程通过程序替换的方式,执行不同的代码块。

然后,利用上篇博客当中的对于 进程之间通信的共享文件,哪一个进程是读端,哪一个进程是写端,确定好。

也就是对每一个进程的 0,1,2 号文件,所输入或者输出重定向,使得这些文件的输出和输入不再是单纯的从 键盘文件读取数据,显示器文件输出数据。而是在指定的共享文件当中进行的数据的输入和输出,实现三个进程之间的通信。

这样就建立好了这三个进程之间的链接。

所以,这个 "|" 管道,在底层实现上就是使用 PIPE()函数,实现的 进程之间的通信

 而这 "|" 管道,就是一种匿名管道

 shell 当中实现 "|" 管道 原理

 Linux - 基础IO(重定向 - 重定向模拟实现 - shell 当中的 重定向)- 下篇-CSDN博客

Linux - 实现一个简单的 shell-CSDN博客

 上述 Linux - 基础IO(重定向 - 重定向模拟实现 - shell 当中的 重定向)博客当中是对shell 加入 重定向之后的shell,其中大体流程如下所示:

显示做出与用户交互的函数,也就是打印出控制台,接受用户输入的命令,判断输入的命令是否有误等等。

然后是对接受到的命令做字符串的分割,把需要执行什么命令解析出来,也就是解析用户的输入的字符串。

然后,判断解析出来的命令,是不是内建命令,如果是,就执行内建命令;如不是,就执行普通命令。

但是,现在要想实现管道的话,管道左右两边的命令可以是内建命令,也可以是普通命令。

所以,我们在解析命令的过程当中,先要解析用户当中有没有 "|" 管道,如果有,就要以 "|" 管道为分割符,把命令一个一个的解析出来;也就是判断,命令字符串当中有多少 "|" ,从而把这个字符串打散为多个字符串。

这些字符串就是一个一个需要执行的命令,所以,需要 malloc 开辟空间来保存这个字符串(如果当中malloc 满了,好临时扩容空间)。

然后,在创建这些命令的子进程之前,需要先创建出 "|",也就是在代码当中写一个循环,在创建管道左右的子进程之时,就可以通过 pipe()函数,链接出左右两个进程之间的 共享文件的链接关系(重定向)。

之后就是循环创建出子进程,对各个子进程的 输入和输出重定向做修改,实现子进程之间的通信。(在管道的最开始就是 1->指定一个管道的写端;管道中间的进程,0标准输入重定向到上一个管道的读端,标准输出指定到下一个管道的写端;最后一个,将标准输入重定向到最后一个管道的读端) 

在让不同的子进程执行不同的命令,也就是 exec* 程序替换,程序替换不会影响到这个进程在替换之间,曾经打开的文件,也就不会影响到曾经预先设置好的重定向文件位置了

通过管道简单实现进程池 

 在 C/C++ 当中有内存池,这样可以更高效的像操作系统申请内存空间供用户使用。其实本质上也就是 像是打水一样,如果每天都需要去河边打一桶水,那么来回的路程,是相对于每一天都需要去跑的,如果,我们一天一次性打上很多桶水。比如是开车 去打上一车的水,这一车的水可以供我们一个星期来使用,那么这个一个星期都可以不同再去河边去打水了。省去了来回在路上往返的消耗了。

同样的,如果要想要操作系统为我们申请内存空间的话,是有消耗的。因为操作系统本身也是有很多事情要去做的,那么在操作系统做完当前事情之前,我们需要去等待。

而且,操作系统也是通过调用底层系统调用接口来实现的,内存属于硬件,硬件就有自己的驱动程序,操作系统在上层,只能一层一层往下去访问到内存硬件资源。这些其实都是有消耗的。

所以,在不浪费内存资源,适量的情况下,预先加载多个内存资源,就可以在一定程度上缓解,通过操作系统申请内存资源所带来的消耗。

同样的,对于申请进程而言,如果单纯的,需要一个进程就去创建一个进程的话,也是和上述一天打一桶水的结果是一样的。都是有消耗的。

创建进程需要申请内存空间,需要有进程地址空间,有进程PCB,由页表等等的内核数据结构,而且,里链接这些内核数据结构也是需要时间的。

所以,在操作系统当中,同样是有 内存池 这样的 “池”的。

我们对这种池的创建,称之为-- 池化技术

我们预先创建好 多个 进程所需的进程资源,当我们想要用到某一个进程资源之时,只需要直接指派这个进程,帮我们去完成任务。


在父进程接受到任务之前,先预先在创建出多个 子进程的资源:
 

父进程和每一个子进程之间都建立一条管道的信道, 每一个子进程只负责从管道当中读取数据;而父进程之负责,把对应数据,输入到 对应子进程的管道当中。

如果,父进程没有向一个管道当中写入任何数据,那么这个管道对应的子进程就是阻塞在这个管道当中。等待父进程向管道当中输入数据。

一旦父进程向某一个管道当中写入数据了,那么对应子进程就会读到这些数据,就可以继续执行子进程当中的代码。

我们把父进程向管道当中写入的数据,叫做一个一个的任务码

规定,父进程在写入数据之时,一次只能写 4字节的内容,子进程在读取数据之时,一次也只能读取4字节的内容。总之就是以等长的数据长度写入,以等长的数据长度读取(4字节只是假设,具体要看对应的操作系统的设计)


完整代码

// Task.cpp 
// 任务列表,text.cc 源程序当中,用户从这个 LoadTask ()函数当中选择对应的任务
#pragma once#include <iostream>
#include <vector>typedef void (*task_t)();// 下述的 tesk1-4 都是对应的任务
// LoadTask()函数是选择这些任务的函数,供text.cc 源程序当中进行选择
// <---- 这里我们使用函数指针的方式来 使得 子进程能直接跳转到 下述的 tesk1-4 都是对应的任务 函数当中进行执行 ---->
void task1()
{std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}// 传入一个 vector<task_t> ,这个容器当中哦弄个存储的是一个一个的任务
// 下述当中的 tasks 是一个输出型参数
void LoadTask(std::vector<task_t> *tasks)
{tasks->push_back(task1);tasks->push_back(task2);tasks->push_back(task3);tasks->push_back(task4);
}


#include "Task.hpp"
#include <string>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>const int processnum = 10;
std::vector<task_t> tasks;  // 任务列表,用于 LoadTask ()函数返回 任务清单    // 这个 tasks 容器当中存储的是 一个一个 task()任务执行函数的 函数指针// 子进程通过这个函数指针,就可以直接调用到 对应的函数体当中去执行代码
// 先描述
// 父进程为了能管理各个管道
// 把各个管道用结构体对象描述起来
// 一个channel 就是一个管道结构体
class channel
{
public:// 用于构造一个 管道类 的构造函数channel(int cmdfd, int slaverid, const std::string &processname):_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname){}
public:int _cmdfd;               // 发送任务的文件描述符pid_t _slaverid;          // 子进程的PIDstd::string _processname; // 子进程的名字 -- 方便我们打印日志// int _cmdcnt;
};// 子进程需要做的事情
void slaver()
{// read(0)while(true) // 循环一直做,直到做完{int cmdcode = 0;// read参数: (从0号文件当中读取数据, 保存到cmdcode变量当中, 一次只读取 4 字节的内容int n = read(0, &cmdcode, sizeof(int)); // 如果父进程不给子进程发送数据呢??阻塞等待!if(n == sizeof(int))  // n 是read()的返回值,返回的是 成功从文件当中读取的字节个数{//执行cmdcode对应的任务列表// 先 打印子进程的PID,然后打印 当前从文件当中读取的内容std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " <<  cmdcode << std::endl;// 判断当前的 执行任务是否 是在安全区间当中的if(cmdcode >= 0 && cmdcode < tasks.size())tasks[cmdcode]();  // 执行任务}// 如果 read() 函数返回返回 0 ,说明读取错误,我们就跳出这个循环if(n == 0) break;}
}// 先预先建立好管道共享文件,然后预先创建子进程,把各个子进程和父进程之间连接上管道
void InitProcessPool(std::vector<channel> *channels)
{// version 2: 确保每一个子进程都只有一个写端std::vector<int> oldfds;for(int i = 0; i < processnum; i++){// pipedf[] 数组用于pipe函数当中的 输出型参数的返回值的存储// pipedf[0] 是共享文件的读端;pipedf[1] 是共享文件的写端int pipefd[2]; // 临时空间int n = pipe(pipefd); // 创建管道assert(!n);  // pipe() 创建成功返回0,否则返回 -1(void)n;pid_t id = fork();   // 创建子进程if(id == 0) // child 执行的代码块{std::cout << "child: " << getpid() << " close history fd: ";for(auto fd : oldfds) {std::cout << fd << " ";close(fd);}std::cout << "\n";close(pipefd[1]); // 关闭写端文件// 下述的 进程替换,就是把 子进程当中的 0 号文件,本来是标准输入文件,也就是键盘文件读取数据// 现在修改为管道文件的 来读取数据// 这样的好处是 以后 子进程就会不用再去管其他的 去哪里接收父进程传输的数据了// 只需要“无脑”的从 子进程的 0号文件当中读取数据既可以dup2(pipefd[0], 0);  // 进程替换 close(pipefd[0]); // 因为上述已经进行了 重定向,所以 pipefd[0] 号文件就可以关闭了slaver();         // 执行 子进程当中需要做的任务std::cout << "process : " << getpid() << " quit" << std::endl; // 提示子场景完成任务,即将退出// slaver(pipefd[0]);exit(0); // 子进程退出 , 所以下述代码就可以不同执行了}// father 执行的代码块close(pipefd[0]); // 关闭读端文件// 添加channel字段了// 往 存储 channel 对象的 vector 容器当中添加当前管道的 channel 对象std::string name = "process-" + std::to_string(i);channels->push_back(channel(pipefd[1], id, name));oldfds.push_back(pipefd[1]);sleep(1);}
}void Debug(const std::vector<channel> &channels)
{// testfor(const auto &c :channels){std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;}
}void Menu()
{std::cout << "################################################" << std::endl;std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;std::cout << "#                         0. 退出               #" << std::endl;std::cout << "#################################################" << std::endl;
}void ctrlSlaver(const std::vector<channel> &channels)
{int which = 0;// int cnt = 5;while(true){int select = 0;Menu(); // 打印菜单,供用户选择操作std::cout << "Please Enter@ ";std::cin >> select;// 判断用户输入是否正确if(select <= 0 || select >= 5) break;// select > 0&& select < 5// 1. 选择任务// int cmdcode = rand()%tasks.size();    // tasks 是定义的全局 vector<task_t> 容器int cmdcode = select - 1;  // 由用户去选择// 2. 选择进程// 使用随机数来 选择 父进程当前要选择的子进程 来执行子任务// 防止 父进程 一直选择 某一个进程来执行任务,那么其他的资源就浪费了// 除了这种方式,还可以使用 轮询的方式,也就是递增或者是递减循环选择 子进程的方式来实现// write(channels[processpos]._cmdfd, &cmdcode, sizeof(cmdcode)); 这样使用即可// int processpos = rand()%channels.size();   这种方式是使用随机数的方式来 进行 选择进程std::cout << "father say: " << " cmdcode: " <<cmdcode << " already sendto " << channels[which]._slaverid << " process name: " << channels[which]._processname << std::endl;// 3. 发送任务// 这种使用 which 的方式其实就是一种 轮询的方式 来选择 进程的write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));which++;which %= channels.size();  // 不哟让 which 越界了 // cnt--;// sleep(1);}
}// 让所有的进程全部退出
void QuitProcess(const std::vector<channel> &channels)
{for(const auto &c : channels){close(c._cmdfd);  // 将子进程当中的写端直接关闭,这样的话,在子进程当中的 read()函数就会返回 0 // 只要检测到 read()函数返回0 ,就可以在 父进程 控制子进程的函数当中退出 子进程waitpid(c._slaverid, nullptr, 0); // 因为此时 QuitProcess ()函数是 父进程执行的// 子进程退出,父进程要等待子进程退出}// version1 // int last = channels.size()-1;// for(int i = last; i >= 0; i--)// {//     close(channels[i]._cmdfd);//     waitpid(channels[i]._slaverid, nullptr, 0);// }// for(const auto &c : channels) close(c._cmdfd);// // sleep(5);// for(const auto &c : channels) waitpid(c._slaverid, nullptr, 0);// // sleep(5);
}
int main()
{LoadTask(&tasks);  // 获取到任务 tasks 是定义的全局 vector<task_t> 容器srand(time(nullptr)^getpid()^1023); // 种一个随机数种子// 在组织// 用下述的 vector  数据结构把 一个一个的管道结构体管理起来// 从此之后,父进程管理一个一个的管道,就变成了对这个 vector 数据结构的增删查改std::vector<channel> channels;// 1. 初始化 --- bug?? -- 找一下这个问题在哪里?然后提出一些解决方案!InitProcessPool(&channels);// Debug(channels);// 2. 开始控制子进程ctrlSlaver(channels);// 3. 清理收尾QuitProcess(channels);return 0;
}

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

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

相关文章

golang 操作Jenkins

1.創建Agent/Node func CreateAgent(username string, password string, nodeName string, nodeDescription string, numExecutors string, remoteFS string, labelString string, host string) {var obj stringobj "{name:" nodeName ",nodeDescription:&q…

YOLOv4 学习笔记

文章目录 前言一、YOLOv4贡献和改进二、YOLOv4核心概念三、YOLOv4网络架构四、YOLOv4数据增强五、YOLOv4的损失函数总结 前言 在近年来的目标检测领域&#xff0c;YOLOv4的出现标志着一个重要的技术突破。YOLOv4不仅继承了YOLO系列快速、高效的特点&#xff0c;还引入了一系列…

【启扬方案】启扬储能管理平板助力储能电站实现智能且高效化运行

在储能领域&#xff0c;储能电站扮演着重要角色&#xff0c;储能电站技术的应用贯穿于电力系统发电、输电、配电、用电的各个环节。实现电力系统削峰填谷、可再生能源发电波动平滑与跟踪计划处理、高效系统调频&#xff0c;增加供电的可靠性。 但随着储能电⼒系统建设发展得越来…

Java网络编程,使用UDP实现TCP(三), 基本实现四次挥手

简介 四次挥手示意图 在四次挥手过程中&#xff0c;第一次挥手中的Seq为本次挥手的ISN&#xff0c; ACK为 上一次挥手的 Seq1&#xff0c;即最后一次数据传输的Seq1。挥手信息由客户端首先发起。 实现步骤&#xff1a; 下面是TCP四次挥手的步骤&#xff1a; 第一次挥手&…

记录一下如何使用python生成二维码 并简单练习命令行参数供初学者参考

主代码main.py 后面是演示效果图&#xff1a; import argparse import sysimport qrcode import os qr qrcode.QRCode(version1,error_correctionqrcode.constants.ERROR_CORRECT_L,box_size10,border4, ) fileList[] fileName[]parserargparse.ArgumentParser(description生…

Ubuntu20.04降低linux版本到5.4.0-26-generic

前言 试用ubuntu20.04安装昇腾的驱动和cann的时&#xff0c;出现如下问题&#xff1a; (base) rootubuntu:/home/work# ./Ascend-hdk-910-npu-driver_23.0.rc3_linux-aarch64.run --full Verifying archive integrity... 100% SHA256 checksums are OK. All good. Uncompr…

基于Python+WaveNet+MFCC+Tensorflow智能方言分类—深度学习算法应用(含全部工程源码)(三)

目录 前言引言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 模型构建1&#xff09;定义模型结构2&#xff09;优化损失函数 3. 模型训练及保存1&#xff09;模型训练2&#xff09;模型保存3&#xff09;映射保存 相关其它博客工程源代码下载其它资料下载…

“百里挑一”AI原生应用亮相,百度智能云千帆AI加速器首个Demo Day来了!

作者简介&#xff1a; 辭七七&#xff0c;目前大二&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; 七七的闲谈 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f…

亚马逊云科技:向量数据存储在生成式人工智能应用程序中的作用

生成式人工智能深受大众喜爱&#xff0c;并且由于具备回答问题、写故事、创作艺术品甚至生成代码的功能&#xff0c;推动了行业的转变&#xff0c;那么如何才能在自己的企业中充分地利用生成式人工智能等应运而生问题。许多客户已经积累了大量特定领域的数据&#xff08;财务记…

LangChain学习二:提示-实战(下半部分)

文章目录 上一节内容&#xff1a;LangChain学习二&#xff1a;提示-实战&#xff08;上半部分&#xff09;学习目标&#xff1a;提示词中的示例选择器和输出解释器学习内容一&#xff1a;示例选择器1.1 LangChain自定义示例选择器1.2 实现自定义示例选择器1.2.1实战&#xff1a…

静态路由的原理和配置

一.路由器的工作原理 首先我们知道路由器是工作在网络层的&#xff0c;那就是三层设备。网络层的功能主要为&#xff1a;不同网段之间通信、最佳路径选择也就是逻辑地址&#xff08;ip地址&#xff09;寻址、转发数据。 1.路由器是什么 路由器是能将数据包转发到正确的目的地…

【QT 5 调试软件+(Linux下验证>>>>串口相关初试串口)+Windows下qt代码在Linux下运行+参考win下历程+基础样例】

【QT 5 调试软件Linux下验证>>>>串口相关初试串口参考win下历程基础样例】 1、前言2、实验环境3、先行了解4、自我总结-win下工程切到Linux下1、平台无关的代码&#xff1a;2、依赖的库&#xff1a;3、文件路径和换行符&#xff1a;4、编译器差异&#xff1a;5、构…

什么是防抖与节流?应用场景举例

防抖节流如何处理防抖与节流 防抖节流防抖例子节流例子Vue Axios全局接口防抖、节流封装实现 小结 防抖 防抖&#xff1a;触发高频事件后n秒内函数只会执行一次&#xff0c;如果n秒内高频事件再次被触发&#xff0c;则重新计算时间 应用场景&#xff1a; 提交按钮、用户注册…

QEMU源码全解析 —— virtio(2)

接前一篇文章&#xff1a; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; 上一回对于virtio进行了简介&#xff0c;并说明了其基本原理以及框架。对…

【JVM入门到实战】(三) 查看字节码文件的工具

一、 javap -v命令 javap是JDK自带的反编译工具&#xff0c;可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。直接输入javap查看所有参数。输入javap -v 字节码文件名称 查看具体的字节码信息。&#xff08;如果jar包需要先使用 jar –xvf 命令解压&a…

mmyolo的bbox_loss和检测bbox都是空

最近用mmyolo训练自己的数据集的时候发现训练的时候loss_bbox0&#xff0c;测试和eval的时候结果也全是空的&#xff0c;排除了数据集读取的问题&#xff0c;最后发现是config中自定义了自己的类别但是没有传给dataset。。。 简而言之&#xff0c;在自定义了数据集里的metainf…

【C语言】一个RDMACM、Verbs API与epoll一起使用的例子

一、epoll介绍 epoll是Linux内核为处理大批量文件描述符而作了改进的poll&#xff0c;是Linux下多路复用IO接口select/poll的增强版本&#xff0c;它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。 以下是epoll的主要使用方法和优点&#xff1a; epo…

2023-12-05 Qt学习总结10

点击 <C 语言编程核心突破> 快速C语言入门 Qt学习总结 前言二十六 学生信息管理系统插入介绍: QTableView和QSqlTableModelQTableViewQSqlTableModel 程序所用数据库表格程序组成以及界面学生端源码:管理员端源码: 总结 前言 要解决问题: 学习qt最核心知识, 多一个都不…

Android : BottomNavigation底部导航_简单应用

示例图&#xff1a; 1.先创建底部导航需要的图片 res → New → Vector Asset 创建三个矢量图 图片1 baseline_home.xml <vector android:height"24dp" android:tint"#000000"android:viewportHeight"24" android:viewportWidth"24…

nrm 的使用 可以快速切换下载(npm)镜像,解决资源下载慢和运行失败

nrm是什么&#xff1f; 介绍 nrm(npm registry manager) 是 npm 的镜像源管理工具. 有时候国外资源太慢,使用 nrm 可以快速的在 npm 源之间切换 安装 npm install -g nrm 基本使用 查看可选择的源 nrm ls 切换到对应的镜像源 nrm use 对应的镜像 删除镜像源 nrm del 名字 …