目录
一、进程等待
之前在将僵尸进程和孤儿进程的时候,有讲过些许僵尸进程的概念,当一个子进程执行结束的时候,父进程没有退出,但是也对子进程不管不顾,那么子进程就会一直保持僵尸状态,详细可回看文章《僵尸进程与孤儿进程》,传送门给大家放下面啦~
一直保持僵尸状态的进程会持续占用内存空间,造成内存泄漏,那么如何避免这种情况的产生呢?就需要从我们进程等待的必要性开始讲解了
1.进程等待的必要性
①之前讲过,子进程退出,如果父进程不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏
②另外,进程一旦进入"僵尸"状态,那就刀枪不入了,就连"杀人不眨眼"的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
③最后一点,父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对,是否正常退出,我们都需要知道
解决方法:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
2.wait与waitpid
我们先来查看这两个函数的信息

有了它们的头文件和参数信息,我们就可以来验证父进程等待可以解决子进程的僵尸问题了,我们先来演示一下父进程不管不顾而让子进程变成僵尸进程的情况,代码如下:

执行这段代码的同时我们用如下指令来实时观测父子进程的状态(ctrl+c停止):
while true;do ps axj | head -1 && ps axj | grep code;sleep 1;done

当子进程循环结束后,exit退出,但是父进程不等待子进程,因此子进程会进入僵尸状态,如上图,结果符合预期
那我们就给父进程调用wait函数,等待子进程的死亡,看看是否能将子进程回收,为了更加直观看到状态变化情况,我们等到子进程结束后进入僵尸状态,让父进程等待3秒再回收,然后再过3秒,父进程结束,看看父子进程的状态变化


我们看左下和右边部分,可以看到是显示回收成功了!并且右边实时监控画面的画圈的僵尸状态被回收没有了,最后父进程也被回收了(第二个画圈位置的进程),因此,子进程退出,需要僵尸维持自己的退出状态。如果父进程wait子进程,但是子进程就是没有退出,则父进程会阻塞在wait函数中。
wait的调用确实能解决子进程的僵尸问题,但是父进程还需要获取子进程的退出信息以及执行结果是否准确的信息,于是我们就需要下一个系统调用waitpid了,这个系统调用相对会更复杂,我们来看看它的具体信息:

waitpid的参数中,当pid为-1的时候,是选择等待任何一个子进程,大于0的时候,等待其进程PID与pid相等的子进程,第二个参数与wait的参数相同,第三个参数则默认为0,表示阻塞状态,当第一个参数为-1,第三个参数为0的时候,那么waitpid的作用其实和wait是基本一致的。
而在这里主要要将的参数是status
3.status与位图
status参数,其实是一个输出型参数,什么意思呢?就是我们想要的子进程信息(退出码与信号码)都是从这个参数中获取的,具体这样做,先定义一个简单的int类型的status变量,然后再调用waitpid函数,将&status放入到函数的第二个参数当中,函数运行结束后,我们得到的status就是我们想要的子进程信息(status由操作系统填充),而之前的代码传递NULL则是表示不关心子进程的退出状态。我们用代码来演示一下获取status
以下是父进程的代码,我就不打印"我是父进程"的内容了,直接等待8秒(等子进程5秒结束后进入僵尸再等3秒后回收),然后通过status获取子进程的信息

我们运行看一下得到的信息如何

这里我们等待成功并获得的子进程信息是256
这里虽然得到的status是256,但是我们不是需要两个子进程的信息吗?分别是退出码(exit_code)和信号码(sig),这里却只有一个整数是什么原因?
这就要涉及到位图了,status通常属于一个32位整数,其内部用位图(比特位分段)储存了子进程的终止信息,不同比特位代表不同含义(退出码、信号码等)。内核会根据子进程的终止方式(正常退出/信号终止)设置这些比特位,我们就需要通过位图操作解析出具体信息
32位的status的低16位是关键,大致划分为低8位和高8位。低8位是当子进程被信号终止时。存储终止信号值的;而高8位用于存储正常退出的子进程的退出码的

接下来我们就来演示一下如何获取到退出码和信号码


于是我们在等待成功之后,就通过status得到了退出码为1,信号码为0的信息。
这就是父进程通过waitpid调用,获取子进程的退出信息的方式是让
除了正常退出信号码为0的情况,我们也可以通过信号让进程异常终止,通过观察信号码看看是否是让进程终止的那个信号。我们先给子进程设置一个死循环,方便我们由足够的时间输入信号指令


结果的信号码确实是我们输入的9号信号码
当进程无法被正常回收的时候,waitpid的返回值则为-1,我们故意将一个不存在的子进程id写入waitpid的第一个参数,来检验一下返回值,并打印出错误信息


由此可以看到返回值确实为-1,错误信息为不存在这样的子进程
4.获取信号码、退出码的宏定义
WIFEXITED(status):判断子进程是否正常退出
WEXITSTATUS(status):获取子进程的退出码,其实就是 (status >> 8)&0xFF的宏定义
WIFSIGNALED(status):判断子进程是否被未捕获的信号终止
WTERMSIG(status):提取终止子进程的信号码,其实就是 status&0x7F的宏定义
5.阻塞等待与非阻塞等待
回顾我们刚刚讲的waitpid第三个参数

我们一开始默认option的参数值为0,这就是默认为父进程的等待方式为阻塞等待。当子进程迟迟没有退出时,父进程的进度就会阻塞在waitpid函数中,此时父进程除了等待干不了任何事情,这种情况就被称为阻塞等待。
但是我们的父进程也不能只有等待回收子进程一个功能啊,它也有自己的任务要完成,此时就需要非阻塞等待的方式。
非阻塞等待,顾名思义,与阻塞等待相反。阻塞等待时,父进程只"查看"一次子进程是否结束,结束了就回收,没结束就一直保持着"查看"状态,除了这样什么事情也不干;而非阻塞等待则是多次"查看"子进程是否结束的状态,第一次"查看"子进程的时候没有结束,就先干自己的事情,干一会再查看子进程的状态,还没结束再继续埋头干自己的工作,周而复始,直到子进程结束被回收。
option为1时是阻塞等待,要换成非阻塞等待,需要把 WNOHANG 传给option!这个本质上也是一个整数被替换了宏定义而已,wait no hang的意思。
既然非阻塞等待的情况下父进程要不断查看子进程退出状态,那么只等待一次肯定不够的,我们需要等待多次,因此可以给父进程的等待代码设置一个死循环,只有回收子进程后或者回收失败才退出循环,代码如下:


于是这样就实现了让父进程非阻塞等待
父进程在等待的过程中可以做其他事情,但是这里的代码可能不明显,那我就再写几个函数,让父进程一边等待也可以一边执行任务,下面的代码我用C++混合来写吧,将指针函数放入vector当中,利用范围for的语法特性(C++11特性),依次让父进程执行。
先改代码文件名和Makefile编译逻辑


父进程任务我们可以写比如打印日志、将数据写入磁盘、写入数据库等等

然后全部放入vector中

让父进程执行

让我们来看看运行结果

至此,非阻塞等待的优势得以证明!
这里单独说一个初学者可能会遇到的误区,很多初学者会认为非阻塞等待比阻塞等待的等待效率更高,其实这是不对的,等待的时长不由父进程决定,而是由子进程决定,它什么时候结束,父进程就什么时候能回收,只不过是在等待过程中,非阻塞等待能比阻塞等待多做点事情而已
因此,要等待子进程的结束,使用waitpid是最佳实践!
进程等待我们讲解得差不多了,我将讲解进程等待用到的源码给大家放在下面:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<iostream>
#include<vector>
typedef void (*callback_t)();
void PrintLog()
{
std::cout << "print Log" << std::endl;
}
void SyncDisk()
{
std::cout << "write data to Dist" << std::endl;
}
void WriteDataToMysql()
{
std::cout << "write data to mysql" << std::endl;
}
int main()
{
std::vector<callback_t> tasks;
tasks.push_back(PrintLog);
tasks.push_back(SyncDisk);
tasks.push_back(WriteDataToMysql);
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt--)
{
printf("我是子进程,PID:%d,PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
else{
//parent
// int count = 5;
// while(count--)
// {
// printf("我是父进程,PID:%d,PPID:%d\n", getpid(), getppid());
// sleep(1);
// }
// sleep(3);
// pid_t rid = wait(NULL);
// if(rid > 0)
// {
// printf("回收成功!\n");
// }
// sleep(3);
// sleep(8);
// int status = 0;
// pid_t rid = waitpid(id+1, &status, 0);
// if(rid > 0)
// {
// int exit_code = (status >> 8)&0xFF ;
// int sig = status&0x7F;
// printf("wait success! , status:%d, exit_code:%d, sig:%d\n", status, exit_code, sig);
// }
// //sleep(3);
// else{
// printf("ret: %d\n", rid);
// perror("waitpid");
// }
// exit(1);
//father
while(1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG); //非阻塞检测&&回收
if(rid > 0)
{
printf("wait success! 回收的子进程是:%d, exit_code:%d\n", rid, WEXITSTATUS(status));
break;
}
else if(rid == 0)
{
printf("子进程正在运行,父进程还需要等待!\n");
usleep(100000);
for(auto &task : tasks)
{
task();
}
}
else{
perror("waitpid");
break;
}
}
}
return 0;
}
二、创建多进程与回收多进程
有了前面的知识体系,现在我们将通过代码一步步实现多进程的创建与回收。
1.设置指令形式
可以通过环境变量来约定多进程创建的指令,比如我们要一次性创建5个进程,需要这样的指令:./code 5, 字符串只能有两个,如果指令形式不合法,直接结束进程

这里多写了一个枚举,我尽量让代码写正式一点,不能老拿hello world糊弄你们哈哈哈
2.单进程创建与回收
代码不进入if语句执行,则代码合规,我们就要获取要创建的子进程的数目,方便后续创建,由于argv1也就是指令输入的创建进程数目在这个数组里是一个字符串,因此我们还要把它转换成数字。

如下这是我们之前写的子进程的创建与回收的代码形式

子进程的Task()函数具体我没实现,加上去表示待完成的任务的
3.创建多进程
但是这样子的代码只能创建和回收一个子进程,我们需要的是创建多个子进程,于是我们就要用到num了,利用for循环创建和回收子进程

这样就能够实现多次进程的创建与回收了
4.收集多进程PID
但是这样的创建与回收,也只是创建一个就马上回收一个,然后再创建再回收,这样周而复始。如果我们想要一次性创建多个进程,然后后面父进程统一回收呢?我们可以把父进程移除for循环,先创建多个子进程

但是有人就会问了,上一篇文章讲exit的时候,不是说exit在任何位置执行都会直接退出进程吗?那我们上图exit的代码执行后,进程退出了我们还怎么创建多个子进程?这里要注意,那个位置的exit,属于子进程的代码,不属于父进程,退出的也只是子进程,创建一个完成任务后就退出一个,但是for循环属于父进程的代码,会执行下去。最后留下多个僵尸子进程让父进程一次性回收到位
5.回收多进程
那么多个子进程已经被创建出来,父进程不可能只回收一次吧?因此父进程也需要一个循环来不断回收子进程,但是父进程的代码在那个位置无法获取子进程的PID,怎么回收???
我直接讲最佳实践,那就是定义一个vector,在子进程创建并退出后记录其PID,然后一一提供给父进程回收

这样就可以实现多进程的创建与回收了
6.完善代码并封装函数
我们来将代码进一步完善,先把子进程的Task函数写了,让它打印五次进程信息

这样就差不多了
但是这样写的代码还是太乱了不好维护,我们既然要写正式一点,那就把多进程的创建和回收各自封装成一个函数,方便维护



这样就完成了多进程创建鱼回收的函数封装
我们来运行看看创建3个子进程的效果如何

7.提升子进程执行任务的灵活性
但是感觉代码的通用性依旧有待加强,比如子进程执行的函数不应该固定是Task,应该可以通过传参的方式自由切换
因此我们故技重施,使用指针函数




这样就可以根据实际需求,给定子进程不同的任务了。
至此,多进程创建与回收的代码我们圆满完成了
其实还可以继续完善,因为目前的多个子进程都是在做同一个任务,还可以利用vector封装不同的函数,然后循环创建子进程的时候顺便遍历vector然后给不同的子进程不一样的任务,这里就不细讲啦,给大家当作额外动手能力拓展吧~
8.源码分享
对于写刚刚一步一步写的多进程的创建与回收的代码,我给大家放在下面啦~大家自取即可
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<vector>
typedef void (*callback_t)();
enum
{
OK=0,
USAGE_ERR=1
};
void Task()
{
int cnt = 5;
while(cnt--)
{
printf("我是一个子进程,我在完成Task任务,PID:%d, PPID:%d, cnt:%d\n", getpid(), getppid(), cnt);
sleep(1);
}
}
void Hello()
{
int cnt = 5;
while(cnt--)
{
printf("我是一个子进程,我在完成Hello任务,PID:%d, PPID:%d, cnt:%d\n", getpid(), getppid(), cnt);
sleep(1);
}
}
void CreateChildProcess(int num, std::vector<pid_t> *subs, callback_t cb)
{
for(int i = 1; i <= num; i++)
{
pid_t id = fork();
if(id == 0)
{
//child 代码区
cb();
exit(0);
}
//父进程代码区
subs->push_back(id);
}
}
void WaitAllChild(const std::vector<pid_t> &subs)
{
//father
for(auto &pid : subs)
{
int status = 0;
pid_t rid = waitpid(pid, &status, 0);
if(rid > 0)
{
printf("子进程:%d Exit, exit_code:%d\n", rid, WEXITSTATUS(status));
}
}
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cout << "Usage: " << argv[0] << "process_num" << std::endl;
exit(USAGE_ERR);
}
int num = std::stoi(argv[1]);
std::vector<pid_t> subs;
//创建多进程
CreateChildProcess(num, &subs, Task);
//回收多进程
WaitAllChild(subs);
return OK;
}
进程等待的讲解到此结束,感谢大家观看,后续持续更新相关内容喔~