【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 5
std::vector<task_t> tasks;
//master----[]---slaver,master通过信道控制子进程
//先描述,再组织:这个信道是由管道和子进程的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;//子进程的pid
std::string _processname;//子进程的名字
};
std::vector<channel> channels;
//在创建子进程之前创建管道,然后让父子进程分别关闭一端
void slaver()
{
//子进程直接从标准输入里就可以获取到任务码,就没有管道的概念了
while(1)
{
int cmdcode=0;//将任务码带出来
int n=read(0,&cmdcode,sizeof(int));
if(n==sizeof(int))
{
//根据任务码的不同执行不同的函数
std::cout<<"slaver say@ get a command:"<<getpid()<<": code--> "<<cmdcode<<std::endl;
tasks[cmdcode]();
}
if(n==0)//说明读取完 ,没有可读的了,子进程就可以退出了
break;
}
sleep(3);
}
void InitProcesspoll(std::vector<channel>* channels)//输出型参数用指针
{
for(int i=0;i<N;i++)
{
int pipefd[2];
pipe(pipefd);
//创建管道
pid_t id=fork();
if(id==0)
{
close(pipefd[1]);//子进程,读,关闭写
dup2(pipefd[0],0);//--重定向到标准输入
slaver();//子进程执行任务,从信道里读取任务码,封装成一个函数,但这里我们可以重定向,向标准输入里读取
exit(0);
}
close(pipefd[0]);//父进程写,关闭读
//管道和子进程都创建好了,可以初始化信道了
std::string name="process-"+std::to_string(i);
channels->push_back(channel(pipefd[1],id,name));
}
}
void ctrlSlaver(std::vector<channel>& channels)//输入输出型参数用引用
{
//接下来就是父进程控制子进程发送任务给子进程
int cnt=5;
while(cnt--)
{
//1.选择任务
int cmdcode=rand()%tasks.size();
//2.选择进程
int processpos=rand()%channels.size();//可以通过这个位置找到信道,找到子进程
std::cout<<"father say comcode:"<<cmdcode<<" have send to"<<channels[processpos]._slaverid<<"->"<<channels[processpos]._processname<<std::endl;
//3.发送任务
//选好子进程后,就可以往这个信道里发送任务
write(channels[processpos]._cmdfd,&cmdcode,sizeof(int));
sleep(2);
}
}
void QuitProcess(const std::vector<channel>& 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.清理收尾
}