广德网站开发,squarespace wordpress,买奢侈品去哪个网站有正品,兰州市网站建设公司【Linux系统编程十九】#xff1a;匿名管道原理/模拟实现进程池 一.进程通信理解二.通信实现原理三.系统接口四.五大特性与四种情况五.应用场景--进程池 一.进程通信理解
什么是通信#xff1f;
通信其实就是一个进程想把数据给另一个进程#xff0c;但因为进程具有独立性… 【Linux系统编程十九】匿名管道原理/模拟实现进程池 一.进程通信理解二.通信实现原理三.系统接口四.五大特性与四种情况五.应用场景--进程池 一.进程通信理解
什么是通信
通信其实就是一个进程想把数据给另一个进程但因为进程具有独立性想直接给是不行的。 所以就必须得要有通信的方案。
为什么要通信 主要是要么是传数据要么传指令要么是我们进行多进程间的协同要么是可能是一个进行想通知另一个地方某些事情发生了。 啊不管什么原因反正就是要通信。
怎么通信 但通信的时候呢那么对我们来讲呢竞争具有独立性那通信就有成本那怎么办呢 所以通信它的实现方案那么这里呢本质就一定是先要要让不同的进程先看到同一份公共的资源。 这份公共资源不能属于我们通信的进程当中的任何一个。 啊因为竞争具有独立性如果属于任何一个的话那这个资源就不应该让其他竞争看到。 所以这个资源呢就必须只能由操作系统提供。 一般而言必须只能由操作系统提供。 而我们对应的呃就是操作系统提供的资源呢是不允许任何进程直接去访问的。 啊所以就注定了我们一定要提供大量的我们关于就业通信的相关的系统调用接口。 所以从底层设计上呢我们的操作系统既要给我们提供通信的方案又要给我们提供那么进行通信让用户所调用的接口。 如果我们一个文件它能够被多个进程打开并访问。 那么我们文件呢这玩意儿就不就是一个公共资源了吗 等一个进程往文件里写另一个进程从文件里读。 只要你写完之后把数据直接刷新刷新到磁盘上另一个我们对应的这个进程再从文件里读不就读到数据了吗 好同学们那么这种呢那么思想呢其实对的吗 那么所谓的管道呢其实就是基于文件的一种通信方式。 他的思想呢其实跟我刚刚说的那个其实大差不差只不过呢他并不把数据往磁盘当中做刷新。 谁规定数据一定要刷到磁盘上 那么管道通信时只要建立好双方的通信信道往里写你去读就可以。 不一定非得用我们对应的文磁盘呀我们用内存也可以。
二.通信实现原理
我们每一个文件呢它都天然的要提供一个属于自己对应的叫做页缓冲区。全称叫做文件页缓区我们叫做缓冲区。 从理论上来讲呢如果这个文件就是一个普通文件我们这个文件一定是曾经要么在在磁盘当中被新建的要么在我们磁盘当中被打开的。 反正我们最终那么一个文件在磁盘当中假设它也存在的话它会在特定的分区特定的分组有自己的属性和数据块。在操作系统内核当中它会存在非常非常多的内存级文件。 也就是这样的文件呢我们并不需要在我们对应的磁盘当中真正的存在。 啊那么而是呢最终只要能够在内存里让我们那么能够把它用起来就可以。我们只需要把我们曾经学到的知识里面不要做刷新那么剩下的这不就是内存级文件吗 让进程打开一个文件时再创建一个子进程。子进程会进程父进程的PCB页表地址空间。但文件不会继承。 这样父进程里的文件描述符表和子进程的文件描述符表都指向一样的文件。 刚刚新建的这个内存及文件。父子进程是不是都可以访问都可以被父子进程看到。 还记得进程间通信的本质吗让不同的进程看到同一份资源。这不就做到了吗。 进程先打开文件在fok之后创建子进程进程。 此时如果我们有能力打开这种文件的话那么此时它不就叫做父进程程和子进程看到了同一个文件吗那么这个同一个文件它是内存级的。因为每个文件都存在自己对应的缓冲区。 双方就可以实现进程间通信了。 在系统当中父进程在打开我们对应的一个文件的时候呢。 它并不是只是单方面的去把一个文件以读写方式打开或读或写都不是。他在创建这个管道时把同一个文件既以读方式打开又以写方式打开。 所以父进程打开文件的时候以读写方式整体把管道文件打开。 那么接下来呢我们的父进程呢在fork创建出子进程后。 子进程它会拷贝父进程的文件描述符表所以它们两个当中父进程和子进程都会有对应的读写端。指向同一个缓冲区
好这就是我们的管道啊。 至于父进程是读子进程写还是父进程是写子进程是读需要用户来决定 只要让父进程和子进程它们各自要关闭对应的读写端来形成一个叫做单向通信的信道。比如说图当中呢它是想要父进程进行写入。子进程读取只要将父进程就把曾经自己的读端这个关掉。对应的子进程呢它想对我们对应的管道来进行读取读取所以他就把自己的写入关掉。 关掉了这次我们的父子进程就可以使用父进程剩下一个写端子进程剩下一个读端那么建立了这样的一种通信信道。 这种基于文件级别的通信方式那么正是因为它只能进行单向通信。所以我们命名为叫做管道。
以上所做的工作叫做建立通信信道。现在父子进程还没进行通信。
三.系统接口 这个系统调用接口很简单它的参数是一个数组数组只有两个参数。而这个参数是作为输出型参数的。将文件的读写描述符带出来。默认pipefd[0]是读端pipefd[1]是写端。 而这个系统调用所做的工作就是我们上面所讲的实现原理做的工作即建立信道。这个信道是由固定大小的一般为64KB. 创建完信道后我们还有件事情就是要形成单向信道这件事需要让父子进程共同完成父进程需要读就要手动将写端关闭。 子进程需要写就要手动将读端关闭。 最基本的一个叫做我们建立单向信道的过程。建立完单向信道后就可以往信道里写入信息了。写入信息时要注意管道本质上是文件。而文件本质上是内核资源。操作系统允不允许你的父进程和子进程直接去访问这个文件资源呢只能通过系统调用接口去访问所以只能使用write和read系统调用接口进行。
四.五大特性与四种情况 管道的特性有五个 一只有具有血缘关系的进程才可以进行通信。
二管道只能单向通信。 三父子进程是会进行协同的。 四管道是面向字节流的。 五管道本质就是文件属于内核资源进程结束就会自动释放。
管道还会出现四种情况 1.(写段写的速度比读端要慢)读写端都正常管道如果为空那么读端就要阻塞。 2.(写端写的速度比读端要快)读写端正常管道如果被写满写段就要阻塞。 3.(写端关闭)读端正常写端关闭读端就会读到0表面读到了管道的结尾处读端对应的进程并不会阻塞。 4.(读端关闭)写端正常读端关闭操作系统就要杀死正在写入的进程因为没有意义。利用信号杀死。
五.应用场景–进程池
在shell里就存在管道竖画线它就表示管道。 第一当我用管道集连起对应的这若干个命令时。 那么其中每一个对应的命令最终都会被直接启动成一个进程。 也就是他们这些进程是同时被起来的啊也就就是你在跑的同时我也在跑只不过我在等你啊。这表面它们之间都是子进程。谁的子进程呢bash的子进程它们都是bash的子进程所以可以利用管道。 两个竖画线分割三个命令。创建两个管道之后然后连续再创建三个子进程然后每个进程程序计划执行不同的命令。在执行之前呢我们需要对每一个进程的标准输出啊标准输入进行一下重定向。每一个管道它都有自己的读端和写端。勾连起这几个命令然后呢让他们直接互相通信起来。 第一个进程的标准输出重定向到我们管道的写端中间进程的标准输入重定向到上一个管道的读端。表示我本来往显示器上进行打印现在呢把显示器上打印改成向管道里去进行我们对应的写入。 中间有个进程呢它呢本来读的时候是从我们对应的键盘读取今天读的时候我就不要让它从键盘读而是从我们对应的管道文件里读显示的时候就不用再写了不用向显示器写了而是继续向后面的管道再继续写入啊所以做一堆的重定向。 最后一个进程的话做一下输入重定向。这就是shell中的重定向实现原理。
管道还有一个应用场景进程池。 我们能不能提前把一批任务先建立好当有任务到来时然后我直接指派给其中一个进程呢。 其中我们一次把对应的一批进程直接创建好那么这个工作我们就叫做我们先进行一次叫做进程池的储备。 也就是把一个一个的进程呢当做一一份一份儿一份的资源提前储备好提前做个好。 当我们需要的时候再去指派让他去帮我们去完成任务。 首先对应的父进程。在正式接受新需求接受新任务之前。 啊我先一次性同时创建出若干个子进程。然后呢为了后面呢更好的去控制上面的所有的子进程。 我呢想做这样的工作为我的父进程想和其中我所创建的每一个子进程。都建立一条叫做管道的信道。 一个进程建立一个管道和第二个进程建立一个管道和第三个建立管道第四个建立管道和第五个建立管道和第六个建立管道……以此类推。 然后我们让每一个子进程只从管道当中进行读取。父进程呢它把控这批管道。那么父进程想那往哪个管道里写内容他就可以直接向哪个管道里写内容。 那么其中我们对应的父进程如果没有像第一个管道里写任何内容请问这个子进程在干什么 这个子进程它在读取等待我们管道流数据。 那么也就是说子进程当前就阻塞在这个管道当中他就在等这个父进程给他任务呢。 那么父进程呢不给你写子进程就等着呗。 然后父进程一旦向管道当中写了写了之后那么这个进程会读到对应的数据然后这个子进程会继续向后执行。向后我就提前可以让这个进场。那么结合他读进来的数据以及他向后执行这个动作然后我们就可以让这个子进程上去执行对应的任务了。 好换句话说从此往后我的父进程我们规定父进程向子进程当中管道里写的我们把它都叫做一个一个的任务。(父子通信时那么父进程每次写入时只能有写四个字节) 我们对应的父进程呢他想布置任务的时候他无非就是做两件事情。第一个叫做选择任务。第二个叫做选择进程。 好也就是他把任务确定好了。第二他把进程确定好了他就可以把这个任务指派给其中的某一个子进程去运行。 #include TASK.hpp
#include iostream
#include string
#include unistd.h
#include vector
#include sys/wait.h
#include sys/stat.h
#include time.h
#define N 5std::vectortask_t tasks;
//master----[]---slavermaster通过信道控制子进程
//先描述再组织这个信道是由管道和子进程的pid构成class channel
{
public:channel(int cmdfd,pid_t slaverid,const std::string processname):_cmdfd(cmdfd),_slaverid(slaverid),_processname(processname){}public:int _cmdfd;//发送任务的文件描述符pid_t _slaverid;//子进程的pidstd::string _processname;//子进程的名字
};
std::vectorchannel channels;
//在创建子进程之前创建管道,然后让父子进程分别关闭一端
void slaver()
{//子进程直接从标准输入里就可以获取到任务码,就没有管道的概念了while(1){int cmdcode0;//将任务码带出来int nread(0,cmdcode,sizeof(int));if(nsizeof(int)){//根据任务码的不同执行不同的函数std::coutslaver say get a command:getpid(): code-- cmdcodestd::endl; tasks[cmdcode]();}if(n0)//说明读取完 没有可读的了子进程就可以退出了break;}sleep(3);
}
void InitProcesspoll(std::vectorchannel* channels)//输出型参数用指针
{for(int i0;iN;i){int pipefd[2];pipe(pipefd);//创建管道pid_t idfork();if(id0){close(pipefd[1]);//子进程,读关闭写dup2(pipefd[0],0);//--重定向到标准输入slaver();//子进程执行任务,从信道里读取任务码封装成一个函数但这里我们可以重定向向标准输入里读取exit(0);}close(pipefd[0]);//父进程写关闭读//管道和子进程都创建好了可以初始化信道了std::string nameprocess-std::to_string(i);channels-push_back(channel(pipefd[1],id,name)); }
}
void ctrlSlaver(std::vectorchannel channels)//输入输出型参数用引用
{//接下来就是父进程控制子进程发送任务给子进程int cnt5;while(cnt--){//1.选择任务int cmdcoderand()%tasks.size();//2.选择进程int processposrand()%channels.size();//可以通过这个位置找到信道找到子进程std::coutfather say comcode:cmdcode have send tochannels[processpos]._slaverid-channels[processpos]._processnamestd::endl;//3.发送任务//选好子进程后就可以往这个信道里发送任务write(channels[processpos]._cmdfd,cmdcode,sizeof(int));sleep(2);}
}
void QuitProcess(const std::vectorchannel channels)
{//如何关闭子进程只要让写端关闭读端读到0就自然会跳出循环for(auto e: channels){close(e._cmdfd);}sleep(5);//子进程跳出循环后就会退出父进程等待子进程即可for(auto e:channels){waitpid(e._slaverid,nullptr,0);}
}
int main()
{srand(time(nullptr));//生成随机数LoadTask(tasks);//1.把任务加载进来InitProcesspoll(channels);//2.初始化--创建管道创建子进程初始化信道。ctrlSlaver(channels); //3.开始控制子进程QuitProcess(channels);//4.清理收尾}