[ Linux ] 进程间通信介绍 管道
创始人
2024-02-08 03:54:45
0

目录

0.进程间通信介绍

0.1通信背景

0.2进程间通信目的

1.管道

1.1 管道是什么

1.2 匿名管道

1.2.1管道通信的特点

1.2.2 匿名管道编码

父进程控制子进程的行为

进程池 -- 池化概念

1.3管道的特征总结

1.4命名管道

1.4.1创建一个命名管道

1.4.2 命名管道编码


0.进程间通信介绍

0.1通信背景

在之前我们学习进程时知道进程具有独立性,所以进程间交互数据的成本就变得非常高。进程之间为什么也进行进程间通信,这就需要谈谈进程间通信的目的了。但是进程具有独立性不是彻底独立,只管自己不理任何人,当然不是这样的。进程之间也是存在通信的。那么进程间通信的方式也有很多种,包括管道,System V IPC,POSIX IPC........那么今天我们先来看看进程间如何通过管道来进行通信。

0.2进程间通信目的

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

1.管道

1.1 管道是什么

管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。在Linux中管道分两种:匿名管道和命名管道。

1.2 匿名管道

假设现在在内存中有两个独立的进程,如果要想这两个进程之间通信,那么进程1可以先把数据拷贝到磁盘上,然后进程2再去读这个数据【如下图所示】....首先不考虑这种方法是否可行,但是比较好的让我们理解了一个道理:进程在通信之前,必须让不同的进程能够看到同一份资源(文件,内存块.....)

因此在通信之前,如何解决让进程先看到同一份资源呢?并且资源的不同决定了不同种类的通信方式!!!

因此我们正在学习的管道是提供共享资源的一种手段。

我们知道文件在内存和磁盘之间来回切换是非常耗费时间的,因此进程间通信大多都是内存级别的。意思就是在内存内部重建一块区域进行通信。

那么什么是管道呢?

在计算机通信中,我们把文件不再是一个磁盘文件,通过特定的接口表征自己的身份,说明他和磁盘脱离,自己读写数据是就在文件的内存缓冲区,完成数据交互,我们把这个文件叫做管道。因此我们说Linux下一切皆文件,管道也是文件。管道就是一个内存级文件。内容不需要刷新到磁盘中。

1.2.1管道通信的特点

在我们生活中遇到的管道有什么特点呢?那首先问那些都属于管道呢?天然气管道,水龙头管道等等.....那么这些管道大多数情况下都是单向的,并且这些管道都是传输资源的,在计算机中最重要的资源就是数据。

  1. 单向的
  2. 传输数据

那么我们如何来保证单向性呢?

我们来看下图,父进程和子进程通过管道完成进程通信如何保证单向性呢,我们刚刚提到了管道是一个文件,是数据的缓冲区,因此当父进程把需要通信的数据通过写的方式写入管道内时,子进程通过读的方式拿到这些资源即可完成父子间的通信。为了保证单向的,我们需要关闭父进程的读端,让父进程只能写,关闭子进程的写段,让子进程只能读。通过这样的方式我们就可以保证父进程只能写数据子进程只能读数据的单向性。

父进程必须以读写方式打开,这是因为子进程会继承下去,这样子进程就不用再打开了。那么谁决定父子关闭什么读写?这不是由管道决定的,这是由我们的需求所决定的。

那么我们如何来打开管道呢?难道要调用两次open()吗?当然不是了,因此操作系统提供了pipe()接口

1.2.2 匿名管道编码

认识pipe()接口,当我们调用piep时,底层会自动帮助我们把文件以读方式和写方式打开,而且我们会的到两个文件描述符,这两个文件描述符会写进pipefd数组内,因此这个数组是一个输出型参数。并且pipe是一个系统调用。返回0表示成功,返回-1表示失败。

接下来我们进行管道的代码:

下面这段代码是管道的创建和验证输出的数组是否使我们所想的两个文件描述符

#include 
#include 
#include using namespace std;int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0){cerr<<"pipe error"<

匿名管道代码演示

#include 
#include 
#include 
#include 
#include 
#include using namespace std;//演示pipe管道通信的基本过程 -- 匿名管道
int main()
{//1.创建管道int pipefd[2] = {0};if(pipe(pipefd) != 0){cerr<<"pipe error"<0){//读取成功buffer[s] = '\0';cout<<"子进程收到消息,消息的内容是:"<< buffer < 0 ){cout<<"父进程等待成功"<

至此父进程的数据就发给了子进程,子进程也能够接受父进程发送来的数据。在上述代码中,我们在父进程中带了sleep(1),让父进程每间1秒向管道内写入一个数据,那么子进程没有带sleep(1),为什么子进程也会随之休眠一秒呢?为了更好的看到这个现象,我们在父子进程里面带上时间戳,写写日志。

我们通过测试观察到子进程代码没有任何的休眠,子进程会随着父进程的节奏读取,那么我们可以得出结论:当父进程没有写入数据的时候,子进程在等!所以,父进程写入之后,子进程才能read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主!

那么,父进程和子进程读写的时候是有一定的顺序性的!当父进程向管道写入的时候,子进程才可以读!

  • 管道内部,没有数据,reader就必须阻塞等待(read时等待)
  • 管道内部,如果数据写满,writer就必须阻塞等到(writer时等待)

因此pipe内部是自带访问控制机制的以及存在同步和互斥机制的。

所谓的阻塞等待的本质是将当前的tast_struct 放入等待队列中,将PCB的状态由R->S/D/T

父进程控制子进程的行为

假设父进程想让我的子进程做父进程想让子进程做的行为,以及父进程想控制一批(多个)子进程.....该如何来写呢???

我们先写一段父进程控制子进程的行为的代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include using namespace std;// 定义一个函数指针
typedef void (*functor)();vector functors; //方法的集合// for debug
unordered_map info;void f1() { cout << "这是一个处理日志的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]" << endl; }void f2() { cout << "这是一个处理数据备份的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]" << endl; }void f3() { cout << "这是一个处理网络连接的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]" << endl; }void loadFunctor()
{info.insert({functors.size(), "处理日志任务"});functors.push_back(f1);info.insert({functors.size(), "数据备份任务"});functors.push_back(f2);info.insert({functors.size(), "处理网络连接任务"});functors.push_back(f3);
}int main()
{// 0. 加在任务列表loadFunctor();// 1.创建管道int pipefd[2] = {0};if (pipe(pipefd) != 0){cerr << "pipe error" << endl;return 1;}// 2.创建子进程pid_t id = fork();if (id < 0){cerr << " fork error" << endl;return 2;}else if (id == 0){// child read// 关闭不需要的fdclose(pipefd[1]);while (true){uint32_t operatorType = 0;//如果有数据就读取 如果没有数据就阻塞等待ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));if(s == 0) {cout << "我是子进程,我要退出了" <

通过这份代码父进程控制了子进程的行为,往后我们只需要修改functors里面的方法,就可以让子进程执行指定的任务。

进程池 -- 池化概念

那么如果是一个父进程想要控制一批子进程呢??? 父进程怎么样把一批任务交给子进程呢?


因此,我们有多少个进程,我们就创建多少个管道,父进程可以通过对指定管道写入特定的任务,让指定的子进程做对应的事情,这样我们就引入了一个池化的概念!那么我怎么如何书写对应的代码呢?

int processNum  = 5;int main()
{for(int i = 0;i

上面这份代码就成功的将每一次创建的子进程和父进程都独立的进行了控制,因此父进程在不断的循环,给子进程指派任务的时候需要知道给哪一个进程指派,指派什么任务,通过什么指派呢? 因此我们接下来需要做的就是解决这些问题。因此我们需要一节pair结构

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include using namespace std;// 定义一个函数指针
typedef void (*functor)();vector functors; //方法的集合// for debug
unordered_map info;void f1() { cout << "这是一个处理日志的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]\n\n" << endl; }void f2() { cout << "这是一个处理数据备份的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]\n\n" << endl; }void f3() { cout << "这是一个处理网络连接的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]\n\n" << endl; }void loadFunctor()
{info.insert({functors.size(), "处理日志任务"});functors.push_back(f1);info.insert({functors.size(), "数据备份任务"});functors.push_back(f2);info.insert({functors.size(), "处理网络连接任务"});functors.push_back(f3);
}//第一个int32_t:进程pid
//第二个int32_t:该进程对应的管道写端fd
typedef pair elem;
int processNum = 5;void work(int blockFd)
{cout << "进程 [" << getpid() << "] 开始工作" << endl;//进行while (true){//阻塞等待 获取任务信息uint32_t operatorCode = 0;ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));if (s == 0)break;assert(s == sizeof(uint32_t));(void)s;//处理任务if (operatorCode < functors.size()){functors[operatorCode]();}}cout << "进程 [" << getpid() << "] 结束工作" << endl;
}
// [子进程的pid,子进程的管道fd]
void sendTask(const vector & processFds)
{srand((long long)time(nullptr));while (true){sleep(1);//选择一个进程 选择进程是随机的 没有压着一个进程给任务 -- 随机的//较为均匀的将任务给所有的子进程 --- 负载均衡uint32_t pick = rand() % processFds.size();// 选择任务uint32_t task = rand() % functors.size();//把任务给一个指定的进程write(processFds[pick].second, &task, sizeof(task));//打印对应的提示信息cout << "父进程指派任务 --> " << info[task] << "给进程:" << processFds[pick].first<< "编号:" << pick << endl;}
}int main()
{loadFunctor();vector assignMap;for (int i = 0; i < processNum; ++i){//定义保存管道fd的对象int pipefd[2] = {0};//创建管道pipe(pipefd);pid_t id = fork();if (id == 0){//子进程读取close(pipefd[1]);//子进程执行work(pipefd[0]);close(pipefd[0]);exit(0);}//父进程做得事情close(pipefd[0]);elem e(id, pipefd[1]);assignMap.push_back(e);}cout << "create all process success !" << endl;//父进程,派发任务sendTask(assignMap);// 回收资源for (int i = 0; i < processNum; ++i){if (waitpid(assignMap[i].first, nullptr, 0) > 0)cout << "wait for : pid =  " << assignMap[i].first << " wait success"<< "number " << i << endl;close(assignMap[i].second);}return 0;
}

我们发现此时父进程5个子进程随机的派发不同的任务,这就是一种进程池。至此匿名管道全部写完。

其中,我们在shell命令行中写的 | 就是匿名管道

1.3管道的特征总结

  1. 管道只能用来进行具有血缘关系的进程之间,进行进程间通信,常用语父子通信。
  2. 管道只能单向通信(由内核设计实现)半双工的一种有特殊情况
  3. 管道自带同步机制(pipe满,write等;pipe空,read满) -- 自带访问控制
  4. 管道是面向字节流的 -- 先写的字符 一定是先被读取的 没有格式边界 需要用户来自定义区分内容的边界 [sizeof(uint32_t)]
  5. 管道的生命周期随进程 -- 管道是文件 -- 进程退出了 曾经打开的文件也会退出

1.4命名管道

我们刚刚提到的都是父子间(血缘)通信,如果我们想要两个毫不相干的两个进程之间通信,应该怎么办。因此我们接下来要讲的就是命名管道。命名管道和之前的匿名管道最大的区别就是可以让任意两个进程之间通信。

1.4.1创建一个命名管道

  • 创建一个命名管道可以在命令上创建 使用如下这个命令

mkfifo filename

这个myfifo就是一个管道文件,前面以p开头。以p开头就是管道,假如我们现在要在左侧想管道内部写入一些东西,在右侧实时查看,当我们左侧回车按下时候,右侧立马出现了“aaaaaa”

但是这样还是不能很好的观察现象,我们在右侧写一个实时的查看脚本,让一直想管道文件内部写入 bbbbb

while :; do echo "bbbbb" ; sleep 1; done >> myfifo

  • 命名管道也可以在程序中创建,相关函数

int mkfifo(const char* filename,mode_t mode);

我们发现命名管道是带路径的,这有什么作用呢? 其实命名管道是通过一个fifo文件,由于这个文件存在路径,我们都知道路径具有唯一性,因此通过路径我们进程都可以看到这一份资源。

1.4.2 命名管道编码

由于我们知道命名管道可以实现两个不想关的进程完成通信,因此我们接下来将写两个文件(进程),让这两个文件进行通信。我们想让clientFifo.cpp这个进程和severFifo.cpp这个进程通过命名管道通信,该怎么写呢?

//comm.h
#pragma once 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define IPC_PATH "./.fifo"// makefile
.PHONY:all
all: clientFifo severFifoclientFifo:clientFifo.cppg++ -Wall -o $@ $^ -std=c++11severFifo:severFifo.cppg++ -Wall -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f clientFifo severFifo .fifo//clientFifo.cpp
//写入
#include "comm.h"using namespace std;int main()
{int pipeFd = open(IPC_PATH,O_WRONLY);if(pipeFd < 0){cerr<<"Open : " << strerror(errno) < 0 ){buffer[s] = '\0';cout<<" 客户端-->服务器#  " << buffer << endl;}else if(s == 0){cout<< "客户端退出了,我也退出了";break;}else{//do nothingcout << "read error" <<  strerror(errno) <

通过这个代码我们成功的在两个毫无相关的进程之间完成了管道通信。

(本篇完)

相关内容

热门资讯

在公司上班员工证明 在公司上班员工证明  一、工作证明的含义  工作证明是指我国公民在日常生产生活经营活动中的一种证明文...
网络主播的聊天技巧 网络主播的聊天技巧  在视频直播间里面好多网络主播都不敢开口讲话,怕说多,怕说错,其实学会说话聊天技...
最美孝心少年事迹材料简介 最美孝心少年事迹材料简介最美孝心少年事迹材料简介1  冯xx,男,汉族,现年13岁,20xx年xx月...
青年文明号事迹材料 青年文明号事迹材料范文(精选7篇)  在日常学习、工作和生活中,要用到事迹的地方还是很多的,事迹具有...
初中数学名师工作室个人研修工... 初中数学名师工作室个人研修工作小结  初中数学名师工作室个人研修工作小结  新世纪学校· 龙小枝  ...
幼儿园教育叙事故事 幼儿园教育叙事故事  幼儿园教育叙事故事(一)  让感谢成为习惯  我在幼儿园担任小班教师,第一个活...
怎样做好学生工作 怎样做好学生工作怎样做好学生工作作为学生干部,他被推举学校或系领导同意后任命,授予了一定的权力,但他...
农村经典母亲祭文 农村经典母亲祭文范文(通用6篇)  祭文,文体名。 祭祀或祭奠时表示哀悼或祷祝的文章。 体裁有韵文和...
建党百年句子 建党百年句子  一、中国共产党建党日  中国共产党于1921年7月23日成立后,在反动军阀政府的残暴...
学史增信心得体会 学史增信心得体会  一、什么是党史  中共党史是政党史,是专史,它研究建国后中国共产党作为执政党的历...
优秀团干部简要事迹材料 优秀团干部简要事迹材料  事迹材料是指党政军机关为了弘扬正气,表彰先进,推动工作,对本单位具有突出事...
建党一百周年绘画作品 建党一百周年绘画作品  一、建党一百周年关于党的历史简介  中国共产党创建于1921年,中国共产党破...
安全生产知识培训的内容 安全生产知识培训的内容  安全生产是安全与生产的统一,其宗旨是安全促进生产,生产必须安全。以下是小编...
贫困救助申请书 贫困救助申请书  在这个高速发展的时代,各种申请书频频出现,利用申请书我们可以表达自己的愿望和诉求。...
2021年支部书记讲党课材料 2021年支部书记讲党课材料  一、什么是党课  党课,是中国共产党的组织对党员和入党积极分子进行教...
宋江简介 宋江简介宋江  中文名称: 宋江  性  别: 男  朝  代: 宋代  生 卒 年: ?~约112...
寒假社区服务记录表 寒假社区服务记录表 服务主题 xx社区  服务时间 xx--xx  服务社区 xxx社区  组织者 ...
有一种爱情叫兄弟5(结局篇)... 有一种爱情叫兄弟5(结局篇)我命如此我转身,慢慢腾腾地顺着楼梯往楼上走着,走过2楼的时候,我听见有人...
安全生产工作的简报 安全生产工作的简报范文(精选11篇)  在快速变化和不断变革的今天,简报起到的作用越来越大,简报一般...
师德师风学习材料 师德师风学习材料  师德师风学习材料(精选8篇)  师德建设决定我国教师队伍建设的成败,也就决定我国...