我们要模拟的是什么呢?
模拟父进程控制子进程执行任务
我们该怎么去控制呢,我们控制这些子进程本质是去管理这些子进程,那么就离不开
先描述再组织,也就是我们把每个子进程的内核数据信息交给父进程管理,父进程调用
操作系统的接口去管理子进程。描述的话就是把每个子进程的信息描述起来,管理就是
父进程用操作系统提供的接口去控制子进程执行。
我们先把框架搭起来
1.1创建管道
1.2创建子进程
子进程(1.3.0关闭自身写端,1.3.1 输入重定向,1.3.2子进程开始等待获取父进程发送的命令)
父进程(1.3.3关闭父进程的读端,1.4存放子进程信息)
父进程(发命令控制子进程)
父进程(回收子进程)
总结就是三步
1.创建子进程并收集子进程信息
2.控制子进程
3.回收子进程
下面我们跟着代码去探索本质
#include <iostream> #include <cassert> #include <cstdio> #include <cstring> #include <ctime> #include <cstdlib> #include <string> #include <vector> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <fcntl.h> #include <cerrno> #include "Task.hpp" #include <stdio.h> Task T; const int gnum = 5; class EndPoint { private: static int num; public: pid_t _child_id; int _write_fd; std::string processname; public: EndPoint(int id,int fd):_child_id(id),_write_fd(fd) { char namebuffer[64]; snprintf(namebuffer,sizeof(namebuffer),"porcess-%d[%d-%d]",num++,_child_id,_write_fd); processname = namebuffer;//运算符重载= } ~EndPoint() {} }; int EndPoint::num = 0; //子进程接受到命令,执行任务 void WaitCommand() { while(1) { int command; int n = read(0,&command,sizeof(int));//以字节流读取,我们要的是command,整形,读取也是 //读 整形的4个字节 std::cout<<"recv cmd:"<<command<<std::endl; if(n == 0) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; break; } else if(n == sizeof(int)) { T.Execute(command); } else { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; break; } } } void createProcesses(std::vector<EndPoint>* end_point) { std::vector<int> fds; for(int i = 0;i < gnum;i++) { //1.1创建管道 int pipefd[2] = {0}; int n = pipe(pipefd); assert(n == 0); (void)n; //1.2创建进程 pid_t id = fork(); assert(id != -1); if(id == 0) { for(auto &fd:fds) close(fd); //这个点很重要,子进程关闭从父进程继承下来的写端 //关闭自身写端 close(pipefd[1]); //我们期望子进程在读取指令的时候 ,从标准输入读取 //1.3.1 输入重定向 //dup2 dup2(pipefd[0],0); //1.3.2子进程开始等待获取父进程发送的命令 WaitCommand(); std::cout<<"子进程等待命令终于结束了,阻塞的好累"<<std::endl; //子进程退出,关闭读端 close(pipefd[0]); exit(0); } //父进程写入 close(pipefd[0]);//1.3关闭父进程的读端 //1.4 end_point->push_back(EndPoint(id,pipefd[1])); fds.push_back(pipefd[1]); } } void show_board() { std::cout<<"#################"<<"0:执行日志任务"<<" ##################"<<std::endl; std::cout<<"#################"<<"1:执行数据库任务"<<"##################"<<std::endl; std::cout<<"#################"<<"2:执行网络请求任务"<<"################"<<std::endl; std::cout<<"#################"<<"3:执行通行任务"<<" ##################"<<std::endl; std::cout<<"#################"<<">3:退出"<<" ##################"<<std::endl; } //负载均衡算法 void CtrlProcess(const std::vector<EndPoint>& end_point) { //父进程进行写入命令 int cnt = 0;//我们变成轮询式的 show_board(); while(1) { //1.选择任务 int command = 0; std::cin>>command; if(command > 3) break; //2.选择进程 cnt %= end_point.size(); //3.下发任务 std::cout<<end_point[cnt].processname<<std::endl; write(end_point[cnt]._write_fd,&command,sizeof(command)); cnt++; sleep(1);//我们写入之后,子进程读到命令,然后执行相应命令的任务,由于我们的执行速度很快,我们的父进程在执行向显示打印的时候 //子进程也在打印,此时我们父子进程并发式的向子进程打印,那么就会去抢占数据,可能会数据改错,我们让父进程等一等,也就是让父进程执行的 //操作系统知道不需要 } } void WaitChild(const std::vector<EndPoint>& end_point) { //子进程写端关闭,父进程读端关闭,要想子进程退出,因为子进程的读端 //是受父进程的写端影响的,所以父进程写端关闭,读端读完自然就会退出该进程 //还有要回收 for(const auto &end: end_point) close(end._write_fd); //回收所有子进程 int cnt = 0; for(const auto &end: end_point) { pid_t ret = waitpid(end._child_id,nullptr,0); if(ret > 0) { std::cout<<"等待成功,id是:"<<ret<<std::endl; cnt++; } } //这段代码其实也有不足,只是碰巧把最后一个给关闭,因为倒着关闭刚好可以把所有写端关闭 //所以不会堵塞,但是这种做法不合适——取巧,所以采用第二种做法,每次在创建 //新的子进程的时候, // int cnt = 0; // for(const auto &end: end_point) // { // close(end._write_fd); // pid_t ret = waitpid(end._child_id,nullptr,0); // if(ret > 0) // { // std::cout<<"等待成功,id是:"<<ret<<std::endl; // cnt++; // } // } if(cnt == end_point.size()) std::cout<<"父进程回收了所有子进程"<<std::endl; else std::cout<<"内存泄漏"<<std::endl; } int main() { //1.先进行创建控制结构,父进程写,子进程读 std::vector<EndPoint> end_point; createProcesses(&end_point);//子进程阻塞在那等待读命令,等待读命令的时候,这个函数的代码被父进程执行把进程id写入到EndPoint对象中 //然后此时该子进程是堵塞在那等待命令,父进程进行执行循环体,以下同理 //同理循环完后,其实是有五个子进程在那从管道中读,因为管道里没有数据,所以堵塞在那,等父进程向管道写入命令 //然后父进程执行下面的父进程的写入命令的代码,我们每写一个命令,就是向对应管道写入数据,然后对应管道的子进程读入,执行任务 //打个比方,就是说,我们此时父进程所控制的管道内是空的,是空的,所以对于管道的子进程就是堵塞(在读命令,因为管道内没数据) //此时我们父进程发送一个任务码给管道,让对应子进程读到了,那么该子进程执行该任务,此时还是堵塞的,(为什么呢,是因为我们的写端还没关闭,我们的读端就默认一直在等待命名,一直堵塞在那) //所以执行不了std::cout<<"子进程等待命令终于结束了,阻塞的好累"<<std::endl; //2.我们写成自动化的,也可以搞成交互式的 CtrlProcess(end_point); WaitChild(end_point); return 0; }
现象1
其实我们创建完所以子进程的时候,我们的子进程一直处在堵塞状态(等待接受命令)
只有当我们回收之后所有子进程后,所有子进程就会立马执行这个命令
回收后
现象2
理解所有子进程会继承父进程写端的时候再来看下面!!!
怎么回收?
关闭所有写端!!!
方法1:倒着回收
方法2:
代码中实现的就是方法2,看注释
那为什么读端是一样的?
"写端不同,读端相同"是一个和匿名管道有关的表述,主要是指当使用匿名管道时,父进程和多个子进程之间的通信方式。
在这种情况下,父进程会往管道中写入数据,而多个子进程则可以从同一个管道中读取数据进行处理。因此,从子进程的角度来看,读取数据的管道文件描述符都是相同的。
但对于父进程来说,它需要向多个不同的管道中写入数据,以便控制多个不同的子进程并发执行任务。所以从父进程的角度来看,写入数据的管道文件描述符是不同的。
因此,"写端不同,读端相同"的表述中,是从两种进程的角度来描述匿名管道的特性。在这种通信方式下,需要注意管道的缓存限制,防止写入数据超过缓存大小而导致阻塞,同时在完成任务后要关闭掉管道文件描述符。