全部代码:https://download.csdn.net/download/weixin_42295969/86504723
文章目录
一、多进程经典开发模型
多进程的最经典的开发场景就是Nginx,这是因为多进程很适合做负载均衡,进程能够申请资源,而线程更多的则是资源共享。
二、实践多进程程序
分为前台和后台
运行效果如下:
- 先运行前台程序,前台程序会直接通过execl调用后台程序。
整体结构
在结构中对前台以及后台的划分:
如何形成一个逻辑的闭环
在这里需要完成三个同步:1、开始进入消息队列发送接收之前的同步;2、对消息队列的发送与接收进行同步(这里系统调用的方法已经实现了同步);3、对消息队列接收之后的同步,这时候需要对命令处理之后信息进行输出,在后台处理,在前台输出;
消息队列前后的同步
解释图中×的意义以及为何去除
因为之前想的是还要实现消息队列的同步,所以是想把发送以及接收放在同步之前,或者将接收的部分放在同步之后,其实这会导致下一个命令到来的问题,所以这才有图上的×,表示这个顺序是错误的。
现在的实现思路
但是由于消息队列已经是同步的了,所以只需要实现消息队列之前和之后的同步;
如何在消息队列前后实现同步以及同步的意义?
- 首先确定的是消息队列已经实现同步功能,那么只需要保证两个进程的前后顺序就行
- 所以:
- 消息队列之前要保证同步;
- 消息队列之后要保证同步;
消息队列之前的同步:
通过建立有名管道实现同步,并管理内实现清零而已,保证之前是清零,因为这里实现了循环了,所以要保证每一个循环都是空的,然后再往里面写内容,同理消息队列之后的同步也是需要先做清零的
消息队列之后的同步:将结果输出,这里考虑到循环的问题,因为这也是共同的资源,所以也需要进行同步,所以同样通过有名管道将输出信息写进管道并进行读取;
前台分析
在这里实现两大组合:fork+execl+waitpid和fork+prctl(PR_SET_PDEATHSIG, SIGKILL)
- fork+execl+waitpid:waitpid让子进程不会进入僵尸状态,能够在父进程结束的时候,子进程也能够被结束
- fork从父进程分支出一个新的进程,而execl将想要执行的进程覆盖掉fork的子进程,这样的好处就是,之后能够用父进程与子进程之间的管理方式,而waitpid就是指的管理方式,等待子进程结束,防止僵尸进程的出现。
- fork+prctl(PR_SET_PDEATHSIG, SIGKILL)在父进程退出来的时候,能够保证子进程也能够退出来;
对孤儿进程和僵尸进程的分析: - 孤儿进程——父进程退出了,但是子进程还在并没有结束,所以最终被迫被公共的init进程(进程号PID = 1)回收;
- 僵尸进程——子进程退出了,但是正常情况应该是要发送sigCHID信号,通知父进程进行回收资源,尤其是进程号,但是没有发送信号就会产生僵尸进程;
在waitpid的时候需要结合进程的三种状态进行判断:
进程的这三种状态也可以类比理发店排队的情景:
如何同步
使用一个同步管道来维护前后台程序间的同步
- 有名管道有个特性是没写完不能读,没读完不能写,所以管道的双方就能够保证是同步的了;
那么如果是前后台的数据同步就可以利用两个命名管道来进行实现,如下所示: - 这里最终的需求是要消息队列的发送同步了。
前台程序的同步代码为:
writeSynPipe(); //写同步管道
sendMessage(msg, queId); //发送消息
readResPipe(); //读结果管道并打印
后台程序的同步代码为:
readSynPipe(); //读同步管道
if(readMessage(queId, &m) == -1) {
continue; //继续尝试
}
execCommand(m.text, resCommand); //执行命令
writeResPipe(resCommand); //将结果写入管道
其中synPipe是同步管道,resPipe是保存命令执行结果的管道。
后台分析
与前台一样,首先通过消息队列进行同步,然后通过有名管道在消息队列前后进行同步
//核心代码
writePipe();
// 6.对消息队列发送消息,将转换后的指令发到消息队列中去
sendMessage(msg, queId);
// 7.读结果管道并进行打印
readPipe();//这里又重新创建管道了
关键运行指令的函数以及发送信息的结构进行分析
convertCommand(pid, str, &msg);——前台将dir这样的windowns的指令转换linux下的指令ls
后台执行cmd指令并执行之后的结果void execCommand(const char *cmd, vector &res)
这里单独写了一个demo进行示例理解:
//执行shell指令,并将结果输出到字符串中
int myexec(const char *cmd, string &resvec)
{
resvec.clear();
FILE *pp = popen(cmd, "r"); //建立管道
if (!pp)
{
return -1;
}
char tmp[1024]; //设置一个合适的长度,以存储每一行输出
while (fgets(tmp, sizeof(tmp), pp) != NULL)
{
if (tmp[strlen(tmp) - 1] == '\n')
{
tmp[strlen(tmp) - 1] = '\0'; //去除换行符
}
resvec.append(tmp);
}
pclose(pp); //关闭管道
return resvec.size();
}
将ls进行执行,第一次结果
第二次结果:
可以看出pp就是命令行执行的结果,并能够通过一步步遍历获得输出结果
因为这里open里面的属性是r,表示对应到表述命令行里的标准输出,输入到返回的文件指针指向中去。以下是popen的详细理解说明
FILE * popen ( const char * command , const char * type );
int pclose ( FILE * stream );
- type 参数: 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
- command 参数:是一个指向以 NULL 结束的 shell 命令字符串的指针。shell 将执行这个命令。
- 返回的是标准IO流
头文件对应作用
#include <sys/types.h>
- 包含了一些基本的数据类型
key_t
表示进程的关键字,能够区别同一程序的不同进程
#include <sys/ipc.h>- sys/ipc.h是一种通讯格式,可在2个(多数情况下)或多个进程间传递信息
- 匿名管道以及命名管道/消息队列都是需要这个头文件的
#include <sys/msg.h>- 定义了消息队列相关
- 消息队列中发送以及接收消息
== #include<sys/wait.h>==- 定义了使用wait相关的定义
- waitpid使指定进程堵塞
#include <sys/prctl.h>- 给进程或者线程重命名或者其他操作,基本的进程管理函数
- 实现在进程退出来的时候
- 我有一个分叉到子进程的进程。如果父进程存在,子进程不应该存在。所以,我在子进程中调用
:: prctl(PR_SET_PDEATHSIG,SIGKILL)
来杀死父进程。最后发生的是父线程调用pthread_exit
,并且该线程最终成为杀死子进程的催化剂。在父线程退出时调用prctl(PR_SET_PDEATHSIG,SIGNAL)
,而不是父进程退出
#include <sys/stat.h>- 文件状态头文件还有一些常量
- mkfifo 创建一个名为 PATH 的新 FIFO,具有权限位 MODE
- umask(给文件管理权限赋予到最大值)
#include <fcntl.h>- cntl.h,是unix标准中通用的头文件,其中包含的相关函数有 open,fcntl,shutdown,unlink,fclose等!
- extern声明变量在其他地方,所以有时候比如unlink就在其他文件中,但是最终指向是
#include <unistd.h>- unistd.h 中所定义的接口通常都是大量针对系统调用的封装(英语:wrapper functions),如 fork、pipe 以及各种 I/O 原语(read、write、close 等等)。
#include <errno.h>- errno.h 是C语言C标准函式库里的标头档,定义了通过错误码来回报错误资讯的宏。