前言:我们开始讲解进程等待,简单地讲解 wait 函数,然后我们主要讲解?waitpid 函数。由于 wait 只有一个参数 status,且 waitpid 有三个参数且其中一个也是 status,我们本章重点讲解这个 status 参数。
为什么要进行进程等待?不知道大家是否还记得我们在之前的章节讲过的 "僵尸进程" 的概念。
僵尸状态:当一个? 中的进程退出的时候,一般不会直接进入??状态(死亡,资源可以立马被回收),而是进入??状态。?
子进程退出如果父进程不管不顾,就 可能造成僵尸进程的问题,一直占内存进而引发内存泄露。?
所以我们必须让其从??状态变为??状态:?
进而允许操作系统能去释放它(将代码和数据 free 掉,将相关数据结构归还给 slab 分派器)。
?上面我们讲的实际上就是我们需要进程等待的一个原因 —— 解决内存泄露问题 。
然而不仅仅这一个原因,我们还需要进程等待来 获取子进程的退出状态 。
我们需要知道父进程派给子进程的任务完成的如何。
比如子进程运行完成,结果对还是不对,是否正常退出?
通过进程等待的方式,回收子进程资源,获得子进程退出信息。
获取子进程的退出状态是否需要将曾经子进程的退出信息保存起来,然后被恢复、读取呢??
这和我们刚才讲的进程退出有着大大的关系!我们知道了进程退出是有退出码的。
我们需要让子进程退出时它的?return
?结果或者?exit?的结果是需要被父进程读到的。
?总结:需要进程等待的原因:① 解决内存泄露问题? ? ?② 获取子进程的退出状态
函数原型是
#include <sys/types.h>
#include <wait.h>
int wait(int *status)
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。?
我们看到,wait?有一个叫 status?的参数
我们下面验证一下这个函数是否可以 ,我们会把 status?暂且设置为空。
验证:?
简单来说,就是让父子进程都跑起来,并各自打印消息,子进程一直死循环运行,父进程将自己休眠 20 秒,在这 20 秒之内我们手动把处于死循环的子进程给 kill?掉,此时子进程会处于??状态,此时父进程还在休眠不会立即调用 wait(这么做是为了便于观察)。
当 20 秒过后,父进程苏醒后执行了 wait?函数,用一个变量 ret?去接收 wait?的返回值,通过返回值来确定是否等待成功。如果等待成功了就会成功?。?
为了观察进程状态,我们写一段监控脚本,做到每隔一秒就监控一下我们的 mytest?进程。
监控脚本:
$ while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; echo "-----------------------------------------------------------------------"; sleep 1; done
运行后我们只需要死死盯着 ,观察状态的变化即可!
开三个窗口,分别用于运行监控脚本、运行 mytest?和 kill?子进程:
?
至此,我们成功验证了父进程等待了子进程。
通过 wait()?的方案,我们可以解决回收子进程的??状态,让子进程进入??状态。
刚才讲的 wait?并不是主角,因为其功能比较简单,在进程等待时用的更多的是?waitpid
waitpid?可以把?wait?完全包含,wait?是?waitpid?的一个子功能。
waitpid:等待任意一个退出的子进程。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:?
对于返回值 pid_t:
对于参数 pid:
options:
options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret = waitpid(-1, NULL, WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:
ret = waitpid(-1, NULL, 0);
对于参数 status:
?值得强调的是,wait/waitpid?是系统调用!
?
说明:父进程等待子进程,子进程也会执行自己的代码。当子进程执行了 return/exit 退出后,子进程会将自己的退出码信息写入自己的进程控制块 PCB中。子进程退出了,代码可以释放,子进程退出后变成 Z 状态,其本质上就是将自己的 task_struct 维护起来(代码可以释放,但是 task_struct 必须维护)。所谓的 wait/waitpid 的退出信息,实际上就是从子进程的 task_struct 中拿出来的,即 从子进程的 task_struct 中拿出子进程退出的退出码
所以,我们的父进程在等待子进程死亡,等子进程一死,就直接把子进程的退出码信息拷贝过去,通过 wait/waitpid?传进来的参数后,父进程就拿到了子进程的退出结果。即?子进程会将自己的退出信息写入 task_struct??。?
子进程的 task_struct?是操作系统的内部数据结构这个 waitpid?自然就是系统调用,实际上就是调用操作系统的功能。?
总结:父进程通过调用 wait/waitpid,通过 status?参数,就可以将子进程的信息拿到手。
wait 和 waitpid,都有这个 status?参数,如果传递 NULL,则表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。?
该参数是一个 输出型参数?(即通过调用该函数,从函数内部拿出来特定的数据)。
并且,status 参数是由操作系统填充的!是一个整数,该整数就是下面我们要详细研究的。
它虽然是一个?int?型整数,但是不能简单地将其看作整型,而是被当作一个 位图结构?看待。
不过,关于 status?我们只需要关心该整数的 低 16 个比特位!
我们不必去关心它的高 16 位,因为凭借低 16 位就足以判断了。
然而,整数的低 16 位,其中又可以分为 最低八位 和 次低八位(具体细节看图):
?
重点:通过提取 status?的次低八位,就可以拿到子进程的退出码。?
验证:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main ()
{
pid_t id = fork();
if (id == 0) {
int cnt = 5; // 循环5次
// child
while (cnt--) {
// 五秒之内运行状态
printf("我是子进程,我正在运行... Pid: %d\n", getpid());
sleep(1);
// 五秒之后子进程终止
}
exit(233); // 方便辨识,退出码我们设置为233,这是我们的预期结果
}
else {
printf("我是父进程: pid: %d,我将耐心地等待子进程!\n", getpid());
// ***** 使用waitpid进行进程等待
int status = 0; // 接收 waitpid 的 status 参数
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) { // 等待成功
printf (
"等待成功,ret: %d, 我所等待的子进程退出码: %d\n",
ret,
(status>>8)&0xFF
);
}
}
}
我们说了,status?并不是整体使用的,而是区域性使用的,我们要取其次低八位。我们可以用 位操作?来完成,将 status?右移八位再按位与上 ,即 (status>>8)&0xFF?,就可以提取到 status?的次低八位了。
?重点:通过提取 status?的最低七位,就可以拿到子进程的退出信号。
我们的 status?的低八位用于表示处理异常的地方,其中有 1 位是 core dump,我们下面讲。
除去 core dump,剩余七位用于进程中的退出信号,这就是 最低七位。
进程退出,如果异常退出,是因为这个进程收到了特定的信号。
我们虽然还没有开始讲解信号,但是我们前几张就介绍了?kill -9?这样的杀进程操作。
这个 -9?我们当时说了,就是一个信号,发送该信号也确实可以终止进程。
刚才我们讲的 wait/waitpid?和次低八位的时侯,都是关于进程的?正常退出。
如果进程 异常退出 呢?我们来模拟一下进程的异常退出。
代码演示:
结果:
因为子进程是个死循环,父进程又调了?waitpid,导致父进程一直在 "阻塞式" 地等待子进程。
父进程在等待子进程期间什么都没有干,就搬了张板凳坐在那等子进程死。
信号是可以杀掉进程的,我们现在主动输入 kill -9:
此时我们就成功拿到了子进程的退出信号,9?是因为我们输入的信号就是 9。
此时父进程看到子进程寄了,终于可以不用等了,可以给子进程收尸了()
还是那句话,代码跑完结果是什么已经不重要了,我们最关心的是因为什么原因退出的。
当进程收到信号时,就代表进程异常了。进程程出,如果是异常退出,是因为该进程收到了特定的信号。其实除了 9?号信号还有很多信号,输入 kill -l?就可以查看这些;
总结:退出信号代表进程是否异常,退出码代表进程在退出之时代码对还是不对。
我们今天写的代码,是通过位操作去截 status?得到退出码和退出信号的。
实际上,你也可以不用位操作,因为? 已经给我们提供了一些宏供我们直接调用。
它们是 WEXITSTATUS?和 WIFEXITED,在这之前,我们再思考一个问题:
思考:一个进程退出时,可以拿到退出码和推出信号,我们先看谁?
一旦程序发现异常,我们只关心退出信号,退出码没有任何意义。
所以,我们先关注退出信号,如果有异常了我们再去关注退出码。
WEXITSTATUS 宏用于查看进程的退出码,若非 0,提取子进程退出码。
WEXITSTATUS(status)
WIFEXITED?宏用于查看进程是否正常退出,如果是正常终止的子进程返回状态,则为真。
WIFEXITED(status)
?代码演示:运用 WEXITSTATUS?和 WIFEXITED?宏
?
结果为:
当然了,如果你压根就不关注推出信息和退出码,你直接把 status?设置为 NULL?就行。
感谢观看!!!!