网站建设怎么找到客户,电子商务网站采用的开发技术,宁晋网站建设公司,dedecms网站首页#x1f496;作者#xff1a;小树苗渴望变成参天大树#x1f388; #x1f389;作者宣言#xff1a;认真写好每一篇博客#x1f4a4; #x1f38a;作者gitee:gitee✨ #x1f49e;作者专栏#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法#x1f384; 如 果 你 … 作者小树苗渴望变成参天大树 作者宣言认真写好每一篇博客 作者gitee:gitee✨ 作者专栏C语言,数据结构初阶,Linux,C 动态规划算法 如 果 你 喜 欢 作 者 的 文 章 就 给 作 者 点 点 关 注 吧 文章目录 前言一、进程间通信的三个问题1.1什么是进程间通信1.2 为什么要进程间通信1.3 怎么做到进程间通信 二、管道的原理三、接口的测试四、编写代码进行通信4.1管道的四种情况4.2 管道的五大特性 五、基于管道设计一个简单的进程池六、总结 讲解逻辑
根据前面的知识来推测进程之间大致是怎么通信的直接讲解基于文件级别的通信方式关于进程间通信的接口。五大特征以及四种情况谈谈应用场景
前言
今天我们开始讲解进程间通信我们之前讲过进程具有独立性那么有的时候进程还是需要进行一些数据性的交换但是又不能破怪独立性这两者看着自相矛盾但又不冲突博主就是来带大家去解决这个问题从原理到模拟实现一个进程间通信的程序需要大家对之前的进程创建进程等待尤其是文件系统那一章节熟悉那今天的内容才容易理解所以希望没有这些知识储备的小伙伴可以先看我前面的博客讲解再来看这篇效果会更好接下来我们开始进入正文的讲解。 一、进程间通信的三个问题
1.1什么是进程间通信
简单的来说就是两个或者多个进程实现数据层面的交互因为进程独立性的存在导致了进程间通信的成本比较高所以在一会的讲解过程种大家可能会觉得进程间的通信挺费劲的这都是情理之中的。
1.2 为什么要进程间通信
数据传输一个进程需要将它的数据发送给另一个进程资源共享多个进程之间共享同样的资源。通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变.这点一会会模拟实现一个类似于进程池的让主进程控制子进程做任务。
1.3 怎么做到进程间通信
以两个进程间通信为例由于进程间是独立性的想要实现通信需要找一个公共的资源让这两个进程看到同一份资源但这份资源又不属于这两个进程的任意一个这样就不会破坏两者的独立性。
1“资源”指的是什么是一块特定的内存空间。 2 这个“资源”谁提供一般情况下os提供。 为什么不是两个进程的其中一个假设是其中一个另一个读取这个数据或者修改就对拥有这个资源的进程产生影响破坏独立性用反证法也可以论述我开头说的第一句话。
3 由上面两点我们得出结论我们进程通过访问这个“资源”也就是这一块内存空间进行通信本质就是在访问os,我们的进程是通过用户编写代码形成可执行程序形成进程运行在os上所以可以间接认为进程代表的就是用户既然是用户os系统最不信任的其实就是用户所以在进程间进行通信的过程就是用户之间进程通信中间os从创建这个‘’资源‘’使用释放一般今天博主讲的方式是要通过系统接口去调用但是其他方式可能不需要都需要通过系统接口去调用从底层设计这些接口设计都需要os独立去设计的一般操作系统都会设计一个独立的通信模块IPC通信模块归属于文件系统。一会再来介绍为什么归属于文件系统这些通信之间想要实现再任何一台主机或者不同的主机上运行就必须采取同一种方式就是制定一套标准再网络部分也有标准由了这些标准才有了我们现在的互联网。 上面的一切都是我们之前学过的知识来推测出来进程间通信会这样搞的这也更好的衔接我们的知识那我们有哪些标准对于本机内部system V对于网络posix system V System V 消息队列System V 共享内存System V 信号量 posix 消息队列共享内存信号量互斥量条件变量读写锁 对于system V只讲第二点其余两种不介绍对于posix我们等到讲解网络的时候再讲现在知道我们有这两个标准就好了。 管道 还有一种通信方式就是管道这种通信方式可以说非常的简单因为他是基于文件级别的通信方式目前也可以简单理解复用了文件的那一套。他的大致想法是进程管理系统和文件系统这两者是独立的进程之间打开相同的文件只不过把这个文件的引用计数改变一下而已这个文件不属于两个进程中的任意一个。通过对文件的读写来交换数据这样就保证了进程之间是独立的。 但是里面的细节还是比较多接下来我们开始进入下一个话题。
二、管道的原理
什么是管道 管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道 再我们命令行之前使用|来表示管道每条命令就是一个进程 管道的特点就是一头进一头出是单向的一会介绍也是单向通信方式下面介绍的是匿名管道 因为管道是基于文件级别的通信方式我们刚讲解完文件系统没多久所以大家能更好的理解。以父子进程为例因为我们现在没有办法控制两个没有关系的进程所以使用父子进程这也是一个铺垫。 我们先来讲解原理一来看图解(解释是怎么让不同的进程看到同一份资源) 讲解原理二来看图解(单向通信的设计) 大家看到这里对于单向通信的原理应该理解了吧。 再来看一个生动的图这里面的系统调用接口一会再介绍 通过上面的原理介绍我们还有问题
怎么实现双向通信 建立多个管道我们上面进程是父子关系如果两个进程没有任何关系可以这样去设计吗不能只有父子兄弟爷孙有血缘关系的进程才可以常用于父子间。我们刚才的管道有名字吗没有因为是内存级文件再内存中要名字没啥意义所以这就是匿名文件也就是匿名管道。那我们进程之间进行通信了吗没有我们知识建立了通信信道为什么这么费劲进程间具有独立性通信是有成本的。 至此我们管道的原理就讲解完毕接下来我们去使用一些接口来进行测试一下。 三、接口的测试
我们来认识一个系统调用接口pipe,来看文档 pipe就是建立进程间通信的按照刚才的分析自己写的程序出来默认打开的三个文件就没有再打开其他文件如果使用pipe建立信道那么pipe会在内存给我们创建一个内存级文件传进去的参数pipefd数组的返回就会带出两个文件描述符3和4一会就来测试会不会出现这样的效果: 来看代码
#includestdio.h
#includeiostream
#includeunistd.h
using namespace std;int main()
{int pipefd[2];int npipe(pipefd);if(n0)return 1;coutpipefd[0]:pipefd[0] pipefd[1]:pipefd[1]endl;return 0;
}我们看到结果我们分析原理的时候是一模一样的。接下来我将写一个程序来带大家实现父子进程间通信也是为了更好介绍管道的四种情况。 为什么没有创建子进程就可以进程pipe呢原因是我们的管道文件只要一个进成建立好。子进程拷贝父进程数据增加一个执行那个就可以就好比下面这个图 pipefd[0]:读端 pipefd[1]:写端 四、编写代码进行通信
#includestdio.h
#includeiostream
#includeunistd.h
#includestring
#includestdlib.h
#includecstring
#includesys/types.h
#includesys/wait.husing namespace std;
#define N 2
#define NUM 1024void Writer(int wfd)
{string shello,pipe;pid_t cidgetpid();//获得当前进程的pidchar buffer[NUM];//减少io交互次数int numbers0;int cnt5;while(true){sleep(1);方法1//sleep(1);//buffer[0]0;// snprintf(buffer,sizeof(buffer),%s-%d-%d,s.c_str(),cid,numbers);//因为多写一些内容上去所以使用一个buffer过度一下// write(wfd, buffer, strlen(buffer));//发送给父进程方法2// string s1s;// s-;// sto_string(cid);// s-;// sto_string(numbers);// write(wfd,s.c_str(),strlen(s.c_str()));//发送给父进程// ss1;/因为是不终止的给父进程写数据所以使用了buffer现在只给父进程写5次每次写一个字符char cc;write(wfd,c,1);numbers;cout-numbersendl;if(numbers5){break;}}
}void Reader(int rfd)
{char buffer[NUM];while(true){buffer[0]0;size_t nread(rfd,buffer,sizeof(buffer));if(n0){buffer[n]0;//因为读取出来没有结束标志coutfather get msg:[getpid()]:bufferendl;}else if(n0){printf(father read file done\n);break;}else{break;}}
}
int main()
{int pipefd[N]{0};int npipe(pipefd);if(n0)return 1;//coutpipefd[0]:pipefd[0] pipefd[1]:pipefd[1]endl;pid_t idfork();//让子进程写父进程读取if(id0){perror(fork);return 2;}else if(id0){//child;//coutpipefd[0]:pipefd[0] pipefd[1]:pipefd[1]endl;close(pipefd[0]);//子写关闭读端//coutpipefd[0]:pipefd[0] pipefd[1]:pipefd[1]endl;Writer(pipefd[1]);close(pipefd[1]);exit(0);}//fatherclose(pipefd[1]);//父读关闭写端Reader(pipefd[0]);int status0;pid_t retwaitpid(id,status,0);if(retid)//等待成功{coutchild:ret,exitcode:((status8)0xFF),signlcode:(status0x7F)endl;}close(pipefd[0]);sleep(5);return 0;
}我们来看结果一我们通过子进程给父进程不断的发送一句话 写端一直写读端就一直读 来看结果二子进程只给父进程发送五次每次发送一个字符 写入五次之后放下管道里面为空就读不到数据了。 4.1管道的四种情况
读写端正常如果管道为空读端就要被阻塞 我们来验证一下我们将刚才的程序小改一下把子进程的write函数注释掉这样管道就是一直为空而父进程的read不管读取成功还是失败都会返回一个值把这个值打印出来如果没打印出来就说明read这个函数在等待读取还没有返回值。为空为什么不结束因为写端可能随时写数据进来所以读端要阻塞等待。这个要和第三种情况区分开。 读写端正常如果管道被写满写端就要被阻塞。 我们的子进程一直往管道文件里面写入一次写入一个字符而父进程不读取就会一直往管道文件里面写写满了就被阻塞了。使用一个变量记录写了多少次把管道文件写满方便我们去计算管道文件的大小。 通过结果发现我们写到65536次就开始写端就开始阻塞了因为我们是一次往管道里面写一个字符所以我们的管道文件大小为65536/102464kb, 我们使用ulimit -a来查看cat /etc/redhat-release查看内核版本 在我这台机器的版本下我们的管道文件大小居然是4kb与我们计算的不一样啊这是为什么呢我们来查看一下官方文档man 7 page - /page 跳转到这个文档。 前两点我们刚才已经认证过了第三点提到了一个原子性这是什么给大家举个例子假设子进程想给父进程写一句话hello,world,当刚写完hello准备写world的时候父进程看到管道里面有数据直接就把hello读取走了这样父进程就不是一起读到这个helloworld这个数据所以在posix标准里面规定写入的数据小雨这个pipe_buf大小的时候即使管理里面有数据父进程也不读取这样就保证了原子性我们可以理解为刚才看到的pipe size就是这个pipe_buf的大小这个知识大家了解一下就可以了主要记住管道文件是有固定大小的看不同的内核版本大家可以按照我上面的测试方法去计算一下自己的管道文件大小是多少。 读端正常读写端被关闭读端就会读到0,表明读到文件尾了不会被阻塞。 我们子进程先给父进程给5次父进程一直在读取然后直接关闭自己的写端前五次的写入已经被读取走了所以当子进程关闭自己的写端那么父进程此时就发现管道文件为空就读取到文件结尾read函数返回的是0因为写端已经被关闭里面不会有数据了所以父进程的读端如果阻塞没有意义还占资源所以读端就直接读结束了。 我们的程序并没有想第一种情况一种在阻塞等待而是直接结束了 写端正常写读端被关闭os就会直接杀掉正在写的进程 让子进程的写端不断的给父进程写数据父进程读取五次后就关闭自己的读端按照结论子进程会被杀死这就是之前的知识进程进程进程没运行完异常退出了看退出码和系统信号。 我们看到我们的读端一旦关闭子进程就被杀死退出被父进程的waitpid获取到了我们来看一下13号信号是什么 就是管道信号符合我们的测试。这也是博主为什么设计出父进程读子进程写的目的就是为了第四种情况了做实验的等到讲解一个简单的进程池的时候反过来让读者去感受一下 4.2 管道的五大特性
通过上面的四种情况以及管道的原理我们很清楚的知道你名管道具有下面的五点特性
匿名管道是具有血缘关系的进程间进行通信。只能进行单向通信管道是基于文件操作的而文件的生命周期是随进程的后面介绍的有名还有共享内存都不是随着进程的生命周期的通过管道的前两种情况匿名管道是具有同步互斥机制的而后面说的共享内存不具有。管道是面向字节流的这个后面说到多线程的时候再讲。
总结 对于上面的情况都是基于最上面的代码进行修改去测试的每种情况要修改那部分博主也截出来了大家先把我一开始写的程序理解了然后在测试这些情况不然很摸不清楚头脑。
五、基于管道设计一个简单的进程池
大家还记得我们的内存吃他的见到理解就是我们如果想要100mb的空间我们不需要一次申请10mb,申请十次这样会增加消耗所以一次申请100mb放到内存池里面和自己打交道总比和内存打交道省事我们的进程池也是类似的道理先创建对歌子进程然后想要哪个进程做事就直接分配不需要在创建进程了就好比公司先做人才储备需要的时候上如果没有人才储备到时候在要人就来不及了让我们一起来看看这个进程池怎么去实现吧。
test.cpp
#includeiostream
#includestdio.h
#includestring
#includevector
#includestdlib.h#include unistd.h
#include sys/types.h
#include sys/wait.h#include test.hpp
using namespace std;#define process 5 //这是标志一个进程里面有多少个子进程
#define N 2 //这是管道文件返回的数组的大小 vectortask _task;//任务数组//本程序是让父进程
class channls
{
public:channls(const intfd,const pid_tid,const stringprocessname):_fd(fd),_id(id),_processname(processname){}
public:int _fd;//文件描述符pid_t _id;//进程pidstring _processname;//进程名字方便我们观察
};void slaver();//声明
int Initprocess(vectorchannls cls)
{for(int i0;iprocess;i){int pipefd[N];int npipe(pipefd);if(n0){perror(pipe:);return 1;}pid_t idfork();if(id0){perror(fork:);return 2;}else if(id0){//childclose(pipefd[1]);//关闭写端dup2(pipefd[0],0);close(pipefd[0]);//放在这里也可以slaver(); //为了不给这个函数传参数才使用上面的函数一会从键盘文件进行读取就可以了不然就需要这样 slaver(pipefd[0]);//close(pipefd[0]);coutprocess:getpid()quitendl;exit(-1);}//fatherclose(pipefd[0]);//关闭读端string nameprocessto_string(i1);cls.push_back(channls(pipefd[1],id,name));//父进程会返回子进程的id所以这里面的id是子进程的id将自己的写端给子进程到时候直接往子进程里面写入//和每个进程之间都会建立一个管道文件按照文件描述符分配规则父进程的写端的下标会递增。}
}
void debug(const vectorchannls cls)//测试有没有初始对不对有没有建立进程池将每个进程进行初始化
{for(const autoe:cls){coute._fd e._id e._processnameendl;}
}void slaver()//子进程收到任务去执行任务
{int cmdcode0;//收到父进程发过来的任务指令while(true){size_t nread(0,cmdcode,sizeof(int));//如果父进程一直没有给子进程发送数据就会阻塞等待if(nsizeof(int))//读取到父进程给我发送的任务了{coutslaver say get a command:getpid(): cmdcode:cmdcodeendl;if(cmdcode0cmdcode_task.size())//下标的映射关系{_task[cmdcode-1]();//调用对应的任务coutendl;}}if(n0)break;}
}void menu()
{cout********************************endl;cout*****1. 任务1 2. 任务2*******endl;cout*****3. 任务3 4. 任务4*******endl;cout*************0.退出*************endl;cout********************************endl;
}void quitprocess(const vectorchannls cls);
void ctrlSlaver(const vectorchannls cls)
{//这是一直随机给子进程分派任务// while(true)//一直给子进程发送任务如果想控制次数再循环里面操作break即可// {// int whichrand()%cls.size();//随机得出子进程所在数组的下标// int cmdcoderand()%_task.size()1;//因为任务也是数组存储起来的所以父进程给子进程发一个存储任务数组的下标消息就可以了// coutfather say:cmdcode:cmdcodealready sentocls[which]._id processname:cls[which]._processnameendl;// write(cls[which]._fd,cmdcode,sizeof(int));//发送任务// sleep(1);//每隔一秒发送一次任务给子进程// }//这是轮转的给子进程发任务// int which0;//这是选择哪一个进程// while(true)//一直给子进程发送任务如果想控制次数再循环里面操作break即可// {// //随机得出子进程所在数组的下标// int cmdcoderand()%_task.size()1;//因为任务也是数组存储起来的所以父进程给子进程发一个存储任务数组的下标消息就可以了// coutfather say:cmdcode:cmdcodealready sentocls[which]._id processname:cls[which]._processnameendl;// write(cls[which]._fd,cmdcode,sizeof(int));//发送任务// sleep(1);//每隔一秒发送一次任务给子进程// which;// which%cls.size();// }//自己制作一个菜单给子进程发送任务menu();while(true){int whichrand()%cls.size();//随机得出子进程所在数组的下标cout请输入你的选择;int cmdcode0;cincmdcode;if(cmdcode0||cmdcode4){break;}coutfather say:cmdcode:cmdcodealready sentocls[which]._id processname:cls[which]._processnameendl;write(cls[which]._fd,cmdcode,sizeof(int));//发送任务sleep(1);//每隔一秒发送一次任务给子进}
}void quitprocess(const vectorchannls cls)
{for(const auto e:cls) close(e._fd);//子进程读端被关闭就会被信号杀掉等着父进程回收for(const auto e:cls) waitpid(e._id,NULL,0);
}
int main()
{LoadTask(_task);srand(time(nullptr)^getpid()^1023);//种一个随机数种子vectorchannls cls;//就类似于进程池coutgetpid()endl;Initprocess(cls);//初始化ctrlSlaver(cls);//父进程开始控制子进程quitprocess(cls);return 0;
}test.hpp:声明和定义可以在一起的头文件再模板那一节应该提到过
#includestdio.h
#includeiostream
#includevector
using namespace std;
typedef void(*task)();void task1()
{cout任务1endl;
}void task2()
{cout任务2endl;
}void task3()
{cout任务3endl;
}void task4()
{cout任务4endl;
}void LoadTask(vectortask* _t)
{_t-push_back(task1);_t-push_back(task2);_t-push_back(task3);_t-push_back(task4);
}通过这个程序大家应该感觉有点意思了这个代码的所有注释都写了大家下去好好研究一下。 上面程序的bug 上面的程序有一个隐藏的bug但是影响不大大家有没有发现博主的退出进程的函数两个循环是分成写为什么不一起写例如下面这样
void quitprocess(const vectorchannls cls)
{for(const auto e:cls){close(e._fd);waitpid(e._id,NULL,0);}
}给大家画个图 因为我们是循环创建子进程这样就导致后面创建子进程的时候父进程和上一个子进程的写端还没有关闭就被继承下来了这样就导致我们的父子进程之间不是只有一个写端和一个读端的单向通信了我们的子进程之间也可以进行互相通信如果按照一个循环的方式去写目的是想让写端关闭读端就会读到文件尾符合第三种情况但是我们的第一个进程不止一个写端所以一个循环是解决不了的。 但是我们的最后一个进程的管道是只有一个写端和一个读端的我们先把这个写端关闭这个进程就会终止他上面继承下来的写端就会关闭导致在他创建进程的前面的所有子进程的管道的写端指向都少一个这样关闭释放到最后一个子进程的时候也是只有一个写端了。
void quitprocess(const vectorchannls cls)
{int lastcls.size()-1;for(int ilast;i0;i--)//从后往前释放根据进程中指对应的写端下标也就没有了。{close(cls[i]._fd);waitpid(cls[i]._id,NULL,0);}
}但是我们的bug还是没有解决因为还是存在子进程之间互相通信的可能所以我们想要解决这个问题我们就要使用一个数组讲父进程的写端下标保存起来在子进程里面遍历数组将其关闭即可。 加这三处代码也可以解决这个问题该说不说这个bug藏的挺身但是对这个程序影响不大也是通过这个bug让大家可以更好的理解父子进程间通信的原理博主认为人只要一学过难一点的知识前面那些一开始认为难的也会变得简单容易理解多了这也是为什么要坚持下去的原因只有坚持到后面理解东西的成本就会越低这样学起来才更有信心。
六、总结
今天讲的知识是匿名管道他的原理不难理解我们要明白进程间通信的本质是什么让不同的进程看到同一份资源这个在后面讲解命名管道和共享内存的时候都会讲所以希望大家好好的理解这篇的知识点尤其要好好理解进程池这个代码我们下篇再见