【Linux系统编程】:进程池(简易版)

目录

1.制作游戏菜单

 2.对管道进行描述和组织

3.初始化管道

3.1子进程执行任务slaver()

3.2检查管道是否创建有误 

4.父进程向管道写入(控制子进程执行任务)

5.清理资源

修改初始化管道代码

6.完整代码:


1.制作游戏菜单

我们利用管道的原理,创建一个简易的进程池,其中父进程向管道写入“任务码”,子进程从管道中读取 “任务码”,并根据任务码执行对应的任务。

我们先模拟一个简易的游戏任务菜单,

void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

根据菜单中的功能,我们简略模拟对应的函数 :

typedef void (*task_t)();
void task1()
{
    std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{
    std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{
    std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{
    std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}

 我们需要将这些函数组织起来,利用下标去管理,

void LoadTask(std::vector<task_t> *tasks)
{
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

当我们调用菜单中的某个功能, 只要定义一个

std::vector<task_t> tasks;//任务列表

然后加载任务(调用LoadTask()),就可根据下标使用不同的功能,比如要想刷新野区野怪(任务2),只需要调用tasks[1]既可。任务标号和vector下标的映射关系:任务标号 - 1 = vector下标。

 2.对管道进行描述和组织

 假设我们要创建十个管道,每个管道对应的子进程的pid要保存:

//先描述:
class channel
{
public:
    channel(int cmdfd,pid_t slaverid,std::string &processname)
    :_cmdfd(cmdfd),_slaverid(slaverid),_processname(processname)
    {}
public:
    int _cmdfd;                 //发送任务的文件描述符
    pid_t _slaverid;            //子进程的pid
    std::string _processname;   //子进程的名字——方便打印日志
};

然后定义一个vector将这些管道管理起来,

//再组织
std::vector<channel> channels;

3.初始化管道

我们要创建十个子进程,并初始化十个管道,其中要保存子进程的pid(父进程后续要回收),子进程读取管道的文件描述符(通过读取管道接收父进程写入管道的任务码),以及给子进程编号(起名)并保存其编号(方便管理)。

const int processnum = 10;//进程数
void InitProcessPool(std::vector<channel>* channels)
{
    for(int i = 0;i < processnum;i++)
    {
        //1.1 创建管道
        int pipefd[2];
        int n = pipe(pipefd);//创建成功返回0
        assert(!n);//如果n不是0管道就创建失败
        (void)n;
        //1.2 创建子进程并构建单向通信渠道
        pid_t id = fork();
        if(id == 0)//child
        {
            close(pipefd[1]);
            //slaver(pipefd[0]);//一般用dup2取代这种写法
            dup2(pipefd[0],0);//用标准输入替换读端 
            close(pipefd[0]);
            slaver();//这样,以后读取父进程的“任务码”,只需要从标准输入中读
            std::cout << "process:" << getpid << "quit" <<std::endl;
            exit(0);
        }
        //father
        close(pipefd[0]);
        //构建channel字段(名字)
        std::string name = "process--" + std::to_string(i);
        channels->push_back(channel(pipefd[1],id,name));
    }
}

3.1子进程执行任务slaver()

其中slaver()表示子进程要执行的具体的任务,当子进程调用到slaver时,如果父进程没有发送“任务码”,子进程会进入阻塞状态等待父进程写入。

void slaver()
{
    //read(0)
    int cnt = 5;
    while(true)
    {
        int cmdcode = 0;
        int n = read(0,&cmdcode,sizeof(int));
        if(n == sizeof(cmdcode))
        {
            //执行cmdcode对应的任务
            std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: "
            << cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size())
            {
                tasks[cmdcode]();//函数指针的解引用符号可以省略
            }
        }
        if(n == 0) break;
    }
}

3.2检查管道是否创建有误 

创建管道后,我们还可以将所有管道的信息打印出来,间接检查一下管道是否创建有误,

void debug(const std::vector<channel> &channels)
{
    for(const auto c:channels)
    {
        std::cout << "cmdfd:" << c._cmdfd << " slaverid:" << c._slaverid 
        << " processname:" << c._processname << std::endl;
    }
}

4.父进程向管道写入(控制子进程执行任务)

 我们可以通过随机安排进程执行任务,通过输入指定安排进程执行任务等方式,控制子进程执行任务。

void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "please enter@";
        std::cin >> select;
        if(select <= 0 || select > 4) break;
        //2.1 选择任务
        //int cmdcode = rand()%tasks.size();//用随机数模拟"选择"的过程
        int cmdcode = select - 1;
        //2.2 选择进程
        //int processpos = rand()%channels.size();
        //2.3 发送任务
        std::cout << "father say: " << " cmdcode is " <<cmdcode << ",already sendto " 
        << channels[which]._slaverid << ",process name: " << channels[which]._processname << std::endl;
        write(channels[which]._cmdfd,&cmdcode,sizeof(cmdcode));

        which++;
        which %= channels.size();
    }
}

5.清理资源

我们创建了10个进程,就要回收这些进程。对于管道,我们只要关闭写端,读端的进程就会退出。

 
void QuitProcess(const vector<channel> &channels)
{
    for (const auto e : channels) 
    {
        close(e._cmdfd);  //写端
        waitpid(e._slaverid, nullptr, 0);
    }
}

如果我们按照这样写,程序运行时会报错。因为我们在初始化管道时,理想中创建的管道如下图1:

子进程和附近之间的读写端是一一对应的,但实际上,子进程会继承父进程的写段,所以创建的子进程越多,最后创建的子进程继承的读端(也就是父进程向管道中“读”对应的文件描述符就会越多)就会越多,就会造成子进程的读端对应多个写端的情况,真实情况如上图2。

所以我们要对上面初始化管道的代码进行一个修改:

修改初始化管道代码

void InitProcessPool(std::vector<channel>* channels)
{
    //确保每一个子进程都只有一个写端:将父进程的写端记录下来,在子进程中关闭即可
    std::vector<int> oldfd;//记录父进程的写端
    for(int i = 0;i < processnum;i++)
    {
        //1.1 创建管道
        int pipefd[2];
        int n = pipe(pipefd);//创建成功返回0
        assert(!n);//如果n不是0管道就创建失败
        (void)n;
        //1.2 创建子进程并构建单向通信渠道
        pid_t id = fork();
        if(id == 0)//child
        {
            for(auto fd:oldfd) close(fd);//关闭之前父进程的写端
            close(pipefd[1]);
            //slaver(pipefd[0]);//一般用dup2取代这种写法
            dup2(pipefd[0],0);//用标准输入替换读端 
            close(pipefd[0]);
            slaver();//这样,以后读取父进程的“任务码”,只需要从标准输入中读
            std::cout << "process:" << getpid << "quit" <<std::endl;
            exit(0);
        }
        //father
        close(pipefd[0]);
        //构建channel字段(名字)
        std::string name = "process--" + std::to_string(i);
        channels->push_back(channel(pipefd[1],id,name));
        oldfd.push_back(pipefd[1]);
    }
}

这样就没有问题了。

6.完整代码:

Task.hpp:

#pragma once
#include <iostream>
#include <functional>
#include <vector>
typedef void (*task_t)();

void task1()
{
    std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{
    std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{
    std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{
    std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}

void LoadTask(std::vector<task_t> *tasks)
{
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

ProcessPool.cc 

#include "Task.hpp" //后缀hpp表示头文件和源文件中的内容不分开写,比如函数的声明和定义放在同一个文件内
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
const int processnum = 10;//进程数
std::vector<task_t> tasks;//任务列表

//先描述:
class channel
{
public:
    channel(int cmdfd,pid_t slaverid,std::string &processname)
    :_cmdfd(cmdfd),_slaverid(slaverid),_processname(processname)
    {}
public:
    int _cmdfd;                 //发送任务的文件描述符
    pid_t _slaverid;            //子进程的pid
    std::string _processname;   //子进程的名字——方便打印日志
};
void slaver()
{
    //read(0)
    int cnt = 5;
    while(true)
    {
        int cmdcode = 0;
        int n = read(0,&cmdcode,sizeof(int));
        if(n == sizeof(cmdcode))
        {
            //执行cmdcode对应的任务
            std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: "
            << cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size())
            {
                tasks[cmdcode]();//函数指针的解引用符号可以省略
            }
        }
        if(n == 0) break;
    }
}
//输入:const &
//输出: *
//输入输出:&
void InitProcessPool(std::vector<channel>* channels)
{
    //确保每一个子进程都只有一个写端:将父进程的写端记录下来,在子进程中关闭即可
    std::vector<int> oldfds;//记录父进程的写端
    for(int i = 0;i < processnum;i++)
    {
        //1.1 创建管道
        int pipefd[2];
        int n = pipe(pipefd);//创建成功返回0
        assert(!n);//如果n不是0管道就创建失败
        (void)n;
        //1.2 创建子进程并构建单向通信渠道
        pid_t id = fork();
        if(id == 0)//child
        {
            std::cout << "child: " << getpid() << " close history fd: ";
            for(auto fd : oldfds) {
                std::cout << fd << " ";
                close(fd);
            }
            std::cout << "\n";
            close(pipefd[1]);
            //slaver(pipefd[0]);//一般用dup2取代这种写法
            dup2(pipefd[0],0);//用标准输入替换读端 
            close(pipefd[0]);
            slaver();//这样,以后读取父进程的“任务码”,只需要从标准输入中读
            std::cout << "process-id:" << getpid() << " quit" <<std::endl;
            exit(0);
        }
        //father
        close(pipefd[0]);
        //构建channel字段(名字)
        std::string name = "process--" + std::to_string(i);
        channels->push_back(channel(pipefd[1],id,name));
        oldfds.push_back(pipefd[1]);
        sleep(1);
    }
}
void debug(const std::vector<channel> &channels)
{
    for(const auto c:channels)
    {
        std::cout << "cmdfd:" << c._cmdfd << " slaverid:" << c._slaverid 
        << " processname:" << c._processname << std::endl;
    }
}
void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}
void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "please enter@";
        std::cin >> select;
        if(select <= 0 || select > 4) break;
        //2.1 选择任务
        //int cmdcode = rand()%tasks.size();//用随机数模拟"选择"的过程
        int cmdcode = select - 1;
        //2.2 选择进程
        //int processpos = rand()%channels.size();
        //2.3 发送任务
        std::cout << "father say: " << " cmdcode is " <<cmdcode << ",already sendto " 
        << channels[which]._slaverid << ",process name: " << channels[which]._processname << std::endl;
        write(channels[which]._cmdfd,&cmdcode,sizeof(cmdcode));

        which++;
        which %= channels.size();
    }
}
void QuitProcess(const std::vector<channel> &channels)
{
    // //关闭父进程所有的写端,子进程就会退出进入僵尸状态
    // for(auto &c : channels) close(c._cmdfd);
    // //sleep(5);
    // //回收所有子进程
    // for(auto &c : channels) waitpid(c._slaverid,nullptr,0);
    // //sleep(5);
    for (const auto e : channels) 
    {
        close(e._cmdfd);  //写端
        waitpid(e._slaverid, nullptr, 0);
    }
}
int main()
{
    LoadTask(&tasks);
    srand(time(nullptr)^getpid()^1023); // 种一个随机数种子
    //再组织
    std::vector<channel> channels;
    //1.初始化
    InitProcessPool(&channels);
    //test
    debug(channels);
    //2.开始控制子进程
    ctrlSlaver(channels);
    
    //3.清理
    QuitProcess(channels);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值