3.进程等待
引言:什么是进程等待
想象有两个小伙伴,一个是 “大强”(父进程 ),一个是 “小强”(子进程 )。大强给小强安排了任务,比如去收集一些石头。
1. 大强想知道小强啥时候把任务完成,就用了个办法:他跟小强说,等你干完活,记得跟我说一声。然后大强就不做别的事了(从运行状态变成阻塞状态 )在那等着小强的消息,把 CPU 让给其他小伙伴(其他就绪进程 )去用。
2.小强呢,就跑去收集石头了。等他把石头收集完(子进程完成任务并终止 ),就赶紧给大强发个信号,说 “我干完啦”。大强收到这个信号后,就从等着的状态(阻塞状态 )醒过来(变成就绪状态 ),然后 CPU 就又可以让大强接着做他后面的事啦,比如看看小强收集的石头合不合格(处理子进程的终止状态 )。
在计算机里,wait
或者 waitpid
这些函数就像是大强用来等小强消息的工具,通过它们,父进程就能等着子进程把活干完,再接着往下走 。
3.1 进程等待必要性
- 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存 泄漏。
- 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也 没有办法杀死⼀个已经死去的进程。
- 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是 不对,或者是否正常退出。
- ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息.
3.2 进程等待的⽅法
3.2.1 wait⽅法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
wait
函数的作用机制
wait
函数是一个系统调用,其作用是使父进程暂停执行,等待其子进程中的任意一个终止。当父进程调用 wait
时,内核会检查是否有已经终止的子进程。如果有,wait
会获取该子进程的终止状态信息,并返回该子进程的进程 ID,父进程继续执行后续代码;如果没有子进程终止,父进程就会被阻塞(进入睡眠状态 S),直到有子进程终止才会被唤醒并继续执行。
第一种 正常,回收⼦进程资源,获取⼦进程退出信息.
wait
函数会阻塞父进程,直到它的一个子进程终止。当子进程终止后,wait
函数返回终止子进程的进程 ID
第二种 如果等待子进程,子进程没有退出,父进程会阻塞在wait调用出吗?
在同一账户开两个窗口
while :; do ps ajx | head -1 && ps ajx | grep wlw5 ; sleep 1;done |grep -v grep
man 2 wait
3.2.2 waitpid⽅法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
获取id的值 ,与wait效果一样
当pid的值>0时, id+1仿造等待其进程ID与pid相等的⼦进程。
接收指定的子进程所以,导致建立的子进程变成僵尸进程🧟♀️
问题一 获取退出码
首先定义了 int status = 0;
用于存储子进程的终止状态信息。然后调用 pid_t rid = waitpid(id, &status, 0);
,其中 id
是要等待的子进程的进程 ID,&status
用于存储子进程的终止状态,0
表示默认的等待选项。
为什么返回的 为什么不是1,而是256呢?
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
⽐特位):
可以把它当成一张位图来看,而其中整形这个变量一共32个比特位,其中高16位,低16位。
我们不管高16位,一个比特位要么0要么1,其中8-15是(退出状态码)后面的8位 全是0一旦后8位!0,异常退出,退出码毫无意义,那256就是2^8二的八次方=256。
的值正常终止的原因,如何获取呢?
(status>>8)&0xFF
:
-
子进程的终止状态信息存储在
status
变量中,它包含了多种信息。在 Linux 中,子进程正常退出时,其退出状态码存放在status
的低 8 位 。 -
status>>8
是将status
的值右移 8 位,这样原本存放在低 8 位的退出状态码就移到了最低 8 位。 -
&0xFF
是进行按位与操作,0xFF
即二进制的11111111
,与右移后的status
进行按位与操作,是为了只保留最低 8 位的值,也就是准确获取子进程的退出状态码 。通过打印这个值,可以知道子进程是以什么状态码退出的,有助于调试和了解子进程的执行结果。
问题二获取代码异常终止信号
所有信号
tatus&0x7F
:这是一个位运算表达式。在 Linux 中,子进程终止状态信息存储在 status
中,其中低 7 位(0x7F
即二进制的 01111111
)用于存储导致子进程终止的信号编号(如果子进程是因信号终止 )。通过与 0x7F
进行按位与操作,可以提取出这部分信号相关信息。如果子进程是正常退出(使用 exit
函数 ),那么这部分值为 0
;如果是因信号(如 SIGTERM
、SIGKILL
等 )终止,就能获取到对应的信号编号。
获取子进程退出码和特殊信号
刚刚在获取子进程推出码和特殊信号时,我们用的是位操作,但计算机里面的或者是它对应的为了配套的操作里面,他不想让你作为未操作提取,所以他给我们提供了若干个宏,这时候看宏它可以帮我们直接去提取
WEXITSTATUS(status)
: 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
WIFEXITED(status):
若为正常终⽌⼦进程返回的状态,则为真,否则异常。(查看进程是否是正常退出)
阻塞调用和非阻塞调用
想象你去餐厅点餐:
-
阻塞调用:就像你点完餐后,站在柜台前一直等着服务员把做好的餐递给你,期间啥也不干,啥也做不了,只能干等着。在这个过程中,你处于 “阻塞” 状态,一直被这件事绊住 。对应到编程里,比如父进程调用
wait
函数等待子进程结束,在子进程没结束前,父进程就卡在那里,不能去做其他事,这就是阻塞调用。
上面的例子都是阻塞调用
-
非阻塞调用:类似你点餐后,没在柜台干等着,而是去旁边找个位置坐下,玩手机或者和朋友聊天。你告诉服务员做好了喊你,然后你可以同时做其他事。在编程中,像用
waitpid
函数时带上WNOHANG
选项,父进程调用后,如果子进程没结束,它不会一直卡在那,而是马上返回去做自己其他的任务,这就是非阻塞调用 。
pid_ t waitpid(pid_t pid, int *status, int options);
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
-
非阻塞调用两种情况
1.在餐厅点餐之后 ,你什么都不干,一直询问服务员好了没。‘’
2.在餐厅等服务员叫你的同时你干些事情。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>// 定义函数指针类型
typedef void (*func_t)();#define NUM 10 // 假设函数指针数组的最大长度// 以下是任务函数定义
void Download()
{printf("我是一个下载的任务...\n");
}void Flush()
{printf("我是一个刷新的任务...\n");
}void Log()
{printf("我是一个记录日志的任务...\n");
}// 注册函数
void registerHandler(func_t h[], func_t f)
{int i = 0;for (; i < NUM; i++){if (h[i] == NULL)break;}if (i == NUM)return;h[i] = f;h[i + 1] = NULL;
}int main()
{func_t handlers[NUM] = {NULL};registerHandler(handlers, Download);registerHandler(handlers, Flush);registerHandler(handlers, Log);pid_t id = fork();if (id < 0){perror("fork");exit(EXIT_FAILURE);}if (id == 0){// 子进程int cnt = 3;while (1){sleep(3);printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());sleep(1);cnt--;if (cnt <= 0){break;}}exit(10);}// 父进程while (1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);if (rid > 0){if (WIFEXITED(status)){printf("wait success, rid: %d, exit code: %d, exit signal: %d\n",rid, WEXITSTATUS(status), status & 0x7F);}else{printf("子进程退出异常!\n");}break;}else if (rid == 0){// 函数指针进行回调处理int i = 0;for (; handlers[i]; i++){handlers[i]();}printf("本轮调用结束,子进程没有退出\n");sleep(1);}else{perror("waitpid");break;}}return 0;
}