1. 进程等待必要性
- 我们知道,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程”的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
2. 进程等待的方法
2.1 wait方法
在Linux中,wait函数是一个系统调用,用于等待子进程的终止并获取其终止状态。该函数的原型如下所示:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
当子进程终止后,wait函数会返回子进程的进程ID(PID),并将子进程的终止状态存储在指针status指向的变量中。
status参数是一个指向整型变量的指针,用于存储子进程的终止状态。通过status可以获取子进程的退出状态、终止信号等信息。如果不关心终止状态,可以将status设置为NULL。
wait函数返回的PID有以下几种可能的取值:
- 如果成功等待到一个子进程的终止,返回子进程的PID。
- 如果调用进程没有子进程,wait函数会返回-1
- 如果调用进程被一个信号中断,wait函数会返回-1
如何使用wait进行等待?
- 调用wait函数,进程等待子进程的退出。
- 当子进程退出后,会变成一个僵尸进程(短暂的存在不会造成什么影响),然后通过wait函数,进程状态从僵尸状态(Z)变成死亡状态(X)。
- 如果子进程没有退出,父进程必须阻塞等待,直到子进程变成Z,wait自动回收返回。
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdlib>
int main()
{
pid_t fd = fork();
if (fd == 0)
{
sleep(5);
std::cout << "child: " << getpid() << std::endl;
exit(0);
}
if (fd > 0)
{
sleep(1);
std::cout << "parent: " << getpid() << std::endl;
}
sleep(50);
return 0;
}
子进程退出后由于父进程没有等待回收,子进程变成僵尸进程:
调用wait后,子进程就会回收释放了:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdlib>
int main()
{
pid_t fd = fork();
if (fd == 0)
{
sleep(5);
std::cout << "child: " << getpid() << std::endl;
exit(0);
}
if (fd > 0)
{
sleep(1);
std::cout << "parent: " << getpid() << std::endl;
wait(NULL);
}
sleep(50);
return 0;
}
我们再深入理解一下
注:
当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.
如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID为1)继承,当子进程终止时,init进程捕获这个状态.
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。
由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
- 1,WIFEXITED(status) :这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数——指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)
- 2, WEXITSTATUS(status): 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。
请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
我们看个例子
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t id = fork();
if (id == 0) {
// 子进程
printf("子进程开始执行\n");
sleep(3);
printf("子进程执行完毕\n");
exit(0);
} else if (id > 0) {
// 父进程
printf("父进程等待子进程终止\n");
int status;
pid_t child_pid = wait(&status);
if (child_pid == -1)
{
perror("wait");
exit(1);
}
if (WIFEXITED(status))
{
printf("子进程正常终止,退出状态:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status))
{
printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
}
printf("父进程继续执行\n");
}
else
{
perror("fork");
exit(1);
}
return 0;
}
在上面的示例中,父进程通过fork创建了一个子进程。
子进程会执行一段耗时的操作(这里使用sleep模拟),然后退出。
父进程调用wait函数等待子进程的终止,并获取子进程的终止状态。
最后,父进程继续执行。
2.2.参数status
2.2.1、status 参数是位图结构
wait 和 waitpid,都有这个 status 参数,如果传递 NULL,则表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
该参数是一个 输出型参数 (即通过调用该函数,从函数内部拿出来特定的数据)。
什么叫输出型参数?就是函数调用结束以后,会将参数的值写到这个变量里。
换句话说,这个status是用来接收的,本质上不是用来传参的。
我们把我们的status定义好了之后,放到该函数里,作为参数传递过去,函数调用完后,操作系统就会把status的值自动填充好,然后还给我们。实现的原理很简单,因为其用的是指针,传递的是变量的地址。倘若我们不关心这个status状态,那么直接传递NULL即可。
并且,status 参数是由操作系统填充的!是一个整数,该整数就是下面我们要详细研究的。
也就是说我们下面说的参数status不是wait唯一的参数——指向整数的指针status,而是那个指针所指向的整数
它虽然是一个 int 型整数,但是不能简单地将其看作整型,而是被当作一个 位图结构 看待。
不过,关于 status 我们只需要关心该整数的 低 16 个比特位!
我们不必去关心它的高 16 位,因为凭借低 16 位就足以判断了。
然而,整数的低 16 位,其中又可以分为 最低八位 和 次低八位(具体细节看图):
我们之研究status的低16比特位
最低八位(包括core dump)存储的是终止信号,次低八位存储的是退出状态
2、次低八位:拿子进程退出码
重点:通过提取 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 的次低八位了。
3、 最低七位:提取子进程的退出信号
重点:通过提取 status 的最低七位,就可以拿到子进程的退出信号。
我们的 status 的低八位用于表示处理异常的地方,其中有 1 位是 core dump,我们下面讲。
除去 core dump,剩余七位用于进程中的退出信号,这就是 最低七位。
进程退出,如果异常退出,是因为这个进程收到了特定的信号。
我们虽然还没有开始讲解信号,但是我们前几张就介绍了 kill -9 这样的杀进程操作。
这个 -9 我们当时说了,就是一个信号,发送该信号也确实可以终止进程。
刚才我们讲的 wait/waitpid 和次低八位的时侯,都是关于进程的 正常退出。
如果进程 异常退出 呢?
我们来模拟一下进程的异常退出。
结果:
因为子进程是个死循环,父进程又调了 waitpid,导致父进程一直在 "阻塞式" 地等待子进程。
父进程在等待子进程期间什么都没有干,就搬了张板凳坐在那等子进程死。
信号是可以杀掉进程的,我们现在主动输入 kill -9:
此时我们就成功拿到了子进程的退出信号,9 是因为我们输入的信号就是 9。
此时父进程看到子进程寄了,终于可以不用等了,可以给子进程收尸了
还是那句话,代码跑完结果是什么已经不重要了,我们最关心的是因为什么原因退出的。
当进程收到信号时,就代表进程异常了。进程程出,如果是异常退出,是因为该进程收到了特定的信号。其实除了 9 号信号还有很多信号,输入 kill -l 就可以查看这些;
总结:退出信号代表进程是否异常,退出码代表进程在退出之时代码对还是不对。
4、进程退出的宏
我们今天写的代码,是通过位操作去截 status 得到退出码和退出信号的。
实际上,你也可以不用位操作,因为 已经给我们提供了一些宏供我们直接调用。
它们是 WEXITSTATUS 和 WIFEXITED,在这之前,我们再思考一个问题:
思考:一个进程退出时,可以拿到退出码和退出信号,我们先看谁?
一旦程序发现异常,我们只关心退出信号,退出码没有任何意义。
所以,我们先关注退出信号,如果有异常了我们再去关注退出码。
WEXITSTATUS 宏用于查看进程的退出码,若非 0,提取子进程退出码。
WEXITSTATUS(status)
WIFEXITED 宏用于查看进程是否正常退出,如果是正常终止的子进程返回状态,则为真。
WIFEXITED(status)
结果是
当然了,如果你压根就不关注推出信息和退出码,你直接把 status 设置为 NULL 就行。
2.3. waitpid方法
waitpid函数是Linux中用于等待指定子进程终止的系统调用。
与wait函数类似,waitpid函数也可以用于获取子进程的终止状态。
#include <sys/types.h>
#inlclude <sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);
函数功能是:父进程一旦调用了waitpid就立即阻塞自己,由waitpid自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,waitpid就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,waitpid就会一直阻塞在这里,直到有一个出现为止。
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。
下面我们就来详细介绍一下这两个参数:
pid:
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
- pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
- pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
- pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
- pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options:
options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
ret = waitpid(-1, NULL, WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:
ret = waitpid(-1, NULL, 0);
如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。
看到这里,聪明的读者可能已经看出端倪了:wait不就是经过包装的waitpid吗?
没错,察看<内核源码目录>/include/unistd.h文件349-352行就会发现以下程序段:
static inline pid_t wait(int * wait_stat)
{
return waitpid(-1,wait_stat,0);
}
返回值和错误
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
- 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t pc, pr;
pc = fork();
if (pc < 0) /* 如果fork出错 */
{
printf("Error occured on forking.\n");
}
else if (pc == 0) /* 如果是子进程 */
{
sleep(10); /* 睡眠10秒 */
exit(0);
}
/* 如果是父进程 */
do {
pr = waitpid(pc, NULL, WNOHANG); /* 使用了WNOHANG参数,waitpid不会在这里等待 */
if (pr == 0) /* 如果没有收集到子进程 */
{
printf("No child exited\n");
sleep(1);
}
} while (pr == 0); /* 没有收集到子进程,就回去继续尝试 */
if (pr == pc)
{
printf("successfully get child %d\n", pr);
}
else
printf("some error occured\n");
}
父进程经过10次失败的尝试之后,终于收集到了退出的子进程。
因为这只是一个例子程序,不便写得太复杂,所以我们就让父进程和子进程分别睡眠了10秒钟和1秒钟,代表它们分别作了10秒钟和1秒钟的工作。父子进程都有工作要做,父进程利用工作的简短间歇察看子进程的是否退出,如退出就收集它。
提示:可以尝试在最后一个例子中把pr=waitpid(pc, NULL, WNOHANG); 改为pr=waitpid(pc, NULL, 0);或者pr=wait(NULL);看看运行结果有何变化?(修改后的结果使得父进程将自己阻塞,直到有子进程退出为止!)
3.进程的阻塞等待方式
3.1.阻塞等待
- 如果子进程没有退出,而父进程在进行执行waitpid进行等待,阻塞等待。
- 大部分IO类的函数例如scanf各种各样的接口,只要涉及IO的或多或少会可能出现阻塞的状态。
- 现在所用的大部分接口都是阻塞接口(逻辑简单,容易实现)
- **阻塞等待(Blocking Wait)**在编程中通常指的是一个线程或进程在等待某个条件满足或某个操作完成之前,会暂停执行其他任务,处于等待状态。这种状态会一直持续,直到等待的条件满足或操作完成,线程或进程才会继续执行后续的任务。在Java中,阻塞等待常用于多线程编程中,用于线程之间的同步和通信。
3.2.进程阻塞:
- 把进程的R状态设置为S状态
- 把进程的PCB从运行队列移动到等待队列中,不再被调度,而是等待
- 本质上是等待某种条件发生。
- 软件条件满足(子进程退出)
- 硬件资源就绪(scanf键盘输入数据发生)
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0) { //child
printf("子进程已运行, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else {
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//父进程在这里阻塞式等待,等待5S
printf("这是等待的测试\n");
if (WIFEXITED(status) && ret == pid)
{
printf("等待子进程5秒成功,子进程返回代码为:%d.\n", WEXITSTATUS(status));
}
else {
printf("等待失败, return.\n");
return 1;
}
}
return 0;
}
4.进程的非阻塞等待方式:
在子进程运行期间,父进程除了等待子进程或者是休眠,能不能干点其他的事情❓
当然可以,在父进程等待,阻塞状态。可以通过设置options来让父进程干点事情。不阻塞等待而是非阻塞等待。
4.1.什么又是非阻塞等待呢?用代码该怎么去实现呢?
**非阻塞等待(Non-blocking Wait)**则与阻塞等待相反。当线程或进程在等待某个条件满足或某个操作完成时,它不会暂停执行其他任务,而是会继续执行后续的任务。也就是说,即使等待的条件还没有满足或操作还没有完成,线程或进程也不会被阻塞,而是会继续执行其他的操作。
通过设置options的宏值WNOHANG(wait no hang 等待没有阻塞 = 非阻塞等待)
在计算机中,"HANG" 通常指的是程序或系统出现无响应或停顿的状态,也就是常说的“卡住”或“死机”。
当程序或系统由于某种原因(如资源锁定、死循环、死锁或外部系统交互问题等)而无法继续正常执行时,就可能会出现"HANG"的情况。这种情况下,用户可能无法与程序或系统进行交互,需要等待程序或系统恢复正常或进行重启操作。
另外,在一些特定的语境下,"HANG" 也可能被用来描述服务器或数据库的某些服务出现故障或无法访问的情况,这也可以被视为一种"宕机"现象。在这种情况下,"HANG" 指的是服务器或数据库的服务因为某种原因而停止响应或无法提供服务。
具体操作
- options这个参数只要一设置就会出现非阻塞等待。
- 设置waitpid的WNOHANG本质上是检测一次进程的状态变化。
- 调用一次waipid就检测一次。每次调用都是检测,多次调用多次检测。
- 非阻塞等待调用多次waitpid,调用waitpid检测是否退出等待过程无问题,只是子进程还未终止,需要等待下次等待。
综上:非阻塞等待的时候 + 循环 = 非阻塞轮询
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0) { //child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(1);
}
else {
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if (ret == 0) {
printf("child is running\n");
}
sleep(1);
} while (ret == 0);
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n", WEXITS TATUS(status));
}
else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
5.阻塞等待VS非阻塞等待
场景:张三找李四求助帮他复习期末考试。张三在李四的楼下等待李四就绪。
5.1.非阻塞等待:
张三每隔几分钟就给李四打电话询问他是否就绪了,张三在没有打电话的时间看书/游戏/抖音
- 就绪的过程本质就是非阻塞等待。
- 张三非阻塞等待李四过程 == 函数调用
- 张三给李四打电话 == 函数传参
- 李四说等着没好 == 函数的返回值
- 每次函数调用的本质是检测李四的状态(是否就绪)
- 立刻有返回值,多次等待,多次返回。
- pid_ t waitpid(pid_t pid, int *status, WNOHANG);
- pit_t == 0 :检测是成功的,只不过子进程还没退出,需要你下一次进行重复等待。
- pit_t > 0 :等待成功,子进程退出了,并且父进程回收成功。
- pit_t < 0 :等待失败。
5.2.阻塞等待:
张三一直给李四打着电话,直到李四就绪,期间张三一直等待李四就绪,不敢别的事情。一直检测李四的状态(不就绪,就不返回)
一直等待。直到子进程终止才返回。
- pid_ t waitpid(pid_t pid, int *status, 0);
- pit_t > 0 :等待成功,子进程退出了,并且父进程回收成功。
- pit_t < 0 :等待失败。