我们要模拟的是什么呢?
模拟父进程控制子进程执行任务
我们该怎么去控制呢,我们控制这些子进程本质是去管理这些子进程,那么就离不开
先描述再组织,也就是我们把每个子进程的内核数据信息交给父进程管理,父进程调用
操作系统的接口去管理子进程。描述的话就是把每个子进程的信息描述起来,管理就是
父进程用操作系统提供的接口去控制子进程执行。
我们先把框架搭起来
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,看注释
那为什么读端是一样的?
"写端不同,读端相同"是一个和匿名管道有关的表述,主要是指当使用匿名管道时,父进程和多个子进程之间的通信方式。
在这种情况下,父进程会往管道中写入数据,而多个子进程则可以从同一个管道中读取数据进行处理。因此,从子进程的角度来看,读取数据的管道文件描述符都是相同的。
但对于父进程来说,它需要向多个不同的管道中写入数据,以便控制多个不同的子进程并发执行任务。所以从父进程的角度来看,写入数据的管道文件描述符是不同的。
因此,"写端不同,读端相同"的表述中,是从两种进程的角度来描述匿名管道的特性。在这种通信方式下,需要注意管道的缓存限制,防止写入数据超过缓存大小而导致阻塞,同时在完成任务后要关闭掉管道文件描述符。