原理
匿名管道
可用于父子进程间的通讯,于是可以有父进程创建多个子进程形成进程池
,并通过匿名管道
文件向各个子进程派发任务
戳我去管道原理🔗
这里使用C++编写进程池代码,在程序中创建多个进程,并在父进程中使用自定义类channel
用于描述和管理子进程,然后在task.hpp
中模拟一些任务给主程序随机派发
代码
代码规范
这次对形参有了新的规范,这里用variable
代指形参名
- 输入:
const &variable
- 输出:
*variable
- 输入输出:
&variable
准备makefile文件
这里使用g++
编译,规定语法标准为C++11
1 2 3 4 5 6
| processPool:processPool.cc g++ -o $@ $^ -std=c++11
.PHONY:clean clean: rm -rf processPool
|
准备任务文件
首先要准备前后需要用到的头文件,然后构建模拟任务,并提供加载任务列表的函数
task.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #pragma once
#include <vector> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <cassert> #include <iostream> #include <string>
typedef void (*task_t)();
void task1() { std::cout << "PVZ 刷新日志" << std::endl; } void task2() { std::cout<< "PVZ 生成阳光"<<std::endl; } void task3() { std::cout<<"PVZ 检测更新" <<std::endl; } void task4() { std::cout<<"PVZ 使用能量豆"<<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
,我们按类和接口从上往下编程
描述和组织
我们把头文件准备好,然后定义最大进程数和创建任务列表,接着创建channel
类来描述每个进程池中的子进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include "task.hpp" #include <ctime> #include <sys/stat.h> #include <sys/wait.h>
const int processNum = 5; std::vector<task_t> tasks;
class channel { public: channel(int cmdfd,pid_t slaverid,std::string& name) :_cmdfd(cmdfd),_slaverid(slaverid),_processname(name) {}
public: int _cmdfd; pid_t _slaverid; std::string _processname; };
|
slaver函数
创建子进程后,子进程都进入slaver
函数等待获取任务
这里统一采用输入重定向
,在创建子进程后,进入slaver()
前先把标准输入重定向到管道文件,然后从标准输入读取任务码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void slaver() { while(true) { int cmdcode = 0; int n = read(0,&cmdcode,sizeof(int)); if(n == sizeof(int)) { if(cmdcode >=0 && cmdcode < tasks.size()) { tasks[cmdcode](); } else { std::cout<<"wrong cmdcode: "<<cmdcode <<" max size: "<<tasks.size()<<std::endl; } } } }
|
InnitChannels
该函数用鱼创建子进程,创建子进程,并封装进频道类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void InitChannels(std::vector<channel>* channels) { for(int i = 0;i<processNum;i++) { int pipefd[2]; int n = pipe(pipefd); assert(!n); (void)n;
pid_t id = fork(); if(id == 0) { close(pipefd[1]); dup2(pipefd[0],0); slaver(); std::cout<<"proccess "<< getpid() << " quit"<<std::endl; exit(0); } close(pipefd[0]); std::string name = "process-"+std::to_string(i); channels->push_back(channel(pipefd[1],id,name)); } }
|
channelDEBUG
这里封装一个用于测试创建频道的DEBUG
函数
1 2 3 4 5 6 7
| void Debug(const std::vector<channel> channels) { for(auto &c:channels) { std::cout<<c._cmdfd << " " << c._slaverid<< " " << c._processname << std::endl; } }
|
ctrlSlaver
这里封装一个ctrlSlaver
用于控制子进程,也就是用于派发任务的函数
1 2 3 4 5 6 7 8 9 10 11 12
| void ctrlSlaver(const std::vector<channel> &channels) { for(int i = 0;i<10;i++) { int cmdcode = rand() % tasks.size(); int select_num = rand()%channels.size(); std::cout<<"father say# "<<"taskcode: "<<cmdcode<<" send to "<<channels[select_num]._processname << std::endl; write(channels[select_num]._cmdfd,&cmdcode,sizeof(cmdcode)); sleep(1); } }
|
QuitProcess
最后当然要有父进程控制关闭子进程,并回收僵尸进程
因为InitChannels
中管道文件的处理比较粗糙,会发生如下图的关系,所以关闭时两步操作一定要放在两个循环中
1 2 3 4 5 6
| void QuitProcess(const std::vector<channel>& channels) { for(const auto &c:channels)close(c._cmdfd); for(const auto &c:channels)waitpid(c._slaverid,nullptr,0); }
|
main函数
经过上文一系列封装,main
函数就可以简洁明了地描述子进程的运行过程了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int main() { srand(time(nullptr) ^ getpid()^1023); std::vector<channel> channels; LoadTask(&tasks); InitChannels(&channels);
ctrlSlaver(channels);
QuitProcess(channels);
return 0; }
|
源文件
戳我去github仓库🔗