Linux进程间通信——池化技术与模拟实现进程池

池化技术

池化技术其实是分治思想的一个体现,在生活中也有很多例子

例如,河水的水资源非常丰富,但是要多次从河水中取水使用是比较繁琐且低效的,尤其是当家离河流较远的时候(消耗较大),这时候我们就可以每次取一大缸水,需要使用的时候直接取水使用就可以了

进程池也是类似的思想,因为每一次fork创建子进程都需要进行系统调用,也需要消耗一定的资源,因此我们可以一次申请一定数量的进程PCB,分别分配对应的任务给他们就可以了

这实际上就是一种多进程并发运行的思想,我们可以用父进程来进行子进程的管理与任务分配、任务验收,然后让这些兄弟进程来运行不同的任务,这其中传递信息就需要用到我们前一篇所学的匿名管道

但是在这里我们先不具体分配,只是体会其中的思想即可

进程池

进程池原理

画成示意图就是这样的

蓝色是每一次我们fork子进程和pipe管道之后,需要关闭的接口,因为每一次的读接口都是3,所以从4、5、6以后的分别对应的就是不同的子进程的写管道请添加图片描述

这里其实还有一个隐藏起来的bug,就是在第二个子进程以及之后的子进程fork的时候,实际上他拷贝的是父进程的文件描述符表,因此他也存在一个接口指向之前申请的写接口,只有最后一个子进程是只有一个写接口的

请添加图片描述

这会造成什么影响呢,我们说过,当一个管道的写口被关闭了之后,读口再读的返回值就是0,这样就标志管道使用结束了,但是如果我们想通过父进程从从上至下主动关闭写口并等待子进程退出,就会导致阻塞(死循环)

这是因为虽然父进程的写口关了,但是子进程2和3的写口仍然开的,从而导致管道1不关闭,子进程1就一直等待管道关闭

这里就有两个解决的思路

第一个思路是由下至上关闭退出子进程

第一个方法是,因为最后一个子进程只有一个写口,关闭之后,管道也可以正常关闭,进程也就可以正常退出了

第二个方法是,我一口气把所有的写口全部关了,等子进程自己退出,这样做是没有问题的,因为本质上还是第一个方法,因为只有关到最后一个写口的时候,子进程才开始从下到上依次退出

第二个思路是

产生这个bug的原因本质上是因为写口是直接被拷贝过来的,因此只需要在创建初始化子进程的时候,就把之前父进程拷贝来的其他子进程写口全部关掉就可以了

我们采取第二个思路,因为第一个思路只能一口气全部关闭,或者关闭一个进程及其以后创建出来的进程,没有办法做到想关哪个关哪个,简直太不优雅了

模拟实现进程池

在实现进程池之前,我们遇到的第一个问题就是,怎么管理这些进程

在之前我们学习的过程中,只有一个子进程,我们可以在父进程的视角,通过进程id是否为0分出来子进程和父进程

但是在进程池的视角来看,所有子进程的pid都是0,也就无从下手管理,了吗?

创建与初始化

我们讲过所有管理的操作思想就是,先描述后组织,想要描述这些进程,我们可以用类和结构体,用到面向对象的思想进行管理即可

请看实现

#include<iostream>
#include<string>
#include<vector>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include"Task.hpp"

const int process_num = 5;
static int number = 1; // 表示进程数、信道数

class channel
{
public:
    channel(int fd, pid_t id)
        : CtrlFD(fd)
        , WorkerID(id)
        {
            name = "Channel " + std::to_string(number++);
        }
public:
    int CtrlFD; // 写口id
    pid_t WorkerID; // 子进程id
    std::string name; // 信道名称
};

void Work()
{


}

void PrintFd(const std::vector<int>& fds)
{
    std::cout<<getpid()<<"关闭的写口有:";
    for(auto fd : fds)
    {
        std::cout<<fd<<" ";
    }
      std::cout<<std::endl;
}

void CreateChannels(std::vector<channel>* channels)
{
    std::vector<int> old_channels;
    for(int i=0; i<process_num; i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        assert(n==0);
        (void)n;

        pid_t id = fork();
        assert(id!=-1);

        if(id==0)
        {
            // 子进程创建成功
            
            // 关闭之前子进程的写口
            if(!old_channels.empty())
            {
               for(auto fd : old_channels)
               {
                  close(fd);
               }
               // 打印已经关闭的写口提示信息
               PrintFd(old_channels);
            }

            close(pipefd[1]); // 关闭自己的写口
            dup2(pipefd[0],0); // 将自己的读口设为默认读口
            Work(); // 做自己的任务
            exit(0); // 任务结束退出
        }

        // 父进程操作
        close(pipefd[0]); // 关闭父进程的读
        channels->push_back(channel(pipefd[1], id)); // 存下子进程的信息,写口和子进程id
        old_channels.push_back(pipefd[1]);
    }

}

int main()
{
    std::vector<channel> channels;
    CreateChannels(&channels);
    return 0;
}

  • 我们把进程用写口id、进程id、名字表述组成,称之为信道,这样可以控制写入(输入任务),关闭(waitpid)
  • 用create函数来控制信道的创建,这个函数由父进程执行,主要任务是创建规定数目的子进程,并且进行初始化,让子进程进Work函数准备进行工作
  • 初始化主要让子进程删除从父进程拷贝过来的写口,自己管道的写口也需要关闭
  • 父进程需要关闭管道的读口,将此次创建的进程口记录下来
  • 这里很巧妙的运用了子进程对父进程的拷贝,因为old_channel是在父进程声明的,所以每一个新创建的子进程都会复制一份,而不会拥有之后任意一个子进程的写口

分配任务

接下来要做的工作就是让子进程运行并且获取父进程分配给他的任务

我们可以设置很多种类的任务,分别设置对应的任务号,然后子进程可以获取对应的任务号,再使用自己的资源进行运行

#pragma once

#include<iostream>
#include<functional>
#include<vector>
#include<ctime>
#include<unistd.h>

using task_t = std::function<void()>; // 包装器,可以理解为函数指针类型

void Task1()
{
    std::cout<<"这是任务1,交给pid:为"<<getpid()<<"运行"<<std::endl;
}
void Task2()
{
    std::cout<<"这是任务2,交给pid:为"<<getpid()<<"运行"<<std::endl;
}
void Task3()
{
    std::cout<<"这是任务3,交给pid:为"<<getpid()<<"运行"<<std::endl;
}
void Task4()
{
    std::cout<<"这是任务4,交给pid:为"<<getpid()<<"运行"<<std::endl;
}

class Init
{
public:
    Init()
    {
        tasks.push_back(Task1);
        tasks.push_back(Task2);
        tasks.push_back(Task3);
        tasks.push_back(Task4);

        srand(time(nullptr)^getpid()); // 产生随机种子
    }

    int SelectTask()
    {
        // 此处应为任务分配算法
        return rand() % tasks.size();
    }

    std::string TaskName(int code)
    {
        switch (code)
        {
        case Task1_Code:
            return "Task1";
            break;
        case Task2_Code:
            return "Task2";
            break;
        case Task3_Code:
            return "Task3";
            break;
        case Task4_Code:
            return "Task4";
            break;
        default:
            return "Unknow";
            break;
        }
    }

    bool CheckSafe(int code)
    {
        // 确保子进程只能执行我们交付的任务
        if(code>=0&&code<tasks.size())
            return true;
        else
            return false;
    }

    void RunTask(int code)
    {
        return tasks[code](); // 调用函数指针
    }
public:
    const static int Task1_Code = 0;
    const static int Task2_Code = 1;
    const static int Task3_Code = 2;
    const static int Task4_Code = 3;
    // 可以使用枚举对象

    std::vector<task_t> tasks; // 可以理解为函数指针数组
};

Init init; // 创建一个初始化对象,避免重复使用匿名对象
#include<iostream>
#include<string>
#include<vector>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include"Task.hpp"

const int process_num = 5;
static int number = 1; // 表示进程数、信道数

class channel
{
public:
    channel(int fd, pid_t id)
        : CtrlFD(fd)
        , WorkerID(id)
        {
            name = "Channel " + std::to_string(number++);
        }
public:
    int CtrlFD; // 写口id
    pid_t WorkerID; // 子进程id
    std::string name; // 信道名称
};

void Work()
{
    // 子进程工作函数
    while(true)
    {
        int code = 0;
        // 准备读取父进程信息
        ssize_t n = read(0,&code,sizeof(code));
        if(n==sizeof(code))
        {
            // 正确读取
            if(!init.CheckSafe(code))
            {
                std::cout<<"子进程未收到正确任务"<<std::endl;
                continue;
            }    
            init.RunTask(code); // 执行任务
        }
        else if(n==0)
        {
            // 父进程写口关闭,子进程退出
            break;
        }
    }
    std::cout<<"子进程退出"<<std::endl;

}

void PrintFd(const std::vector<int>& fds)
{
    std::cout<<getpid()<<"关闭的写口有:";
    for(auto fd : fds)
    {
        std::cout<<fd<<" ";
    }
      std::cout<<std::endl;
}

void CreateChannels(std::vector<channel>* channels)
{
    std::vector<int> old_channels;
    for(int i=0; i<process_num; i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        assert(n==0);
        (void)n;

        pid_t id = fork();
        assert(id!=-1);

        if(id==0)
        {
            // 子进程创建成功
            
            // 关闭之前子进程的写口
            if(!old_channels.empty())
            {
               for(auto fd : old_channels)
               {
                  close(fd);
               }
               // 打印已经关闭的写口提示信息
               PrintFd(old_channels);
            }

            close(pipefd[1]); // 关闭自己的写口
            dup2(pipefd[0],0); // 将自己的读口设为默认读口
            Work(); // 做子进程的任务
            exit(0); // 任务结束退出
        }

        // 父进程操作
        close(pipefd[0]); // 关闭父进程的读
        channels->push_back(channel(pipefd[1], id)); // 存下子进程的信息,写口和子进程id
        old_channels.push_back(pipefd[1]);
    }

}

void SendTaskNum(const std::vector<channel> &c, bool flag, int num = -1)
{
    int pos = 0;
    while(true)
    {
        // 选择任务
        int TaskNum = init.SelectTask();

        // 选择进程
        const auto & channel = c[pos++];
        pos%=c.size();
        
        // 输出控制信息
        std::cout<<"指定的任务是"<<init.TaskName(TaskNum)<<"任务编号是"<<TaskNum<<"交付的子进程是"<<channel.WorkerID<<std::endl;

        // 发送任务,写入管道
        write(channel.CtrlFD, &TaskNum, sizeof(TaskNum));

        // 判断是否循环执行
        if(!flag)
        {
            num--;
            if(num<=0)
                break;
        }
        sleep(1);
    }
}

int main()
{
    std::vector<channel> channels;
    CreateChannels(&channels);

    const bool g_always_loop = true;
    SendTaskNum(channels, !g_always_loop, 10);
    return 0;
}

这里我们从主函数看起

在创建完信道之后,我们设置了一个标志,用于表示这个任务是否循环执行

在SendTaskNum函数中,后两个参数表示是否循环执行和循环执行的次数

其次我们写了一个hpp文件,用来表示任务类,将所有任务放到包装器functions中,当作函数指针数组,方便任务类完成集中调用

父进程这里的选取任务算法是随机的,轮流分配给五个信道,让五个信道执行10轮分配的任务

子进程的work是一直循环接收父进程传递的消息,若有效接收,则判断是否为任务列表中的任务,然后执行,若父进程写口关闭,则跳出读取循环,关闭子进程

关闭子进程

void ReleaseChannels(std::vector<channel> c)
{
    for(const auto &channel : c)
    {
        close(channel.CtrlFD);
        waitpid(channel.WorkerID, nullptr, 0);
    }
    // 输出提示信息
    std::cout<<"子进程释放完毕"<<std::endl;
}

最后的关闭子进程比较简单,因为我们在创建的过程中已经做好了bug排除,可以从头到尾依次释放

效果

请添加图片描述

进程池完整代码

// Task.hpp
#pragma once

#include<iostream>
#include<functional>
#include<vector>
#include<ctime>
#include<unistd.h>

using task_t = std::function<void()>; // 包装器,可以理解为函数指针类型

void Task1()
{
    std::cout<<"这是任务1,交给pid:为"<<getpid()<<"运行"<<std::endl;
}
void Task2()
{
    std::cout<<"这是任务2,交给pid:为"<<getpid()<<"运行"<<std::endl;
}
void Task3()
{
    std::cout<<"这是任务3,交给pid:为"<<getpid()<<"运行"<<std::endl;
}
void Task4()
{
    std::cout<<"这是任务4,交给pid:为"<<getpid()<<"运行"<<std::endl;
}

class Init
{
public:
    Init()
    {
        tasks.push_back(Task1);
        tasks.push_back(Task2);
        tasks.push_back(Task3);
        tasks.push_back(Task4);

        srand(time(nullptr)^getpid()); // 产生随机种子
    }

    int SelectTask()
    {
        // 此处应为任务分配算法
        return rand() % tasks.size();
    }

    std::string TaskName(int code)
    {
        switch (code)
        {
        case Task1_Code:
            return "Task1";
            break;
        case Task2_Code:
            return "Task2";
            break;
        case Task3_Code:
            return "Task3";
            break;
        case Task4_Code:
            return "Task4";
            break;
        default:
            return "Unknow";
            break;
        }
    }

    bool CheckSafe(int code)
    {
        // 确保子进程只能执行我们交付的任务
        if(code>=0&&code<tasks.size())
            return true;
        else
            return false;
    }

    void RunTask(int code)
    {
        return tasks[code](); // 调用函数指针
    }
public:
    const static int Task1_Code = 0;
    const static int Task2_Code = 1;
    const static int Task3_Code = 2;
    const static int Task4_Code = 3;
    // 可以使用枚举对象

    std::vector<task_t> tasks; // 可以理解为函数指针数组
};

Init init; // 创建一个初始化对象,避免重复使用匿名对象

// ProcessPool.cc
#include<iostream>
#include<string>
#include<vector>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include"Task.hpp"

const int process_num = 5;
static int number = 1; // 表示进程数、信道数

class channel
{
public:
    channel(int fd, pid_t id)
        : CtrlFD(fd)
        , WorkerID(id)
        {
            name = "Channel " + std::to_string(number++);
        }
public:
    int CtrlFD; // 写口id
    pid_t WorkerID; // 子进程id
    std::string name; // 信道名称
};

void Work()
{
    // 子进程工作函数
    while(true)
    {
        int code = 0;
        // 准备读取父进程信息
        ssize_t n = read(0,&code,sizeof(code));
        if(n==sizeof(code))
        {
            // 正确读取
            if(!init.CheckSafe(code))
            {
                std::cout<<"子进程未收到正确任务"<<std::endl;
                continue;
            }    
            init.RunTask(code); // 执行任务
        }
        else if(n==0)
        {
            // 父进程写口关闭,子进程退出
            break;
        }
    }
    std::cout<<"子进程退出"<<std::endl;

}

void PrintFd(const std::vector<int>& fds)
{
    std::cout<<getpid()<<"关闭的写口有:";
    for(auto fd : fds)
    {
        std::cout<<fd<<" ";
    }
      std::cout<<std::endl;
}

void CreateChannels(std::vector<channel>* channels)
{
    std::vector<int> old_channels;
    for(int i=0; i<process_num; i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        assert(n==0);
        (void)n;

        pid_t id = fork();
        assert(id!=-1);

        if(id==0)
        {
            // 子进程创建成功
            
            // 关闭之前子进程的写口
            if(!old_channels.empty())
            {
               for(auto fd : old_channels)
               {
                  close(fd);
               }
               // 打印已经关闭的写口提示信息
               PrintFd(old_channels);
            }

            close(pipefd[1]); // 关闭自己的写口
            dup2(pipefd[0],0); // 将自己的读口设为默认读口
            Work(); // 做子进程的任务
            exit(0); // 任务结束退出
        }

        // 父进程操作
        close(pipefd[0]); // 关闭父进程的读
        channels->push_back(channel(pipefd[1], id)); // 存下子进程的信息,写口和子进程id
        old_channels.push_back(pipefd[1]);
    }

}

void SendTaskNum(const std::vector<channel> &c, bool flag, int num = -1)
{
    int pos = 0;
    while(true)
    {
        // 选择任务
        int TaskNum = init.SelectTask();

        // 选择进程
        const auto & channel = c[pos++];
        pos%=c.size();
        
        // 输出控制信息
        std::cout<<"指定的任务是"<<init.TaskName(TaskNum)<<"任务编号是"<<TaskNum<<"交付的子进程是"<<channel.WorkerID<<std::endl;

        // 发送任务,写入管道
        write(channel.CtrlFD, &TaskNum, sizeof(TaskNum));

        // 判断是否循环执行
        if(!flag)
        {
            num--;
            if(num<=0)
                break;
        }
        sleep(1);
    }
}

void ReleaseChannels(std::vector<channel> c)
{
    for(const auto &channel : c)
    {
        close(channel.CtrlFD);
        waitpid(channel.WorkerID, nullptr, 0);
    }
    // 输出提示信息
    std::cout<<"子进程释放完毕"<<std::endl;
}

int main()
{
    std::vector<channel> channels;
    CreateChannels(&channels);

    const bool g_always_loop = true;
    SendTaskNum(channels, !g_always_loop, 10);

    ReleaseChannels(channels);
    return 0;
}
  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

栖林_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值