进程池的实现与匿名管道通信(task 2)
目录
前言
核心逻辑
先描述
任务执行
初始化
控制子进程
资源退出
设置任务
缺陷
编辑
前言
上文介绍了task1:什么是进程间的通信,什么是管道,如何利用匿名管道进行父子进程的通信。
本文将实现进程池的实现与匿名管道的通信。
核心逻辑
int main()
{LoadTask(&tasks); srand(time(nullptr)^getpid()^1023); // 种一个随机数种子//1.获得子进程与管道vector<channel> channels;InitProPool(&channels);//2.创建任务,写入数据CtrlSlaver(channels);//3.资源清理QuitProcess(channels);return 0;
}
核心代码为:
1.创建子进程与管道
2.创建任务写入数据
3.资源清理
先描述
对于父子间通信的管道,需要用一个结构去描述管道的属性,以便后续通信
//描述管道属性
class channel
{
public:channel(int cmdfd, pid_t slaverid, string processname):_cmdfd(cmdfd),_slaverid(slaverid),_processname(processname){}public:int _cmdfd; // 发送任务的文件描述符(后续write发送任务的时候只需要channel对象即可,read已经设置了从0号文件读)pid_t _slaverid; // 子进程IDstring _processname; // 子进程名称
}; //结构与类后面需要加分好:;
任务执行
void slaver()
{while (true){int cmdcode = 0;int ret = read(0, &cmdcode, sizeof(cmdcode)); //ssize_t read(int fd, void *buf, size_t count);if (ret == 4){std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " << cmdcode << std::endl;if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode](); //tasks内部数据是函数指针}else if (ret == 0)break; //跳出循环}
}
初始化
// 输入:const &
// 输出:*
// 输入输出:&void InitProPool(vector<channel>* channels)
{// version 2: 确保每一个子进程都只有一个写端std::vector<int> oldfds;for (int i = 0; i < tasks_num; i++){//创建管道int pipefd[2] = {0};int ret = pipe(pipefd);if (ret < 0) return;//创建子进程pid_t id = fork();/*当父进程调用fork()时,以下步骤会发生:操作系统创建一个新的进程,这个新进程称为子进程。子进程获得父进程的代码段、数据段、堆、栈等资源的副本(注意是副本,但是这些资源在物理内存中是共享的,直到任一进程尝试写入时发生写时复制)。fork()在父进程中返回子进程的进程ID,在子进程中返回0。也就是说子进程会获得副本中的数据,同时执行fork下面的if(ret == 0)内部的代码。*/if (id == 0) //子进程{std::cout << "child: " << getpid() << " close history fd: ";for(auto fd : oldfds) { //oldfds存放的是写端,子文件需要关闭!所有的!写端std::cout << fd << " ";close(fd);}std::cout << "\n";close(pipefd[1]);dup2(pipefd[0], 0); // 0:标准输入close(pipefd[0]);slaver();std::cout << "process : " << getpid() << " quit" << std::endl;exit(0);}//父进程oldfds.push_back(pipefd[1]); //父进程开辟出写端之后,将写端push_backclose(pipefd[0]);std::string name = "process-" + std::to_string(i);channels->push_back(channel(pipefd[1], id, name)); //匿名对象传参 //一定要在父进程去初始化channel对象,因为channel对象需要子进程的pidsleep(1); }
}
本阶段着重完成:1.创建管道 2.创建子进程 3.子进程调用读接口,在read处阻塞等待4.子进程创建结束之后初始化管道
控制子进程
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 >= 5) break;int cmdcode = select - 1;//选择进程:1.轮询 2.随机 // int processpos = rand()%channels.size();std::cout << "father say: " << " cmdcode: " <<cmdcode << " already sendto " << channels[which]._slaverid << " process name: " << channels[which]._processname<< std::endl;// 3. 发送任务write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode)); //一旦写入之后,被阻塞的子进程尝试启动 //每一个子进程都一个写端的wfdwhich++;which %= channels.size();}
}
核心逻辑:1.选择任务 2.选择子进程 3.发送任务
资源退出
void QuitProcess(const std::vector<channel> &channels)
{for(const auto &c : channels){ //close与wait一起进行。但是在子进程中,需要close(oldWfds)close(c._cmdfd);waitpid(c._slaverid, nullptr, 0);}// version1 // int last = channels.size()-1;// for(int i = last; i >= 0; i--) //倒着进行// {// close(channels[i]._cmdfd);// waitpid(channels[i]._slaverid, nullptr, 0);// }// for(const auto &c : channels) close(c._cmdfd); //先close再wait// // sleep(5);// for(const auto &c : channels) waitpid(c._slaverid, nullptr, 0);// // sleep(5);
}
设置任务
using namespace std;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);
}
定义一个函数指针类型。用vector存储函数,用下表就可以访问函数。
缺陷
后续fork的子进程也会继承父亲的多个w接口,因此我们需要将子进程的w端彻底关闭。
资源清理时的解决方案:关闭写端接口,读端读取到0,退出。
1.先关闭接口再清理。
2.倒着清理
最后一个子进程只被一个写端接口(来自父进程)指向,关闭之后,读端就没有了写端对应,可以实现退出。
如果正向进行,当来自父进程的写端关闭之后,还有后续其他子进程的写端指向该进程,因此无法关闭该进程。其他子进程的文件描述符表继承自父进程,因此还有其他子进程的写端指入该子进程的管道。
这个管道可以理解为文件缓冲区。
3.在Init阶段,就将父进程不断打开的写端fd记录到数组中
在子进程中,将写端fd彻底关闭。