一. 僵尸进程
正如前面所提到的那样, 一个进程已经终止了, 但是它的父进程还没有获取到其退出信息, 那么这个进程就叫做僵尸进程. 僵尸进程还会消耗一些系统资源, 虽然消耗很少, 仅仅够描述进程之前状态的一些概要信息. 保留这些概要信息主要是为了在父进程查询子进程的状态时可以提供相应的信息. 一旦父进程得到了想要的信息, 内核就会清除这些信息, 僵尸进程就不存在了.
例如, 对于以下代码, fork()
创建的子进程在打印 5 次信息后会退出, 而父进程会一直打印信息. 也就是说, 子进程退出了, 父进程还在运行, 但父进程没有读取(等待)子进程的退出信息, 那么此时子进程就成为了僵尸进程.
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if (id == 0) { // child process
int count = 5;
while (count) {
printf("I am child process......PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child process quit\n");
exit(0);
}
else if (id > 0) { // father process
while (1) {
printf("I am father process.....PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else { // fork error
}
return 0;
}
运行生成的可执行程序后, 可以通过以下监控脚本, 每隔一秒对父子进程的信息进行检测.
bash
while :; do ps ajx | head -1 && ps ajx | grep test | grep -v grep; echo "#######################"; sleep 1; done
检测后即可发现, 当子进程退出后, 子进程的状态就变成了僵尸状态.

所以, 如果进程创建了一个子进程, 那么它就有责任去等待子进程, 即使会丢弃得到的子进程信息.
二. 孤儿进程
然而, 如果父进程在子进程结束之前就结束了呢? 或者父进程还没有机会等待其僵尸的子进程, 就先结束了呢? 一个父进程退出, 而它的一个或多个子进程还在运行, 那么这些子进程将成为孤儿进程. 无论何时, 只要有进程结束了, 内核就会遍历它的所有子进程, 并且把它们的父进程重新设为 init 进程, (即 pid 为 1 的那个进程). 这就保证了系统中不存在没有父进程的进程. init 进程会周期性地等待所有子进程, 确保不会有长时间存在的僵尸进程. 因此, 当父进程在子进程之前结束, 或者在退出前没有等待子进程, 那么 init 进程会被指定为这些子进程的父进程, 从而确保了这些子进程在将来退出的时候不会成为僵尸进程.
例如, 对于以下代码, fork()
创建的子进程会一直打印信息, 而父进程在打印 5 次信息后会退出, 此时该子进程就变成了孤儿进程, 会被 init 进程领养, 此后当该孤儿进程终止时就由 init 进程进行处理回收, 不会成为僵尸进程.
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if (id == 0) { // child process
while (1) {
printf("I am child process......PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else if (id > 0) { // father process
int count = 5;
while (count) {
printf("I am father process.....PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father process quit\n");
exit(0);
}
else { // fork error
}
return 0;
}
运行生成的可执行程序后, 可以通过以下监控脚本, 每隔一秒对父子进程的信息进行检测.
bash
while :; do ps ajx | head -1 && ps ajx | grep test | grep -v grep; echo "#######################"; sleep 1; done
检测后即可发现, 在父进程未退出时, 子进程的 PPID 就是父进程的 PID, 而当父进程退出后, 子进程的 PPID 就变成了 1, 即子进程被 init 进程领养了.

三. 进程等待的必要性
-
子进程退出, 父进程如果不读取子进程的退出信息, 子进程就会变成僵尸进程, 进而造成内存泄漏.
-
进程一旦变成僵尸进程, 那么就算是
kill -9
命令也无法将其杀死, 因为谁也无法杀死一个已经死去的进程. -
父进程需要通过进程等待的方式, 回收子进程资源, 获取子进程的退出信息, 进而了解自己指派给子进程任务的完成情况.
四. 获取子进程 status
进程等待所使用的两个函数 wait()
和 waitpid()
, 都有一个 status 参数, 该参数是一个输出型参数, 由操作系统进行填充. 如果对 status 参数传入 NULL, 表示不关心子进程的退出状态信息. 否则, 操作系统会通过该参数将子进程的退出信息反馈给父进程.
status 是一个无符号整型变量, 但 status 不能简单的当作整型来看待 (可以当作位图来看待), status 的不同比特位所代表的信息不同, 具体细节如下 (只研究 status 低 16 比特位):

在 status 的低 16 位当中, 高 8 位表示进程的退出状态 (进程正常终止), 即退出码; 低 7 位表示使进程终止的信号编号(进程被信号所杀, 即进程异常终止), 而第 8 位是 core dump 标志位 (后期谈论进程信号时再谈论这一话题).

通过一系列位操作, 就可以根据子进程 status 得到进程的退出码和退出信号.
c
ExitCode = (status >> 8) & 0xFF; // 获取子进程退出码
ExitSignal = status & 0x7F; // 获取子进程退出信号
注意: 当一个进程异常终止时, 说明该进程是被信号所杀, 则该进程的退出码也就没有意义了.
终止状态用定义在 <sys/wait.h>
中的各个宏来查看. 有 4 个互斥的宏可用来取得进程终止的原因, 它们的名字都以 WIF 开始. 基于这 4 个宏中哪一个值为真, 就可选用其他宏来取得退出状态, 信号编号等.

上述宏函数的定义, 位于 /usr/inlcude/sys/wait.h
中.
c
/* This will define all the `__W*' macros. */
# include <bits/waitstatus.h>
# define WEXITSTATUS(status) __WEXITSTATUS (__WAIT_INT (status))
# define WTERMSIG(status) __WTERMSIG (__WAIT_INT (status))
# define WSTOPSIG(status) __WSTOPSIG (__WAIT_INT (status))
# define WIFEXITED(status) __WIFEXITED (__WAIT_INT (status))
# define WIFSIGNALED(status) __WIFSIGNALED (__WAIT_INT (status))
# define WIFSTOPPED(status) __WIFSTOPPED (__WAIT_INT (status))
# ifdef __WIFCONTINUED
# define WIFCONTINUED(status) __WIFCONTINUED (__WAIT_INT (status))
# endif
#endif /* <stdlib.h> not included. */
#ifdef __USE_BSD
# define WCOREFLAG __WCOREFLAG
# define WCOREDUMP(status) __WCOREDUMP (__WAIT_INT (status))
# define W_EXITCODE(ret, sig) __W_EXITCODE (ret, sig)
# define W_STOPCODE(sig) __W_STOPCODE (sig)
#endif
上述宏函数的实现, 位于 /usr/inlcude/bits/waitstatus.h
中.
c
/* If WIFEXITED(STATUS), the low-order 8 bits of the status. */
#define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
/* If WIFSIGNALED(STATUS), the terminating signal. */
#define __WTERMSIG(status) ((status) & 0x7f)
/* If WIFSTOPPED(STATUS), the signal that stopped the child. */
#define __WSTOPSIG(status) __WEXITSTATUS(status)
/* Nonzero if STATUS indicates normal termination. */
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)
/* Nonzero if STATUS indicates termination by a signal. */
#define __WIFSIGNALED(status) \
(((signed char) (((status) & 0x7f) + 1) >> 1) > 0)
/* Nonzero if STATUS indicates the child is stopped. */
#define __WIFSTOPPED(status) (((status) & 0xff) == 0x7f)
/* Nonzero if STATUS indicates the child continued after a stop. We only
define this if <bits/waitflags.h> provides the WCONTINUED flag bit. */
#ifdef WCONTINUED
# define __WIFCONTINUED(status) ((status) == __W_CONTINUED)
#endif
/* Nonzero if STATUS indicates the child dumped core. */
#define __WCOREDUMP(status) ((status) & __WCOREFLAG)
五. 进程等待方法 wait() && waitpid()
如果终止时, 子进程完全消失了, 父进程就无法获取关于子进程的任何信息. 所以, UNIX 的最初设计者们做了这样的决定: 如果子进程在父进程之前结束, 内核应该把该子进程设置成特殊的进程状态. 处于这种状态的进程成为僵尸进程. 僵尸进程只保留最小的概要信息, 一些基本内核数据结构, 保存可能有用的信息, 比如子进程退出码以及子进程的退出信号 (存放在task_struct中)

僵尸进程会等待父进程来查询自己的状态 (这个过程称为在僵尸进程上等待). 只有当父进程获取到了已终止的子进程的信息, 这个子进程才会正式消失, 不再处于僵尸状态.
Linux 内核提供了一些接口, 可以获取已终止子进程的信息. 其中较为常用的两个为 wait()
, waitpid()
.

wait()
函数原型:
pid_t wait(int* status);
作用: 等待任意子进程.
返回值: 等待成功: 如果有子进程已经终止, 并且是一个僵尸进程, 则返回被等待子进程的 PID; 否则使其调用者 (父进程) 阻塞, 直到有子进程终止; 等待失败: 返回 -1.
参数: 输出型参数, 获取子进程的退出信息, 不关心可设置为 NULL.
例如, fork()
创建子进程后, 父进程可使用 wait()
阻塞等待子进程, 直到子进程退出后读取该子进程的退出信息.
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0) { // child process
int count = 5;
while (count) {
printf("I am child process.....PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child process quit\n");
exit(0);
}
// father process
int status = 0;
pid_t ret = wait(&status);
if (ret > 0) {
// wait success
printf("wait child process success\n");
if (WIFEXITED(status)) {
// exit normal
printf("child process exit code:%d\n", WEXITSTATUS(status));
}
}
sleep(3);
printf("father process quit\n");
return 0;
}
运行生成的可执行程序后, 可以通过以下监控脚本, 每隔一秒对父子进程的信息进行检测.
bash
while :; do ps ajx | head -1 && ps ajx | grep test | grep -v grep; echo "#######################"; sleep 1; done
这时我们可以看到, 当子进程退出后, 父进程读取了子进程的退出信息, 子进程也就不会变成僵尸进程了.

waitpid()
函数原型:
pid_t waitpid(pid_t pid, int* status, int options);
作用: 等待指定子进程.
返回值: 三种情况
- 如果 options 设为 0, 若等待的子进程没有退出, 父进程阻塞等待; 若等待的子进程已经终止, 则返回该子进程的 PID.
- 如果 options 设为
WNOHANG
, 若等待的子进程没有退出, 直接返回 0, 不予以等待; 若等待的子进程已经终止, 则返回该子进程的 PID. - 如果调用中出错, 则返回 -1.
参数: 三个参数, pid, status, options
- pid: 待等待子进程的 pid, 若设置为 -1, 则等待任意子进程.
- status: 输出型参数, 获取子进程的退出信息, 不关心可设置为 NULL.
- options: 当设置为
WNOHANG
时, 若等待的子进程没有退出, 直接返回 0, 不予以等待. 若等待的子进程已经终止, 则返回该子进程的 PID.
例如, fork()
创建子进程后, 子进程一直打印信息, 父进程可使用 waitpid()
一直等待子进程(此时将 waitpid()
的第三个参数设置为 0), 直到子进程退出后读取子进程的退出信息 (此时用 kill -9
杀死子进程)
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0) { // child process
while (1) {
printf("I am child process.....PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
// father process
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) {
// wait success
printf("wait child process success\n");
if (WIFEXITED(status)) {
// child process exit normal
printf("child process exit code:%d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) {
// child process killed by signal
printf("child process killed by siganl %d\n", WTERMSIG(status));
}
}
sleep(3);
printf("father process quit\n");
return 0;
}
在父进程运行过程中, 使用 kill -9
命令将子进程杀死, 这时父进程也能等待子进程成功.

注意: 当一个进程异常终止时, 说明该进程是被信号所杀, 则该进程的退出码也就没有意义了.
多进程创建与等待
以上代码所演示的都是父进程创建以及等待一个子进程的例子, 实际上还可以同时创建多个子进程, 然后让父进程依次等待子进程退出, 这叫做多进程创建以及等待.
例如, 以下代码中同时创建了 10 个子进程, 同时将子进程的 pid 放入到 ids 数组当中, 并将这 10 个子进程退出时的退出码设置为该子进程 pid 在数组 ids 中的下标, 之后父进程再使用 waitpid()
等待指定的这 10 个子进程.
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t ids[10];
for (int i = 0; i < 10; i++) {
pid_t id = fork();
if (id == 0) {
// child process
printf("child process created successfully.....PID:%d\n", getpid());
sleep(3);
exit(i); // 将子进程的退出码设置为该子进程 PID 在数组 ids 中的下标
}
// father process
ids[i] = id;
}
for (int i = 0; i < 10; i++) {
int status = 0;
pid_t ret = waitpid(ids[i], &status, 0);
if (ret > 0) {
// wait child success
printf("wait child process success, PID:%d\n", ids[i]);
if (WIFEXITED(status)) {
// child process exit normal
printf("child process exit code:%d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status)) {
// child process killed by signal
printf("child process killed by siganl %d\n", WTERMSIG(status));
}
}
}
return 0;
}
运行生成的可执行程序, 可以看到父进程同时创建多个子进程, 每一个被新创建的子进程三秒后退出, 父进程再依次读取这些子进程的退出信息.

非阻塞轮询
上述所给例子中, 当子进程未退出时, 父进程都在一直等待子进程退出, 在等待期间, 父进程不能做任何事情, 这种等待叫做阻塞等待.
实际上我们可以让父进程不要一直等待子进程退出, 而是当子进程未退出时父进程可以做一些自己的事情, 当子进程退出时再读取子进程的退出信息, 即非阻塞等待.
做法很简单, 向 waitpid()
的第三个参数传入 WNOHANG
, 这样一来, 等待的子进程若是没有结束, 那么 waitpid()
将直接返回 0, 不予以等待; 而等待的子进程若已经终止, 则返回该子进程的 PID.
例如, 父进程可以隔一段时间调用一次 waitpid(
), 若是等待的子进程尚未退出, 则父进程可以先去执行其他的任务, 过一段时间再调用 waitpid()
读取子进程的退出信息.
js
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0) {
// child process
int count = 3;
while (count) {
printf("child process do his things.....PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(3);
count--;
}
exit(0);
}
// father process
while (1) {
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0) {
printf("wait child process success\n");
printf("child process exit code:%d\n", WEXITSTATUS(status));
break;
}
else if (ret == 0) {
printf("father process do his things.....\n");
sleep(1);
}
else {
printf("waitpid error...\n");
break;
}
}
return 0;
}
运行结果就是, 父进程每隔一段时间去查看子进程是否退出, 若未终止, 则父进程先去执行自己的任务, 过一段时间再来查看子进程是否退出, 直到子进程退出后读取子进程的退出信息.
