上海低价网站建设,carwling wordpress,返利系统网站开发,怎么做自己的网站推广#x1f525; 本文专栏#xff1a;Linux Linux实践项目 #x1f338;作者主页#xff1a;努力努力再努力wz #x1f4aa; 今日博客励志语录#xff1a; 人生没有标准答案#xff0c;你的错题本也能写成传奇。 ★★★ 本文前置知识#xff1a;
匿名管道 1.前置知识回顾… 本文专栏Linux Linux实践项目 作者主页努力努力再努力wz 今日博客励志语录 人生没有标准答案你的错题本也能写成传奇。 ★★★ 本文前置知识
匿名管道 1.前置知识回顾对此十分熟悉的读者可以跳过
那么在上一篇博客中我们知道了进程之间具有通信的需求因为进程之间需要合作协同完成某项任务那么就需要各个进程之间进行分工合作那么进程就需要知道对方完成的进度以及完成的结果所以进程之间需要通信但是进程无法直接访问对方的数据因为进程之间具有独立性所以为了达到进程的通信的需求又保证进程之间的独立性那么操作系统采取的策略就是在内存中创建一块公共区域那么一个进程向这个公共区域中写入另一个进程从该公共区域读取就能完成进程的通信
而对于父子进程或者说有血缘关系的进程那么我们知道创建子进程的过程会拷贝父进程的task_struct结构体并且修改其中的部分属性得到子进程自己独立的一份task_struct结构体那么其中就会涉及到文件描述表的拷贝那么意味着子进程会继承父进程打开的文件而进程之间通信的核心思想便是创建一个公共区域而由于子进程和父进程会共享被打开的文件那么意味着文件就可以作为这个公共区域所以父子进程通信的方式就是通过文件所以在创建子进程之前那么父进程会先创建一份用来通信的文件而该文件不需要刷新写入到磁盘当中因为该文件的内容只是临时用来保存父子之间写入的内容不需要刷新到磁盘长时间来保存所以需要创建一份内存级别也就是不需要刷新到磁盘的文件那么其中就要调用pipe接口那么它会创建两个分别以只读权限打开以及只写权限打开同一个管道文件的file结构体对象并返回这两个结构体的文件描述符然后再调用fork接口创建子进程那么子进程会继承父进程创建的两个以不同权限打开的file结构体对象而该文件只能单向通信也就是只能一个进程往该文件中写入另一个进程从该文件中读取不能双方同时写入不然会造成内容混乱而正是由于一个进程只能往该文件写另一个文件只能从该文件读那么这个特点和我们生活中的自来水管道是十分相似的因为自来水管道只能从一端流入然后从一端流出所以该文件又称之为管道文件那么为了实现单向通信就需要父子进程关闭各自其中的一个读写端
那么这就是对上文的内容大致回顾如果你对此感到陌生那么可以去看我上一期文章
进程池项目介绍
1.进程池的意义
那么这里我们用之前所学的内容来实现一个进程池其中就包括匿名管道那么首先在讲进程池具体实现之前那么我们得知道进程池是用来干什么的它有什么用也就是做这个进程池有什么意义那么想必这些问题是读者对于进程池首先的一个疑惑所以这里我们就先来认识做进程池的意义
上文前置知识回顾的开篇我就说道过进程之间需要共同来完成某项任务那么此时进程就需要分工合作来完成各自分配的任务那么假设有这么一个场景那么你现在有100个task要完成然后你把这些task都准备交给子进程来完成那么此时你是如何去分配这些任务给子进程呢
那么有的小伙伴采取的是这种方式也就是他先调用fork接口然后创建一个子进程然后给该子进程分配一个任务接着父进程则是等待子进程退出通过退出码来查看子进程完成的情况如果子进程正常退出并且结果正确那么接着它便继续调用fork接口重复上面的步骤也就是循环创建子进程然后给其分配任务让其执行而对于父进程则是等待其子进程退出获取其子进程的退出码而现在有100个任务那么意味着这个小伙伴要调用100次fork接口
而还有的小伙伴采取的是另一种方式那么他不是创建出一个子进程然后就直接给创建出的该子进程分配任务去完成他则是先创建出一批的进程比如20个进程那么此时创建完这20个进程之后那么此后他不会再调用fork接口去创建其他新的子进程而就是利用手头上持有的这20个进程来完成这100个task那么就需要父进程依次给这20个子进程分配各自的任务然后分配完之后等待这20个进程退出获取其退出码看进程是否正常退出然后再依次给执行完任务结束的子进程继续分配新的任务
那么我们就来比较并且评价一下上面的这两个小伙伴各自的实现方式首先明确的是这两个小伙伴的实现方式肯定都是正确没有问题的也就是说上面的这两种方式都能够成功的完成这100个task但是这两种方式完成的效率就会有所差别那么第二个小伙伴的实现的方式的效率要比第一个小伙伴的实现方式的效率要高很多那么为什么呢
那么首先我们一定要记住并且理解的一个道理那就是系统接口的调用是具有代价的虽然你在代码中for循环连续100次调用fork接口创建了一批子进程然后一运行你的代码发现程序还是正常运行并且结果正确但是你要知道的是fork系统调用接口底层所涉及到的工作其中就包括会拷贝父进程的task_struct结构体然后修改其中的部分属性得到子进程自己独立的一份task_struct结构体然后创建完子进程的task_struct结构体之后还涉及到写时拷贝以及页表的重新映射并且操作系统还要将创建出来的子进程的task_struct结构体放到相应的队列中来维护管理比如放到就绪队列中那么当子进程运行结束之后那么又会涉及到子进程的task_struct结构体等各种资源的释放那么从子进程的创建以及销毁所涉及到的工作就可以看出来那么调用一个fork接口其实是有成本的就如同以前你看到初中班上学习成绩十分优秀的同学那么他上课的时候总是趴在桌子上睡觉结果人家考试还次次考全班第一你看着人家学习很轻松但其实人家在你看不到的地方其实在偷偷努力比如晚上学习到凌晨几点
所以对于第一种实现方式那么它的缺点就是十分的明显那么要多次调用系统接口那么效率必然不会优秀而第二种方式相比于第一种方式那么它则是先创建一批子进程俗话说磨刀不误砍柴功那么这里我们先创建一批进程但是不让其执行特定的任务然后创建完之后那么我们就只需要让这创建出的进程轮流去执行这100个任务
那么对于第一种方式那么假设要交给子进程完成100个task那么意味着要调用100次fork接口然后这100个task就分别交给每一个fork创建出来的进程最终完成这100个task而对于第二种方式假设我们预先创建20个进程然后让这20个进程轮流执行完成这100个task任务那么我们来对比一下这两种方式的效率
那么对于第一种那么假设完成一个task的代价是k那么调用fork接口的代价是m那么第一种实现的方式的总代价就是100m100K,而对于第二种方式来说那么它预先创建了20个进程来执行这100个task那么对于第二种方式的总代价就是20m100k,所以粗略估计下来那么第二种方式明显比第一种更加优秀
而第二种方式正是我们进程池的实现的核心思想那么为什么称其为进程池我们就可以用和尚下山去取水的故事例子来理解那么有一个和尚住在一个高山山顶上的一座寺庙那么它如果要喝水或者洗澡只能到山脚下的小溪中去取水然后再将水运回山顶那么一旦和尚口渴了或者想洗澡那么意味着他就要跑到山脚下去取水那么这样做明显代价就太大了并且十分的不划算那么为了提升效率减少上山下山的时间的浪费那么和尚采取的做法就是在半山腰上建立一个蓄水池先存储一大部分水那么一旦有用水的需求就到这个池子中去即可而不需要跑到山脚下去运水
所以我们为什么叫起进程“池”那么这个池字就很形象那么我们预先创建一批进程的这个过程和上文那个例子中建立一个蓄水池是一个道理那么我们就不需要在去调用fork来去创建一个进程直接从创建好的进程池中选取子进程去完成任务即可那么这就是进程池的意义目的就是为了提高效率减少系统调用的开销
2.进程池的大体框架
那么知道了进程池的意义之后那么我们再来说一下进程池的实现那么首先我们脑海中得先有一个大体的实现框架以及思路也就是说我们得先分析出进程池涉及到的各个模块然后再来谈这各个模块具体的代码的实现
1.进程池的创建
那么根据上文我们知道那么我们在执行任务之前首先得创建一批子进程那么假设要创建的子进程的数量是n那么意味着我们会涉及到一个循环其中在循环内部调用n次fork接口来创建n个子进程那么其次我们子进程是来完成某项任务的那么这个任务的发送就得交给父进程由父进程来分配给子进程要执行的任务那么这个任务可以通过一个任务码来传递也就是一个int类型的变量那么既然父进程要给子进程发送任务码那么必然就要涉及到进程之间的通信而父子进程如何通信我们也很熟悉了那么便是通过匿名管道进行通信所以这里就注意在调用fork之前那么我们得先调用pipe接口所以刚才说的这一系列内容比如管道以及子进程的创建我们都可以把它封装到一个函数模块中具体的实现细节下文会提到
2.任务列表的制作
那么我们知道子进程到时候是会通过管道读取父进程交给它的任务码那么任务码的本质其实就是一个编号因为到时候我们所有要执行的函数都会有一个函数指针指向它那么最终会定义一个全局的函数指针数组那么所谓的任务码就是对应着这个函数指针数组的一个下标那么由于定义成了全局的指针数组那么到时候fork创建子进程那么子进程也能访问到这个函数指针数组那么就可以读取管道中的任务码然后根据函数指针数组来执行相应的函数那么我们要执行的各个任务的逻辑都是封装在函数当中而我们函数指针数组就可以理解为任务列表到时候我们就要完成函数指针数组的初始化那么这个初始化工作就会交给一个函数来完成
3.子进程执行任务父进程传递任务
而我们知道我们会通过fork接口来创建子进程然后利用fork的返回值使得父子进程有着不同的执行流那么我们知道在创建子进程之前会首先创建管道文件那么接着调用fork创建子进程那么意味着子进程会继承并且会和父进程共享者打开的管道文件所以到时候在子进程的执行流中就需要关闭管道文件的写端关闭完之后下一步便是读取管道文件传来的任务码获取到任务码然后执行任务那么这就是子进程执行任务的大致思路至于具体的细节我们下文在进行补充
而父进程对应的代码段则是想管道文件中写入子进程要执行的任务码
4.资源的清理
那么资源的清理便是进程池的最后阶段了那么这个阶段的工作就是父进程会关闭之前打开的管道文件并且等待子进程退出看子进程是否正常退出那么具体的实现细节下文会说到
进程池的各个模块的具体实现
1.进程池的创建
那么这里我们进程池的创建专门放到process_init模块当中,那么其中会涉及到一个for循环的逻辑然后在循环调用pipe接口然后创建管道文件得到管道文件的读写端的文件描述那么接着再调用fork接口创建子进程然后利用fork的返回值让父子进程有着各自的执行流那么在子进程的执行流中那么它会调用close接口来关闭管道文件的写端而对于父进程则是关闭管道文件的读端
那么到时候父进程得要向管道文件中写入任务码那么意味着父进程的知道管道文件的文件描述符因为到时候向管道文件写入需要调用write接口而write接口会接收一个文件描述符作为参数向该文件描述符所指向的文件中写入一定字节数并且我们还得知道该管道文件相连接的是哪个子进程所以我们得记录子进程的PID那么我们可以定义一个channel类然后内部封装了两个成员变量分别是管道文件的文件描述符以及其连接的子进程的PID那么父进程在关闭玩对应的管道文件的读端之后还要初始化channel对象将其插入到一个vector数组中那么vector数组中就维护了创建出来的各个管道的属性
std::vectorchannel channelarray;
class channel
{public:int _processid;int _write_fd;channel(int processid,int write_fd):_processid(processid),_write_fd(write_fd){}
};而对于子进程来说那么它关闭玩管道的写端之后接着的任务就是去获取父进程在管道文件中写入的任务码以及执行任务那么这个内容我们可以封装到一个start_mission函数模块中那么我下文会详细解析这个函数 其次这里有一个小细节那么到时候子进程要去管道文件读取任务码那么这里我进行了一个重定向也就是将子进程的管道文件重定向到标准输入文件那么这里就会调用dup2接口那么其会关闭标准输入文件将标准输入文件的下标的指针指向管道文件这样做的好处就是我们子进程在读取管道文件的输入的时候不需要知道管道文件的文件描述符统一的去标准输入的文件描述符中读取即可 dup2(pipefd[0],0);close(pipefd[0]);void processpool_init()
{for(int i0;iprocessnum;i){int pipefd[2];int npipe(pipefd);if(n0){perror(pipe fail);exit(EXIT_FAILURE);}int idfork();if(id0){perror(fork);close(pipefd[0]);close(pipefd[1]);exit(EXIT_FAILURE);}if(id0){close(pipefd[1]);dup2(pipefd[0],0);close(pipefd[0]);start_mission();exit(0);}close(pipefd[0]);channelarray.push_back(channel(id,pipefd[1]));}
}2.任务列表的制作
那么任务列表的制作就非常轻松那么到时会我们会定义一个全局的函数指针数组那么其中函数指针数组的每一个元素是一个函数指针指向一个函数那么我们会将这个数组中的每一个元素给初始化指向对应的函数那么这个函数就是子进程要执行的任务那么函数指针数组的下标就是任务码那么刚才所说的这些工作都交给mission_load来完成
#define missionnum 4
typedef void (*mission)() ;
std::vectormission missionarray;
void task1()
{std::coutI am childprocess: getpid() running task1std::endl;
}
void task2()
{std::coutI am childprocess: getpid() running task2std::endl;
}
void task3()
{std::coutI am childprocess: getpid() running task3std::endl;
}
void task4()
{std::coutI am childprocess: getpid() running task4std::endl;
}
void mission_load()
{missionarray.push_back(task1);missionarray.push_back(task2);missionarray.push_back(task3);missionarray.push_back(task4);
}3.子进程执行任务父进程传递任务
那么子进程执行任务我们专门设置了一个start_mission函数模块来实现那么其中在start_mission模块中就会涉及到一个死循环因为子进程不可能执行完一个任务就退出了因为它还要继续被父进程分配执行下一个任务就和之前实现shell外壳程序一样那么整体的大框架也是一个死循环那么你获取以及执行完用户输入的一个指令之后你的bash进程不可能就退出结束了吧同理这里你子进程在获取父进程向管道文件中写入的任务码以及执行对应的函数之后那么就循环继续读取下一次父进程向管道文件中的写入的任务码所以涉及到一个死循环的逻辑
那么读取任务码就涉及到调用read接口那么从上文可知我们已经将管道文件重定向到标准输入文件那么这里我们就从标准输入文件中读取任务码由于函数指针数组是全局变量那么获取到任务码之后直接根据函数指针数组执行相应的函数即可而注意还要判断read的返回值如果read返回0说明了此时管道文件的写端已经被关闭那么父进程已经关闭了该管道文件的写端所以子进程没必要在进行读取所以直接break然后子进程退出
void start_mission()
{while(true){int staues;int nread(0,staues,sizeof(int));if(nsizeof(int)){if(staues0stauesmissionnum){std::cout我是子进程getpid() 成功获取到任务码stauesstd::endl;missionarray[staues]();}}else if(n0){break;}else if(n0){perror(read);exit(EXIT_FAILURE);}}
}而父进程要做的则是传递任务我们同样也是定义一个process_control函数来实现那么其中就要注意的就是负载均衡所谓的负载均衡指的就是我们给创建出来的所有子进程分配任务的时候希望让所有子进程都尽可能的分配执行到任务也就是大家有事干都别闲着和操作系统调度进程是一个道理那么做到负载均衡的方式有两种第一种就是随机分配那么由于之前我们用数组记录了每一个管道文件对应的channel对象其中channel对象保存了子进程的编号那么假设有n个管道那么我们可以产生一个0到n-1的随机数然后调用对应的子进程由于产生0到n-1这每一个数的概率肯定是相等所以可以做到负载均衡
其次第二种方式则是轮询那么所谓的轮询就更加直观就是我们先分配给任务按照子进程被创建的顺序依次分配从第一个依次分配到最后一个最后再回到第一个那么其中就会涉及到取模运算
void process_control()
{srand((unsigned int)time(NULL));int which0;for(int i0;i100;i){int cmdrand()%missionnum;int nwrite(channelarray[which]._write_fd,cmd,sizeof(int));if(n0){perror(write);exit(EXIT_FAILURE);}std::coutfather process send a message tochannelarray[which]._processid cmd :cmdstd::endl;which(which1)%processnum; }
}4.资源的清理
那么最后的资源清理任务则放到process_clean函数模块那么这个模块就是关闭回收管道以及等待子进程那么这里要注意的一点就是我们每创建一个子进程那么该子进程会继承之前创建出的所有管道文件这会让管道文件的引用计数加一那么子进程以及父进程会关闭各自的读写端会让其引用计数减一那么对于最后一个管道文件来说那么它只被最后一个创建的子进程以及父进程所共享那么由于子进程与父进程再关闭各自的读写端那么最后一个管道文件的读写端的引用计数是1那么以此往前类推那么前面的管道文件的读写端就是从2开始递增所以我们关闭管道文件得从最后一个管道文件往前关闭不然你从前往后关闭的话那么管道的引用计数不会为0那么会导致资源泄漏并且子进程一直陷入阻塞状态因为管道的写端未被关闭并且父进程一直没有写入
void process_clean()
{for(int il1.size()-1;i0;i--){close(channelarray[i]._write_fd);int statues;int nwaitpid(channelarray[i]._processid,statues,0);if(n0){perror(waitpid);}else{std::cout子进程channelarray[i]._processid等待成功std::endl;}}
}完整实现
processpool.cpp
#includeprocesspool.hpp
int main()
{mission_load();processpool_init();process_control();process_clean();return 0;
}processpool.hpp
includeiostream
#includeunistd.h
#includevector
#includesys/wait.h
#includesys/types.h
#includecstdlib
#includetime.h
#includetask.hpp
#define EXIT_FAILURE 1
#define missionnum 4
const int processnum10;
std::vectorchannel channelarray;
class channel
{public:int _processid;int _write_fd;channel(int processid,int write_fd):_processid(processid),_write_fd(write_fd){}
};
void mission_load()
{missionarray.push_back(task1);missionarray.push_back(task2);missionarray.push_back(task3);missionarray.push_back(task4);
}
void start_mission()
{while(true){int staues;int nread(0,staues,sizeof(int));if(nsizeof(int)){if(staues0stauesmissionnum){std::cout我是子进程getpid() 成功获取到任务码stauesstd::endl;missionarray[staues]();}}else if(n0){break;}else if(n0){perror(read);exit(EXIT_FAILURE);}}
}
void process_control()
{srand((unsigned int)time(NULL));int which0;for(int i0;i100;i){int cmdrand()%missionnum;int nwrite(channelarray[which]._write_fd,cmd,sizeof(int));if(n0){perror(write);exit(EXIT_FAILURE);}std::coutfather process send a message tochannelarray[which]._processid cmd :cmdstd::endl;which(which1)%processnum; }
}
void process_clean()
{for(int il1.size()-1;i0;i--){close(channelarray[i]._write_fd);int statues;int nwaitpid(channelarray[i]._processid,statues,0);if(n0){perror(waitpid);}else{std::cout子进程channelarray[i]._processid等待成功std::endl;}}
}
void processpool_init()
{for(int i0;iprocessnum;i){int pipefd[2];int npipe(pipefd);if(n0){perror(pipe fail);exit(EXIT_FAILURE);}int idfork();if(id0){perror(fork);close(pipefd[0]);close(pipefd[1]);exit(EXIT_FAILURE);}if(id0){close(pipefd[1]);dup2(pipefd[0],0);start_mission();exit(0);}close(pipefd[0]);channelarray.push_back(channel(id,pipefd[1]));}
}
task.hpp
typedef void (*mission)() ;
std::vectormission missionarray;
void task1()
{std::coutI am childprocess: getpid() running task1std::endl;
}
void task2()
{std::coutI am childprocess: getpid() running task2std::endl;
}
void task3()
{std::coutI am childprocess: getpid() running task3std::endl;
}
void task4()
{std::coutI am childprocess: getpid() running task4std::endl;
}运行截图
结语
那么这就是本期博客关于进程池的详细介绍了那么从进程池的意义以及进程池的实现大体框架到具体细节这几个维度带你全面解析进程池其次注意就是进程池的应用场景一定是要执行任务数量要大于子进程的数量如果你要执行30个任务创建27个子进程其实意义不大那么读者下来也可以自己实现一个属于你自己的进程池那么我的下一期博客会介绍命名管道那么我会持续更新希望您能够多多关注哦如果本篇文章有帮组到你还请三连加关注哦你的支持就是我创作的最大动力