目录
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;
}