等待子进程终止

等待子进程终止

可以通过信号通知父进程,但是很多父进程想知道关于子进程终止的更多信息——比如子进程的返回值

如果终止时,子进程就完全消失了,父进程就无法获取关于紫禁城的任何信息。所以UNIX的最初设计者们做了这样的决定:如果子进程在父进程之前结束,内核应该把该子进程设置成特殊的进程状态。出于这种状态的进程称为僵尸(zombie)进程。

僵尸进程只保留最小的概要信息——一些基本内核数据结构,保存可能有用的信息。僵尸进程会等待父进程来查询自己的状态(这个过程称为在僵尸进程上的等待)。只有当父进程获取到了已终止的子进程的信息,这个子进程才会正式消失,不在处于僵尸状态

Linux内核提供了一些接口,可以获取已终止子进程的信息。其中最简单的时wait():

1
2
3
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

调用成功会返回已终止子进程的pid,出错时,返回-1.如果没有子进程终止,调用会阻塞,直到有一个子进程终止。如果有个子进程已经终止,调用会立即返回。因此当得到子进程终止信息后,调用wait()就会立即返回,不会被阻塞

出错时,errno有两种可能的值:

ECHILD : 调用进程没有任何子进程

EINTR : 在等待子进程结束时收到信号,调用提前返回

如果status指针不是NULL,那它包含了关于子进程的一些其他信息,由于POSIX郧西实现可以根据需要给status定义一些合适的比特位来表示附加信息,POSIX标准提供了一些宏来解释status参数:

1
2
3
4
5
6
7
8
9
10
11
#include <sys/wait.h>

int WIFEXITED (status);
int WIFSIGNALED (status);
int WIFSTOPPED (status);
int WIFCONTINUED (status);

int WEXITSTATUS (status);
int WTERMSIG (status);
int WSTOPSIG (status);
int WCOREDUMP (status);

前两个宏可能会返回真(一个非0值),这取决于子进程的结束情况。如果进程正常结束了——也就是调用了_exit(),第一个宏WIFEXITED就会返回真。在这种情况下WEXITSTATUS会返回status的低八位,并传递给_exit()函数

如果信号导致进程终止,WIFSIGNALED会返回真。在这种情况下,WTERMSIG会返回导致进程终止的信号编号。如果进程收到信号时生成core,WCOREDUMP就会返回true。虽然很多UNIX系统都支持WCOREDUMP,但POSIX并没有定义它

当子进程停止或继续执行时WIFSTOPPED和WIFCONTINUED会分别返回真。当前,进程状态是通过系统调用ptrace()跟踪。只有当实现了调试器时,这些状态才可用。

通常情况下,wait()仅用于获取子进程的终止信息。如果WIFSTOPPED返回真,WSTOPSIG就返回使进程终止的信号编号。虽然POSIX没有定义WIFCONTINUED,但是新的标准为waitpid()函数定义了这个宏。正如在2.6.10内核中,Linux也为wait()函数提供了这个宏

实例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main (void)
{
int status;
pid_t pid;

if (!fork())
{
//abort();
return 1;
}

pid = wait (&status);

if (pid == -1)
{
perror ("wait");
}

if (WIFEXITED (status))
{
printf ("Normal termination with exit status=%d\n",WEXITSTATUS (status));
}

if (WIFSIGNALED (status))
{
printf ("Kill by signal=%d%s\n",WTERMSIG (status), WCOREDUMP (status)?"dumped core" : "");
}

if (WIFSTOPPED (status))
{
printf ("Stopped by signal=%d\n",WSTOPSIG (status));
}

if (WIFCONTINUED (status))
{
printf ("Contiued\n");
}

return 0;
}

创建子进程立即退出,随后父进程调用wait()来获取进程状态。会有如下打印:

Normal termination with exit status=1

如果调用abort(),会给自己发送一个SIGABRT信号,会有如下打印:

Kill by signal=6

等待特定进程

如果知道需要等待的进程的pid,可以使用系统调用waitpid()来等待特定的进程:

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid (pid_t pid, int *status, int options);

比起wait(),waitpid()是一个更强大的系统调用。它额外的参数可以支持细粒度调整

参数pid指定要等待的一个或多个进程的pid.它的值必须是下面四种情况之一:

参数pid 行为
<-1 等待一个指定进程组中的任何子进程退出,该进程组的id等于pid的绝对值
-1 等待任何一个子进程退出,行为和wait()一致
0 等待同一个进程组中的任何子进程
>0 等待进程pid等于pid的子进程

status的作用和wait()函数的唯一参数是一样的,而且之前的宏可以继续使用

参数options是0个或多个以下选项按二进制“或”运算的结果:

WNOHANG : 不要阻塞,如果要等待的子进程还没结束、停止或继续运行,会立即返回

WUNTRACED : 如果设置该位,即使调用进程没有跟踪子进程,也会设置返回调用参数中的WIFSTOPPED位。和标志位WUTRACED一样,这个标志位可以用来实现更通用的作业控制,如shell

WCONTINUED : 如果设置该位,即使是调用进程没有跟踪子进程,也会设置返回调用参数中的WIFCONTINUED位

返回值

调用成功时,waitpid()会返回状态发生改变的那个进程的pid。如果设置了WNOHANG并且等待的子进程没有发生状态改变,返回0,出错时返回-1,并设置errno值为下三个之一:

ECHILD : 参数pid所指定的进程不存在,或者不是调用进程的子进程

EINTR : 没有设置WNOHANG,在等待时收到了一个信号

EINVAL : 参数options非法

例程

作为一个例子,假设程序期望获得pid为1742的子进程的返回值,如果该子进程没有结束,父进程就立即返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int status;
pid_t pid;
pid = waitpid (1742, &status, WNOHANG);

if (pid == -1)
{
perror ("waitpid");
}
else
{
printf ("pid = %d\n",pid);

if (WIFEXITED (status))
{
printf ("Normal termination with exit status=%d\n",WEXITSTATUS (status));
}

if (WIFSIGNALED (status))
{
printf ("Kill by signal=%d%s\n",WTERMSIG (status), WCOREDUMP (status)?"dumped core" : "");
}
}

等待子进程的其他方法

对于某些应用,它们希望有更多等待子进程的方式。XSI扩展了POSIX,而Linux提供了waitid():

1
2
#include <sys/wait.h>
int waitid (idtype_t idtype, id_t id, siginfo_t *infop, int options);

和wait()、waitpid()一样,waitid()是用于等待子进程结束并获取其状态变化的信息。waitid()提供了更多的选项,但是其代价是复杂性变高

类似于waitpid(),waitid()支持开发人员指定等待哪个子进程,但waitid()需要两个参数,而不是一个。参数idtype和id是指定要等待哪个子进程,idtype的值是下面三个中的一个:

P_PID : 等待pid值是id的子进程

P_GID : 等待进程组是id的那些子进程

P_ALL : 等待所有子进程,参数id被忽略

参数id的类型是id_t,这个类型很少见,他代表着一种通用的ID号。由于将来可能会增加idtype值,所以引入这个类型,这样新加入的idtype值也可以很容易表示。id_t类型足够大,可以保证能存储任何类型的pid_t值,可以直接把它当作pid_t来使用

参数options是以下一个或多个选项值进行二进制”或“运算的结果:

WEXITED : 调用会等待结束的子进程

WSTOPPED : 调用进程会等待因收到信号而停止执行的子进程

WNOHANG : 不要阻塞,如果要等待的子进程还没结束、停止或继续运行,会立即返回

WNOWAIT : 调用进程不会删除相应子进程的僵尸状态。将来可能会继续等待处理僵尸进程

WCONTINUED : 调用进程会等待因收到信号而继续执行的进程

成功时,waitid()会填充参数infop,infop指向一个合法的siginfo_t类型。siginfo_t结构体的具体成员变量与实现相关的,但在调用waitpid()之后,有一些成员变量就生效了。也就是说一次成功的调用可以保证下面的成员会被赋值:

si_pid : 子进程的pid

si_uid : 子进程的uid

si_code : 根据子进程的状态时终止/被信号所杀死、停止或者继续执行而分别设置为CLD_EXITED、CLD_KILLED、CLD_STOPPED或CLD_CONTIUED

si_signo : 设置为SIGCHLD

si_status : 如果si_code是CLD_EXITED,该变量是子进程的退出值。否则该变量是导致状态改变的那个信号编码

返回和出错处理

当成功时,waitid()返回0.出错时返回-1并设置errno:

ECHLD : 有id和idtype确定的进程不存在

EINTR : 在options里没有设置WNOHANG,而且一个信号打断了子进程的执行

EINVAL : options参数不合法,或者id和idtype的组合不合法

和wait()、waitpid()相比,waitid()提供了更多有用的语义定义功能。特别地,从结构体siginfo_t获取的信息可能是很有用的。如果不需要这些信息,那么就应该选择更简单的函数,这样可以被更多系统所支持,并且可以更容易的移植到非Linux系统上

创建并等待新进程

ANSI和POSIX都定义了一个用于创建新进程并等待它结束的函数——可以把它想象成是同步创建进程。如果一个进程创建了新进程并立刻开始等待它的结束,那么就很适合使用下面这个接口:

1
2
3
4
#define _XOPEN_SOURCE
#include <stdlib.h>

int system (const char *command);

调用system()会执行参数command所提供的命令,而且还可以为该命令指定参数。“/bin/sh -c”会作为前缀加到command参数前面。通过这种方式,再把整个命令传递给shell

成功时,返回值是执行command命令所得到的返回状态,该状态和执行wait()所获取的状态一致。因此可以通过WEXITSTATUS获取执行command命令的返回值。如果调用/bin/shell本身失败了,那么从WEXITSTATUS返回的值和调用exit(127)的返回值是一样的

实现一个简单的system()

利用fork()、exec系统调用和waitpid()实现一个system():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int my_system(const char *cmd)
{
int status;
pid_t pid;

pid = fork ();
if (pid == -1)
{
return -1;
}
else if (pid ==0)
{
const char *argv[4];

argv[0] = "sh";
argv[1] = "-c";
argv[2] = cmd;
argv[3] = NULL;
execv ("/bin/sh",argv);

exit(-1);
}

if (waitpid (pid, &status, 0) == -1)
{
return -1;
}
else if (WIFEXITED (status))
{
return WEXITSTATUS (status);
}

return -1;
}

僵尸进程

一个进程已经终止了,但是它的父进程还没获取到其状态,那么这个进程就叫做僵尸进程。僵尸进程还会消耗一些系统资源,虽然消耗很少——仅仅够描述进程之前状态的一些概要信息。保留这些概要信息主要是为了在父进程查询子进程状态时可以提供相应的信息。一旦父进程得到了想要的信息,内核就会清除这些信息,僵尸进程就不存在了。

如果创建了一个子进程,那么它就有责任去等待子进程,即使会丢弃得到的子进程信息。否则,如果父进程没有等待子进程,其所有子进程就会成为僵尸进程,并一直存在


等待子进程终止
https://carl-5535.github.io/2021/04/28/Linux系统编程/等待子进程终止/
作者
Carl Chen
发布于
2021年4月28日
许可协议