Inter-process communication - process pool design

Process pool design

code purpose

Create a parent process and multiple child processes, and complete inter-process communication between the parent and child processes through anonymous pipes. Let the parent process act as the writing end, and the child process act as the reading end, and the parent process randomly writes data to any child process to let the child process complete the corresponding task.

head File

#include<iostream>
#include<vector>
#include<unistd.h>
#include<string.h>
#include<cassert>
#include<ctime>
#include<sys/types.h>
#include<sys/wait.h>

Operate on subprocesses

Create a child process object and put the child process object into the array

//创建子进程对象
class  SubEp//endpoint---子进程对象
{
    
    

public:
SubEp(pid_t subid,int writefd)//第一个参数是子进程的pid,第二个参数是该子进程读端对于父进程的写端fd
:_subid(subid)
,_writefd(writefd)
{
    
    
   char namebuffer[1024];
   //第一个参数是表示第几号子进程,第二个参数是子进程的pid,第三个参数是该子进程读端对于的父进程的写端
   snprintf(namebuffer,sizeof namebuffer,"process: %d [pid(%d) - fd(%d)]",num++,_subid,_writefd);
   _name=namebuffer;
}
public:
static int num;
string _name;
pid_t _subid;
int _writefd;//该子进程与父进程匿名管道对于的父进程的写端fd
};
int SubEp::num=0;
  • The subprocess object needs to pass two parameters to initialize the member variables _subid and _writefd. One is the pid of the child process, and the other is the file descriptor fd corresponding to the write end of the parent process at the read end of the child process

  • The member variable num indicates the number of sub-processes created. The first sub-process created is 0. After use, the num of subsequent sub-processes is 1, 2, and so on. Therefore, num cannot be destroyed after leaving the scope of the SubEp object, so it is defined as static, and the life cycle of the variable num depends on the life cycle of the SubEp class

  • The member variable _name is initialized with namebuffer to identify other member variables of the subprocess

Create a list of tasks that need to be executed by the child process

//创建父进程给子进程派发的任务列表
typedef void(*func_t)();//函数指针类型,函数返回值为void

void downloadTask()//模拟下载任务
{
    
    
    cout<<getpid()<<": 下载任务\n"<<endl;
    sleep(1);
}

void fflushTask()//模拟刷新任务
{
    
    
    cout<<getpid()<<": 刷新任务\n"<<endl;
    sleep(1);
}

void subscribeTask()//模拟订阅任务
{
    
    
    cout<<getpid()<<": 订阅任务\n"<<endl;
    sleep(1);
}
//把上面的三种任务load到列表中即让存放函数指针的vector的各个指针能够指向上面的函数,为了后面方便调用
void loadTaskFunc(vector<func_t>*out)
{
    
    
    assert(out);//vector创建成功
    out->push_back(downloadTask);
    out->push_back(fflushTask);
    out->push_back(subscribeTask);
}
  • The tasks that the child process needs to perform are all function objects. Create an array out whose objects are function pointers, insert the task function tail into the array out through the loadTaskFunc function, and then return it through the output parameter.

Create a pipe for communication between the child process and the parent process, and let the child process block the read

void CreateSubProcesses( vector<SubEp>*subs,vector<func_t>& funcMap)
{
    
    
    vector<int> deleteFd;
//创建子进程并且创建好父进程与各个子进程通信的管道
int fds[2];
for(size_t i=0;i<PROCESS_NUM;i++)//创建子进程
{
    
    
        int n=pipe(fds);//建立父子间进程的匿名管道--建立成功返回0,建立失败返回-1
        assert(n==0);//判断管道是否建立成功
        (void)n;

        pid_t id=fork();//创建子进程
        if(id==0)
        {
    
    
             for(size_t i=0;i<deleteFd.size();i++) close(deleteFd[i]);//因为有写实拷贝,所以这里关闭不会影响父进程
//因为子进程会继承父进程文件描述符表,所以上一个子进程的读端对应的父进程的写端这个进程也会继承下来,即当前子进程和上一个子进程之间也有匿名管道
//可能会导致上一个子进程的父进程读端关闭,而此时还有当前这个子进程的读端连接着上一个子进程,使得上一个子进程不能正常关闭读端而造成bug
//所以要手动关闭当前子进程对应上一个子进程的读端的写端。
            close(fds[1]);//关闭子进程的写端-保留读端负责读
           //对子进程操作
     while(true)
     {
    
    
        //1. 获取任务码,让子进程阻塞等待父进程写写入的任务码,
        int taskcode=receiveTask(fds[0]);
        //2.完成任务--调用对应任务码的函数
        if(taskcode>=0 && taskcode<funcMap.size())
         funcMap[taskcode]();//调用函数完成任务
        else if(taskcode==-1)  break;
     }
            exit(0);//子进程退出
        }
    //这里往后是父进程语句
  //写端关闭,读端读到0然后读端自己关闭
        close(fds[0]);//关闭当前子进程与父进程相联系的匿名管道的父进程的读端
        SubEp sub(id,fds[1]);//第一个参数传的是子进程的pid,第二个参数传的是此时子进程读端对于的父进程的写端
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);//记录当前的写端供下个子进程用

}
  • In the function CreateSubProcesses, the anonymous pipeline connected to the parent process is first established, and then the child process is created. The child process also copies a file descriptor table of the parent process, which can be connected to the anonymous pipeline through the file descriptor, so the pipeline for parent-child process communication Build complete.

  • In the parent process statement, it should be noted that the child process object is constructed by passing the parameter pid of the child process and the write end fd of the parent process for the child process read end to the SubEP class, and the object is put into the array subs.

  • In the statement of the child process, the task code is obtained through the receiveTask function

int receiveTask(int readfd)
{
    
    
    int retcode=0;//返回任务码
    ssize_t s= read(readfd,&retcode,sizeof(retcode));//从读端读出来的任务码放到retcode里
    cout<<"process has read the TaskCode: "<<retcode<<endl;
    if(s==sizeof(int)) return retcode;
    else if(s<=0)return -1;
    else return 0;
}
  • Let the child process block the data in the read pipeline in the receiveTask function. The premise is that the parent process writes integer data to the anonymous pipe, and the data range is [0, number of tasks - 1], which is the subscript range corresponding to the task array. The child process stores the read data in the variable retcode, and then judges Whether the retcode is an integer data size, if yes, return the data to the upper layer CreateSubProcesses function, if not, return -1.
  • When the variable taskcode receives the task code returned by the receiveTask function, if the task code meets the range [0, number of tasks - 1], the parent-child process communicates normally according to our wishes, and then the child process takes the task code and calls the funcMap array to execute the task; However, if the received return value is -1, the communication between the parent and child processes is abnormal, and the judgment statement is directly exited.

The child process operation mentioned here is mainly that the child process blocks reading the data written by the parent process, and the child process gets the data to perform tasks.

Operate on the parent process

void loadBalanceContrl(const vector<SubEp>& subs,const vector<func_t> &funcMap,int comcode)
{
    
    
    int processnum=subs.size();//子进程的个数
    int tasknum=funcMap.size();//任务的个数
    bool numoftime=(comcode==0?true:false);//若命令码是0则一直运行,若命令码为正数x,则允许x次后退出
    while(true)
    {
    
    
   //rand()为伪随机数
   //1.找到哪一个子进程
   int subIndex=rand()%processnum;
    //2.找到哪一个执行哪一个任务
   int taskIndex=rand()%tasknum;
    //3.任务发送给选择的进程
     sendTask(subs[subIndex],taskIndex);//第一个参数传第几个子进程,第二个参数传第几个任务
     sleep(1);
  if(!numoftime)
  {
    
    
    comcode--;
    if(comcode==0)
    break;
  }
    }
    //走到这里则是父进程给子进程通信完了,需要逐个关闭子进程读端对于的写端--倒退关解决bug
    for(int i=0;i<subs.size();i++)
    {
    
    
      close(subs[i]._writefd);
      cout<<"close process: [ "<<i<<" ]'s writeeop"<<endl;
    }


}
  • The loadBalanceContrl function needs the main function to pass in the subprocess array subs, the task array funcMap and the command code comcode. Comcode is used to specify how many times the parent process sends data to the child process, that is, how many times the child process needs to perform tasks

  • numoftime is used to identify how many times the parent process needs to write data. When the comcode is 0, numoftime is true, and the parent process writes data to the anonymous pipe in an endless loop; if the command code is positive and x is not 0, numoftime is false , the parent process writes data x times to the anonymous pipe.

  • Let the parent process select the specified child process through the sendTask function, and write the specified task code to the anonymous pipe

void sendTask(const SubEp& process, int tasknum)
{
    
    
    cout<<"send Task num: "<<tasknum<<" to the process: "<<process._name<<endl;//打印日志:任务几发送给几号子进程
    ssize_t n=write(process._writefd,&tasknum,sizeof(tasknum));//该子进程读端对于的写端往管道里写入任务几-4个字节的数据
    assert(n==sizeof(int));//判断写入的数据是否是4个字节
    (void)n;
}
  • After the process communication between the parent and the child is completed, the write end of the parent process corresponding to the read end of the child process is closed in sequence according to the creation time of the child process.

recycle child process

void waitProcess(const vector<SubEp>& processes)
{
    
    
    for(size_t i=0;i<processes.size();i++)
    {
    
    
        waitpid(processes[i]._subid,nullptr,0);
        cout<<"wait success for process: "<<processes[i]._subid<<endl;
    }
}
  • According to the creation time of the child process, the child processes are recycled in order from first to last.

overall code

#include<iostream>
#include<vector>
#include<unistd.h>
#include<string.h>
#include<cassert>
#include<ctime>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define PROCESS_NUM 3
#define MakeSeed() srand((unsigned long)time(nullptr)^getpid()^rand()%1234)//建立伪随机数种子
//创建父进程给子进程派发的任务列表
typedef void(*func_t)();//函数指针类型,函数返回值为void

void downloadTask()//模拟下载任务
{
    
    
    cout<<getpid()<<": 下载任务\n"<<endl;
    sleep(1);
}

void fflushTask()//模拟刷新任务
{
    
    
    cout<<getpid()<<": 刷新任务\n"<<endl;
    sleep(1);
}

void subscribeTask()//模拟订阅任务
{
    
    
    cout<<getpid()<<": 订阅任务\n"<<endl;
    sleep(1);
}
//把上面的三种任务load到列表中即让存放函数指针的vector的各个指针能够指向上面的函数,为了后面方便调用
void loadTaskFunc(vector<func_t>*out)
{
    
    
    assert(out);//vector创建成功
    out->push_back(downloadTask);
    out->push_back(fflushTask);
    out->push_back(subscribeTask);

}

//创建子进程对象
class  SubEp//endpoint---子进程对象
{
    
    

public:
SubEp(pid_t subid,int writefd)//第一个参数是子进程的pid,第二个参数是该子进程读端对于父进程的写端fd
:_subid(subid)
,_writefd(writefd)
{
    
    
   char namebuffer[1024];
   //第一个参数是表示第几号子进程,第二个参数是子进程的pid,第三个参数是该子进程读端对于的父进程的写端
   snprintf(namebuffer,sizeof namebuffer,"process: %d [pid(%d) - fd(%d)]",num++,_subid,_writefd);
   _name=namebuffer;
}
public:
static int num;
string _name;
pid_t _subid;
int _writefd;//该子进程与父进程匿名管道对于的父进程的写端fd
};
int SubEp::num=0;

int receiveTask(int readfd)
{
    
    
    int retcode=0;//返回任务码
    ssize_t s= read(readfd,&retcode,sizeof(retcode));//从读端读出来的任务码放到retcode里
    cout<<"process has read the TaskCode: "<<retcode<<endl;
    if(s==sizeof(int)) return retcode;
    else if(s<=0)return -1;
    else return 0;
}

void CreateSubProcesses( vector<SubEp>*subs,vector<func_t>& funcMap)
{
    
    
    vector<int> deleteFd;
//创建子进程并且创建好父进程与各个子进程通信的管道
int fds[2];
for(size_t i=0;i<PROCESS_NUM;i++)//创建子进程
{
    
    
        int n=pipe(fds);//建立父子间进程的匿名管道--建立成功返回0,建立失败返回-1
        assert(n==0);//判断管道是否建立成功
        (void)n;

        pid_t id=fork();//创建子进程
        if(id==0)//子进程进入判断语句
        {
    
    
            for(size_t i=0;i<deleteFd.size();i++) close(deleteFd[i]);//因为有写实拷贝,所以这里关闭不会影响父进程
//因为子进程会继承父进程文件描述符表,所以上一个子进程的读端对应的父进程的写端这个进程也会继承下来,即当前子进程和上一个子进程之间也有匿名管道
//可能会导致上一个子进程的父进程读端关闭,而此时还有当前这个子进程的读端连接着上一个子进程,使得上一个子进程不能正常关闭读端而造成bug
//所以要手动关闭当前子进程对应上一个子进程的读端的写端。
            close(fds[1]);//关闭子进程的写端-保留读端负责读
           //对子进程操作
     while(true)
     {
    
    
        //1. 获取任务码,让子进程阻塞等待父进程写写入的任务码,
        int taskcode=receiveTask(fds[0]);
        //2.完成任务--调用对应任务码的函数
        if(taskcode>=0 && taskcode<funcMap.size())
         funcMap[taskcode]();//调用函数完成任务
        else if(taskcode==-1)  break;
     }
            exit(0);
        }
  //写端关闭,读端读到0然后读端自己关闭
        close(fds[0]);//关闭当前子进程与父进程相联系的匿名管道的父进程的读端
        SubEp sub(id,fds[1]);//第一个参数传的是子进程的pid,第二个参数传的是此时子进程读端对于的父进程的写端
        subs->push_back(sub);
       deleteFd.push_back(fds[1]);//记录当前的写端供下个子进程用

}
}
void sendTask(const SubEp& process, int tasknum)
{
    
    
    cout<<"send Task num: "<<tasknum<<" to the process: "<<process._name<<endl;//打印日志:任务几发送给几号子进程
    ssize_t n=write(process._writefd,&tasknum,sizeof(tasknum));//该子进程读端对于的写端往管道里写入任务几-4个字节的数据
    assert(n==sizeof(int));//判断写入的数据是否是4个字节
    (void)n;
}

void loadBalanceContrl(const vector<SubEp>& subs,const vector<func_t> &funcMap,int comcode)
{
    
    
    int processnum=subs.size();//子进程的个数
    int tasknum=funcMap.size();//任务的个数
    bool numoftime=(comcode==0?true:false);//若命令码是0则一直运行,若命令码为正数x,则允许x次后退出
    while(true)
    {
    
    
        //rand()为伪随机数
   //1.找到哪一个子进程
   int subIndex=rand()%processnum;
    //2.找到哪一个执行哪一个任务
   int taskIndex=rand()%tasknum;
    //3.任务发送给选择的进程
     sendTask(subs[subIndex],taskIndex);//第一个参数传第几个子进程,第二个参数传第几个任务
     sleep(1);
  if(!numoftime)
  {
    
    
    comcode--;
    if(comcode==0)
    break;
  }
    }
    //走到这里则是父进程给子进程通信完了,需要逐个关闭子进程读端对于的父进程写端
    for(int i=0;i<subs.size();i++)
    {
    
    
      close(subs[i]._writefd);
      cout<<"close process: [ "<<i<<" ]'s writeeop"<<endl;
    //    waitpid(subs[i]._subid,nullptr,0);
    //     cout<<"wait success for process: "<<subs[i]._subid<<endl;
    }
}

void waitProcess(const vector<SubEp>& processes)
{
    
    
    for(size_t i=0;i<processes.size();i++)
    {
    
    
        waitpid(processes[i]._subid,nullptr,0);
        cout<<"wait success for process: "<<processes[i]._subid<<endl;
    }
}

int main()
{
    
    
    MakeSeed();//建立伪随机数种子

vector<SubEp> subs;//创建子进程对象并将子进程对象放进数组里
vector<func_t> funcMap;//建立一个任务表:父进程写入管道,子进程在管道读取,读取到的数据引导子进程去完成一些任务
loadTaskFunc(&funcMap);

//1.创建子进程并且创建好父进程与各个子进程通信的管道,并且让子进程阻塞等待父进程写入
CreateSubProcesses(&subs,funcMap);

//2.对父进程操作

//父进程给子进程发送命令码,为0则一直运行,为正数x则运行x次后退出
int Runcount=0;
cout<<"请输入需要执行几次任务,输入0则为一直循环执行任务,请输入: ";
cin>>Runcount;
cout<<endl;
//这个函数负责让父进程给子进程发送命令码,让子进程去执行任务,要求子进程做到负载均衡     
loadBalanceContrl(subs,funcMap,Runcount);//第一个参数是子进程列表,第二个参数任务列表,第三个参数是父进程给子进程发送的命令码 

//3.回收子进程
waitProcess(subs);
    return 0;
}

subprocess has read end not closed bug

The following draws a picture to simulate the flow of the parent process forking out multiple child processes

image-20230522164129612

image-20230522164147020

image-20230522164456853

  • As can be seen from the above illustration, the No. 2 subprocess has a write terminal communicating with the read terminal of the No. 1 subprocess.

Explanation: Assume that the file descriptor fd[3] is the read end and fd[4] is the write end when the parent process creates the pipe. Then when creating the No. 1 child process, the child process copies the file descriptor table of the parent process, and then closes the read end fd[3] of the parent process, and closes the write end fd[4] of the child process, so that the parent process (write end fd [4]) and No. 1 child process (reader fd[3]) constitute the inter-process communication pipeline.

  1. When the parent process is creating the No. 2 child process, the No. 2 child process also copies a file description table of the parent process. At this time, the write end of fd[4] on the table is connected to the anonymous pipe of the No. 1 child process fd[3]. , then the No. 2 child process will also inherit. Because it has a realistic copy, the child process 2 opens the read end at fd[3] and does not affect the parent process. At this time, the parent process read end fd[3] is closed again, and the child process closes the write end fd[5], because the existence of the write end fd[4] of the No. 2 child process will not affect the communication with the parent process, so fd will not be closed [4].

  2. Then when closing the write end of the parent process later, the desired effect is that the read ends of the two child processes both read 0, and then the child process automatically closes the read end. However, the reality is that the parent process closes the write terminal, and the read terminal of the No. 2 child process only corresponds to one write terminal of the parent process, so the read terminal of the No. 2 child process will be closed. The read end of the child process No. 1 corresponds to the write end of the parent process and the write end of the child process No. 2. When the write end of the parent process is closed, the anonymous pipe is still connected to the write end of the child process No. 2, resulting in the write end of the child process No. 1 The read end will not read 0, so the read end of the No. 1 subprocess cannot be closed normally! , which leads to the failure of the child process to exit normally, and the failure of the parent process to recycle the child process normally

It is concluded that when the parent process creates multiple child processes, and the parent process is used as the write end and the multiple child processes are used as the read end for inter-process communication, when the parent process creates the next child process, the child process will inherit the file of the parent process The descriptor table will inherit the write end fd of the parent process corresponding to the read end of the previous child process, so the child process will have a write end to communicate with the read end of the previous child process. Therefore, when the parent process creates a child process, it needs to separately close all write ports of the child process.

Here are two ways to close all write ports of the child process

  1. Solution 1: Build a vector object before the parent process creates the child process. After the parent process creates the child process, put the write end of the parent process into the vector. When the parent process finishes creating the next child process, the write end in the vector is the write end of the previous child process corresponding to the read end of the current child process (it may not be just a write end), and then write all the write ends in the vector Just close it.

image-20230522202611512

image-20230522203254968

  • This solution is used in this implementation. In fact, it is not necessary, because when the parent process finishes writing data into the anonymous pipe, it first closes all the write terminals of the parent process corresponding to each child process, and then recycles all the child processes. There will be no bugs in this order; but if the write end of a parent process is turned off from the old to the new according to the creation time of the child process, and then immediately waits to recycle a corresponding child process, it will cause the child process to have a read end The writing end of other child processes communicates. If the reading end of the child process does not read 0, the child process does not exit normally, and the parent process cannot recycle the child process.
  1. Solution 2: When the parent process closes the write end and all child processes need to close the read end, close the write end of the parent process from new to old (from back to front) in sequence according to the creation time . Since the read end of the last created child process only corresponds to the write end of the parent process, when the parent process closes the write end, the read end of the last child process reads 0 and normally closes the read end, then the file descriptor table of the child process is also closed. will be closed, and then the child process will exit normally, so the write end of the child process connected to the previous child process will also be closed; then when it is the turn of the next child process, the read end of the child process will only correspond to the parent process For the write terminal, the parent process closes the write terminal, the child process reads 0 and closes the read terminal normally, and the child process exits normally.
    image-20230523145830675

Guess you like

Origin blog.csdn.net/m0_71841506/article/details/130815996