进程等待是进程控制中非常重要的一环,这关系着多进程之间的联动。
目录
一.概念
二.wait
三.waitpid
(一). pid_t pid
(二). int* status ※
①退出码
②退出信号
③status结构
④获取退出码
⑤获取退出信号
(三). int options
①使用
②本质
(四). 返回值pid_t
四.拓展
进程等待是父进程等待子进程退出的过程。在等待中父进程可以选择阻塞或非阻塞状态。
阻塞状态:父进程等待过程中不进行任何操作,进入挂起状态,本质是将父进程PCB放入等待队列(wait_queue)。
非阻塞状态:父进程在等待过程中继续执行自己代码,即父进程一直处于运行状态。
进程等待常用的两个函数接口是wait与waitpid。
先看一下wait的定义:
值得注意的是wait是系统调用接口,linux环境下使用man指令即可查询相关信息。
$ man 2 wait
wait在使用时一般采用wait(NULL)形式即可,且父进程规定为阻塞等待。
因为wait无法指定子进程,当有子进程终止,父进程即会接收到相关信号。
至于status参数小编将在waitpid中进行详细介绍。
因此,在使用时,wait常常使用方式如下:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am child process, pid: %d\n", getpid());
sleep(3);
exit(0);
}
else
{
printf("I am father process, pid: %d\n", getpid());
wait(NULL);//阻塞等待子进程退出
printf("child is kill\n");
}
return 0;
}
首先看一下waitpid的定义(同样是使用man 2 wait指令进行查看):
需要注意,waitpid也是系统调用接口。头文件与wait一致。
头文件:
#include
#include
与wait不同的是,waitpid可以指定进程,也可以选择父进程等待时的状态(阻塞or非阻塞)。
小编将对参数逐一进行讲解。
当父进程创建多个子进程时,需要确定等待哪个子进程终止。
此处的pid作用是用来指定子进程的。因此pid值应为子进程的id。
当然,这里有多个可选项:
pid值 | 含义 |
>0 | 等待指定子进程。该pid值应为子进程id。 |
== -1 | 等待任意子进程。 |
== 0 | 等待任意子进程,必须同进程组。 |
< -1 | 等待指定子进程,必须同进程组。该pid值绝对值为子进程id。 |
使用方式:
pid_t id = fork();
if(id == 0)
{ ... }
else
{
waitpid(id, ..., ...);//等待指定子进程
waitpid(-1, ..., ...);//等待任意子进程
}
return 0;
}
status参数作用是接收子进程的退出码和退出信号。
退出码是指当子进程正常退出后,父进程用于判断子进程运行结果是否正确的方式。
比如在main函数中,return 0所代表的就是程序结果正确,返回0值给父进程作为退出码。
不同的退出码代表着不同的运行结果。可以使用strerror打印相应结果。
可以使用下列程序打印相应结果:
#include
#include
int main()
{
int i = 0;
for(i = 0; i < 255; i++)
{
printf("%d: %s\n", i, strerror(i));
}
return 0;
}
操作系统通过发送信号的方式让子进程在还没有运行完毕的情况下终止的信号,叫做退出信号。
即操作系统通过发送退出信号让子进程异常终止。
如果子进程正常终止,退出信号为0。
我们可以通过kill -l指令查看各退出信号含义:
kill -l
linux环境下,int有32bit位。按区域划分,其中低7位表示退出信号,第8位表示code dump,次低8位(9-16位)表示退出码。
其中code dump是当进程异常终止时,操作系统会将此时进程内存状态保存下来,用于日后分析调试使用,即内存快闪。
结构如图:
当子进程正常运行而终止时,退出码看子进程return/exit值,退出信号为0 。
当子进程异常终止时,退出码不启用(随机值),退出信号为相应kill信号。
有两种获得退出码的方式。
一是调用WIFEXITED和WEXITSTATUS函数。
WIFEXITED(status)用于判断子进程是否正常退出。正常终止返回值非0,异常终止返回值为0。
WEXITSTATUS(status)当子进程正常终止时,用于提取子进程退出码。
使用方式如下:
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0) { ... }
else
{
int status = 0;
waitpid(id, &status, ...);
if(WIFEXITED(status))
{
printf("退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
二是通过位操作获取。
因为退出码在次低8位,因此可以通过将其移至低八位再与0xFF(1111 1111)按位与获得。
(status >> 8) & 0xFF
上方代码就可改为:
if(id == 0) { ... }
else
{
int status = 0;
waitpid(id, &status, ...);
...//判断子进程退出情况,退出码中会讲解
printf("退出码:%d\n", (status>>8) & 0xFF);
}
获取退出信号很简单,只需要将status与0x7F(0111 1111)按位与。
即获得status低7位值。
if(id == 0) { ... }
else
{
int status = 0;
waitpid(id, &status, ...);
...//判断为子进程异常退出
printf("退出信号:%d\n", status & 0x7F);
}
这个参数用于确定父进程等待状态。阻塞or非阻塞
常用参数有两个:0和WNOHANG。
当参数为0时,父进程为阻塞状态。
当参数为WNOHANG时,父进程为非阻塞状态。
int main()
{
pid_t id = fork();
if(id == 0) {
sleep(2);
}
else//父进程
{
int status = 0;
pid_t ret = 0;//接收waitpid返回值
if(ret == 0)//子进程未退出,返回值中会说明
{
ret = waitpid(id, &status, WNOHANG);//非阻塞等待
printf("still wait child, but i can run\n");
sleep(1);
}
printf("child exit\n");//子进程已退出
}
return 0;
}
本质上,在waitpid内部会对options进行判断。
当值为0时,将进程挂起,从而父进程进入阻塞状态,然后死循环判断子进程是否返回,直到子进程终止或等待失败(无此子进程)时,将父进程唤醒,确定返回值,结束waitpid。
当值为WNOHANG时,根据当前子进程状态确定返回值,结束waitpid。
通俗一点,就是值为0时会死循环判断子进程状态,值为WNOHANG时只会判断一次。
因此,当使用非阻塞等待时通常需要对waitpid循环执行。
waitpid返回值共有三种可能。
返回值 | 含义 |
>0 | 子进程已退出 |
==0 | 子进程未退出(用于WNOHANG) |
<0 | 子进程等待失败(没有该子进程...) |
由此,我们可以将options中代码再完善一下 :
int main()
{
pid_t id = fork();
if(id == 0) {
sleep(2);
}
else//父进程
{
int status = 0;
pid_t ret = 0;//接收waitpid返回值
if(ret == 0)//子进程未退出,返回值中会说明
{
ret = waitpid(id, &status, WNOHANG);//非阻塞等待
printf("still wait child, but i can run\n");
sleep(1);
}
if(ret > 0) printf("child exit\n");//子进程已退出
else{
printf("wait false!\n");//ret < 0
}
}
return 0;
}
父进程所得到的退出信号、退出码等数据,属于子进程的数据,且子进程已经终止,父进程无法从子进程处直接得到。
但是子进程的PCB结构体还在操作系统中。退出信号、退出码保留在PCB的exit_signal、exit_code中。
之所以wait、waitpid是系统调用接口,就是因为需要从操作系统处得到子进程PCB内的这些数据。
也正因如此,当子进程退出时,会把自己的退出信号写入exit_signal,wait、waitpid才得以知道子进程已经退出。
控制复杂性是计算机编程的本质——Brian Kernigan
如有错误,敬请斧正