Linux基于匿名管道的进程池详解

1:匿名管道

参考博客链接:

蒋灵瑜的笔记本

2:基于匿名管道的进程池详解

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

#define MakeSeed() srand((unsigned long)time(nullptr)^getpid()^0x183211^rand()%2345)
#define PROCESS_NUM 10
typedef void (*func_t)();
void downloadTask()
{
    std::cout<<"下载任务开始"<<std::endl;
    sleep(2);
}
void printTask()
{
    std::cout<<"打印任务开始"<<std::endl;
    sleep(2);
}
void xiezuoTask()
{
    std::cout<<"协作任务开始"<<std::endl;
    sleep(2);
}
//用一个类维护子进程的相关信息
class subEp//EndPoint
{
    public:
    subEp(pid_t id,int writeFd)
    :subId_(id)
    ,writeFd_(writeFd)
    {
        char buffer[1024];
        snprintf(buffer,sizeof(buffer),"process-%d[pid:%d]-fd[%d]",num++,subId_,writeFd_);
        name_=buffer;
    }
    public:
    static int num;
    std::string name_;
    pid_t subId_;
    int writeFd_;
    
};
void loadTask(std::vector<func_t>* out)
{
    assert(out);
    out->push_back(xiezuoTask);
    out->push_back(printTask);
    out->push_back(downloadTask);


}
int recvTask(int readFd)
{
    int code = 0;
    ssize_t n = read(readFd,&code,sizeof(code));//表示从读端这个fd里面读数据,读到code中,大小为int大小,因为要读的是方法表的下标
    if(n==4)
    {
        return code;
    }
    else if(n<=0)//写端关闭 读端就会读到0,让他返回-1.如果读出错就是<0也让他返回-1
    {
        return -1;
    }
    else//这里不可能出现这个情况就让他返回0吧
    {
        return 0;
    }

}
void sendTask(const subEp& process,int taskNum)
{
    std::cout<<"send task num:"<< taskNum<<"to->"<<process.name_<<std::endl;
    int n  = write(process.writeFd_,&taskNum,sizeof(taskNum));//表示把tasknum的值写到写端里面,子进程就可以从读端去读
    assert(n==sizeof(int));
    (void)n;

}
void createSubProcess(std::vector<subEp>* subs,std::vector<func_t> funcmap)
{
    std::vector<int> deleteFd;
    for(int i = 0;i<PROCESS_NUM;i++)
    {
        //创建管道
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;//这一步是因为如果创建了n但是后续不使用n,编译器会warning没有使用这个变量,这里给他做一个void处理,表示后面不用了
        pid_t id = fork();
        if(id == 0)//是子进程,关闭写端fds[1]
        {
            for(int  i = 0;i<deleteFd.size();i++)close(deleteFd[i]);
            close(fds[1]);
            while(true)
            {
                //读取命令码从而执行对应的方法表里面的任务 如果没有发送 子进程应该阻塞
                int commandCode = recvTask(fds[0]);//fds的元素代表的文件fd的值
                //读到下标后直接执行函数指针容器里面的函数就行
                if(commandCode >=0 && commandCode<funcmap.size())
                funcmap[commandCode]();
                else if(commandCode == -1)break;//读到-1说明写端要么关了要么就是读出错了
            }
            exit(0);
        }
        //走到这里就是维护父子进程的通信信道
        close(fds[0]);//父进程关闭读端
        subEp sub(id,fds[1]);//把相关子进程的信息建立起来
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);
    }
}

void loadBalanceControl(const std::vector<subEp>& subs,const std::vector<func_t>& funcmap,int count)
{
    int processNum = subs.size();
    int taskNum = funcmap.size();
    bool forever = (count == 0?true:false);
    while(true)
    {
        //1:选择一个子进程
        int subIdx = rand()%processNum;
        //2:选择1个任务
        int taskIdx = rand()%taskNum;
        //3:任务发送给选择的进程
        sendTask(subs[subIdx],taskIdx);
        sleep(1);
        if(!forever)
        {
            count--;
            if(count == 0)break;
        }
    }
    //走到这里说明任务全发送完毕了,那就关闭写端,子进程就会读到0,这样就结束了,不写这一步子进程就会阻塞式等待
    for(int i = 0;i<processNum;i++)close(subs[i].writeFd_);
}
int subEp::num = 0;
int main()
{
    //创建随机数种子
    MakeSeed();
    //1:创建方发表
    std::vector<func_t> funcMap;//func_t是函数指针,直接用名字就是函数本体
    loadTask(&funcMap);//也可以传引用
    //2:创建子进程,并且维护好父子进程之间的通信信道
    std::vector<subEp> subs;
    createSubProcess(&subs,funcMap);//创建进程

    //3:父进程负载均衡的向子进程发送任务
    int taskCnt = 3;
    loadBalanceControl(subs,funcMap,taskCnt);
    //4:回收子进程
}

2.1:3个步骤

  • 1:创建方法表和创建随机数种子

  • 2:创建子进程,创建子进程与父进程之间的信道通信

  • 3:父进程发送任务给子进程

  • 4:父进程等待子进程的回收

2.2:创建方法表和随机数种子

#define MakeSeed() srand((unsigned long)time(nullptr)^getpid()^0x183211^rand()%2345)
#define PROCESS_NUM 10
typedef void (*func_t)();
  • 把void定义为一个函数指针,类型为func_t

  • 定义几个方法(函数)

void downloadTask()
{
    std::cout<<"下载任务开始"<<std::endl;
    sleep(2);
}
void printTask()
{
    std::cout<<"打印任务开始"<<std::endl;
    sleep(2);
}
void xiezuoTask()
{
    std::cout<<"协作任务开始"<<std::endl;
    sleep(2);
}
  • 创建方法容器vector,把这个几个方法添加进去

void loadTask(std::vector<func_t>* out)
{
    assert(out);
    out->push_back(xiezuoTask);
    out->push_back(printTask);
    out->push_back(downloadTask);
}

2.3:创建子进程,维护父子进程之间的信道

  • 子进程相关的信息用一个类来维护。

class subEp//EndPoint
{
    public:
    subEp(pid_t id,int writeFd)
    :subId_(id)
    ,writeFd_(writeFd)
    {
        char buffer[1024];
        snprintf(buffer,sizeof(buffer),"process-%d[pid:%d]-fd[%d]",num++,subId_,writeFd_);
        name_=buffer;
    }
    public:
    static int num;
    std::string name_;
    pid_t subId_;
    int writeFd_;
    
};
  • 为什么这里给的是写端不是读端呢,是因为后面第三步父进程要给子进程发送任务,发送的任务就是写到子进程的写端当中去,然后子进程从读端读。

  • name表示的子进程的num,subid和写端等属性的一个描述字符串。创建一个子进程,就会让num++,这样就可以让不同的进程用不同的num值(0-9),用于后面父进程向子进程发送任务的时候查看信息。(也就是调用子进程sub的name)

int recvTask(int readFd)
{
    int code = 0;
    ssize_t n = read(readFd,&code,sizeof(code));//表示从读端这个fd里面读数据,读到code中,大小为int大小,因为要读的是方法表的下标
    if(n==4)
    {
        return code;
    }
    else if(n<=0)//写端关闭 读端就会读到0,让他返回-1.如果读出错就是<0也让他返回-1
    {
        return -1;
    }
    else//这里不可能出现这个情况就让他返回0吧
    {
        return 0;
    }

}
void createSubProcess(std::vector<subEp>* subs,std::vector<func_t> funcmap)
{
    std::vector<int> deleteFd;
    for(int i = 0;i<PROCESS_NUM;i++)
    {
        //创建管道
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;//这一步是因为如果创建了n但是后续不使用n,编译器会warning没有使用这个变量,这里给他做一个void处理,表示后面不用了
        pid_t id = fork();
        if(id == 0)//是子进程,关闭写端fds[1]
        {
            for(int  i = 0;i<deleteFd.size();i++)close(deleteFd[i]);
            close(fds[1]);
            while(true)
            {
                //读取命令码从而执行对应的方法表里面的任务 如果没有发送 子进程应该阻塞
                int commandCode = recvTask(fds[0]);//fds的元素代表的文件fd的值
                //读到下标后直接执行函数指针容器里面的函数就行
                if(commandCode >=0 && commandCode<funcmap.size())
                funcmap[commandCode]();
                else if(commandCode == -1)break;//读到-1说明写端要么关了要么就是读出错了
            }
            exit(0);
        }
        //走到这里就是父进程
        close(fds[0]);//父进程关闭读端就可以形成单向信道
        subEp sub(id,fds[1]);//把相关子进程的信息建立起来
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);
    }
}

这里的deletefd我们最后再讲。

  • PROCESS_NUM是总共要创建的子进程个数。

  • 用循环,创建父子进程的管道,(void)n的用法在注释写上了

  • fork创建子进程,子进程关闭写端fds[1],然后执行true的循环,(为什么要执行true循环呢?因为可能后面父进程选择的子进程会是同一个,所以一个进程可能多次执行任务。)接受从读端读取到的命令码(为int类型,也就是方法表的下标),如果读到了就执行方法表里面对应下标的任务。如果读到的命令码为-1,说明要么读到了末尾要么就是读出错,这个时候可以终止循环。

  • 出了id==0的地方,就是父进程,父进程关闭读端,此时就形成了单向信道,然后再执行后面的均衡发送任务的函数。不过在这之前,先把每个子进程的相关信息创建好对象然后填入到subep这个容器中。

  • 当管道创立好了,就可以开始做进程间的通信了,怎么通信是放在fork函数的里面来的

  • 这段代码可以拆成两部分看,一部分是id==0以外的部分,这部分表示创建父子进程的匿名管道

  • 另外一部分是id==0的部分,这部分表示子进程与父进程在通信。所以在设计上是先设计外边的,再设计里面的。

  • 这里的子进程最后commandcode为-1就break是说,任务发送完成后,父进程会把子进程所有写端关闭,这样子进程的revtask内部的code就会读到0,这样commandcod就为-1就退出通信了

2.4:父进程负载均衡的给子进程发送任务

分为3步:

  • 父进程选择一个子进程

  • 父进程选择一个任务

  • 父进程向子进程的写端中发送任务

void sendTask(const subEp& process,int taskNum)
{
    std::cout<<"send task num:"<< taskNum<<"to->"<<process.name_<<std::endl;
    int n  = write(process.writeFd_,&taskNum,sizeof(taskNum));//表示把tasknum的值写到写端里面,子进程就可以从读端去读
    assert(n==sizeof(int));
    (void)n;

}
void loadBalanceControl(const std::vector<subEp>& subs,const std::vector<func_t>& funcmap,int count)
{
    int processNum = subs.size();
    int taskNum = funcmap.size();
    bool forever = (count == 0?true:false);
    while(true)
    {
        //1:选择一个子进程
        int subIdx = rand()%processNum;
        //2:选择1个任务
        int taskIdx = rand()%taskNum;
        //3:任务发送给选择的进程
        sendTask(subs[subIdx],taskIdx);
        sleep(1);
        if(!forever)
        {
            count--;
            if(count == 0)break;
        }
    }
    //走到这里说明任务全发送完毕了,那就关闭写端,子进程就会读到0,这样就结束了,不写这一步子进程就会阻塞式等待
    for(int i = 0;i<processNum;i++)close(subs[i].writeFd_);
}
  • count表示发送的任务个数

  • 用rand函数随机挑选子进程和任务代码。

  • 当任务全部发送完毕后,我们就需要把子进程所有写端关闭,这样读端就读到0了,也就是commandCode会读到-1,这样就会结束任务。与前面的break就对应上了。

2.3中的deleteFd用途

我们知道,在创建子进程的时候会继承父进程的内核相关的东西,

所以创建1个子进程就会继承父进程的文件描述符表(fd array[]),因为匿名管道是用fd作为读写端的,fd会被继承,也就是读写端会被继承。

当我们多次创建子进程的时候,后续的子进程会继承父进程给他的写端fd,这个写端fd就是上一个子进程的写端,这样一来,后续的子进程都会继承前面所有的子进程的写端fd。设想第一个子进程,就算父进程终止了写端,该子进程的读端也读不到0,因为后面的进程没有关闭继承过来的那个写端。

因此我们在创建一个进程与父进程的单向信道后,就把它的写端填充到delete中,在我们进行进程间通信的时候,就写一个循环,关闭掉delete里面所有的写端fd。

2.4:waitprocess

void waitprocess(std::vector<subEp> subs)
{
    int processnum = subs.size();
    for(int i = 0;i<processnum;i++)
    {
        waitpid(subs[i].subId_,nullptr,0);
        std::cout<<"wait process success"<<std::endl;
    }
}

成品如图。

2:命名管道

后续添加内容,可先参考博客

蒋灵瑜的笔记本

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不熬夜不抽烟不喝酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值