lienhua34
2014-10-12

当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD信号。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。

在文档“进程控制三部曲”中,我们讲的第三部曲是使用 wait 函数来获取终止子进程的终止状态。那么,有几个问题我们这里需要详细的学习一下。

1. 父进程一定能够获取到子进程的终止状态吗?如果子进程在父进程调用 wait 函数前就终止了,怎么办?

2. 如果父进程没有获取子进程的终止状态,那会发生什么?

3. 如果父进程有多个子进程,那么获取的是哪个子进程的终止状态呢?

对于第一个问题的回答是:内核为每个终止进程保存了一定量的信息,包括进程 ID、该进程的终止状态、以及该进程使用的 CPU 时间总量。所以,当终止进程的父进程调用 wait 或者 waitpid 函数,即可获取到这些信息。当父进程获取终止进程的终止信息之后,内核就可以释放终止进程所使用的所有存储区、关闭其所有打开的文件。

在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的相关信息)的进程被称为僵尸进程(zombie)。如果编写一个长期运行的程序,调用 fork 产生子进程之后,需要调用 wait 来获取这些子进程的终止状态,否则这些子进程在终止之后将会变成僵尸进程。(后面会讲到用一个技巧以避开父进程调用 wait 获取所有子进程的终止状态。)

那么如果那些被 init 进程领养的进程在终止之后会不会也变成僵尸进程?答案是:不会。因为 init 进程无论何时只要有一个子进程终止,init 就会调用 wait 函数获取其终止状态。

那么关于上面的第三个问题,我们得通过详细学习 wait 和 waitpid 函数才能都做出回答了。

1 wait 函数

#include <sys/wait.h>

pid_t wait(int *statloc);

返回值:若成功则返回终止进程的ID,若出错则返回-1

参数 statloc 是一个整形指针。如果 statloc 不是一个空指针,则终止进程的终止状态将存储在该指针所指向的内存单元中。如果不关心终止状态,可以将 statloc 参数设置为空。

调用 wait 函数时,调用进程将会出现下面的情况:

• 如果其所有子进程都还在运行,则阻塞。

• 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回。

• 如果没有任何子进程,则立即出错返回。

wait 函数获取的终止状态是一个 int 型数值,那我们如何得到具体的终止信息呢?POSIX.1 规定终止状态用定义在 <sys/wait.h> 中的各个宏来参看。有四个互斥的宏可以用来得到进程的终止原因。这四个宏见表 1,

表 1: 检查终止状态的宏
说明
WIFEXITED(status) 若正常终止子进程返回的状态,则为真。此种情况,调用 WEXITSTATUS(status) 可以获取子进程调用 exit 函数的参数的低 8位。
WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真。此种情况,调用 WTERMSIG(status) 取得使子进程终止的信号编号。
WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真。
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。

下面我们来看一下打印终止进程状态说明的例子,

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h> extern void print_exit(int status); int
main(void)
{
pid_t pid;
int status; if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
exit();
}
if (wait(&status) != pid) {
printf("wait error: %s\n", strerror(errno));
exit(-);
}
print_exit(status); if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
abort();
}
if (wait(&status) != pid) {
printf("wait error: %s\n", strerror(errno));
exit(-);
}
print_exit(status); exit();
} void print_exit(int status)
{
if (WIFEXITED(status)) {
printf("normal termination, exit status = %d\n",
WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("abnormal termination, signal number =%d\n",
WTERMSIG(status));
}
}

waitdemo.c

编译该程序,生成并运行 waitdemo 文件,

lienhua34:demo$ gcc -o waitdemo waitdemo.c
lienhua34:demo$ ./waitdemo
normal termination, exit status =
abnormal termination, signal number =

下面我们再来看一个产生僵尸进程的示例,

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h> int
main(void)
{
pid_t pid; if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
exit();
}
printf("fork child process:%d\n", pid); if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
exit();
}
printf("fork child process:%d\n", pid); if ((pid = wait(NULL)) < ) {
printf("wait error: %s\n", strerror(errno));
exit(-);
}
printf("get child process(%d) termination status\n", pid);
sleep();
printf("parent process exit\n");
exit();
}

zombiedemo.c

我们在父进程最后 sleep(5) 让父进程睡眠 5 秒钟是避免父进程太早退出,我们观察不到僵尸进程。我们编译该程序文件,生成并执行文件

lienhua34:demo$ ps -A -ostat,pid | grep -e '[Zz]'
Z
lienhua34:demo$ gcc -o zombiedemo zombiedemo.c
lienhua34:demo$ ./zombiedemo &
[]
lienhua34:demo$ fork child process:
fork child process:
get child process() termination status
ps -A -ostat,pid | grep -e '[Zz]'
Z
Z
lienhua34:demo$ parent process exit
ps -A -ostat,pid | grep -e '[Zz]'
Z
[]+ 完成 ./zombiedemo

ps 命令打印的进程中,Z 表示僵尸进程。从上面的运行结果,我们看到父进程(ID:2961)fork 了两个子进程(ID:2962 和 2963),然后调用了 wait 函数获取了子进程 2963 的终止状态,于是子进程 2962 便成为了僵尸进程。但是,当父进程也退出时,生成僵尸进程的子进程 2962 也被内核释放。

2 waitpid 函数

只要有一个子进程终止,wait 函数就会返回。那么如果父进程希望等待特定的子进程终止,该怎么办?UNIX 提供了提供这样功能的 waitpid 函数。

#include <sys/wait.h>

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

返回值:若成功则返回终止进程ID或0;若出错则返回-1

其中 statloc 参数跟 wait 函数一样,获取终止子进程的状态信息。waitpid 函数通过 pid 参数来控制父进程希望获取特定进程的终止状态信息,

• pid==-1:等待任一子进程,与 wait 函数等效。

• pid>0:等待其进程 ID 与 pid 相等的子进程。

• pid==0:等待其组 ID 等于调用进程组 ID 的任一子进程。(我们这里不学习进程组)

• pid<-1:等待其组 ID 等于 pid 绝对值的任一子进程。

waitpid 函数返回终止子进程的进程 ID。如果指定的进程或进程组不存在,或者参数 pid 指定的进程不是调用进程的子进程则都将出错。waitpid 函数跟 wait 函数的另一个不同之处在于,wait 函数可能会使调用进程阻塞,而 waitpid 函数可以通过第三个参数 options 来控制调用进程是否要阻塞。options 参数可以是 0,也可以是表 2 中各常量或运算的结果。

表 2: waitpid 的 options 常量
常量 说明
WCONTINUED 若实现支持作业控制,那么由 pid 指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态。
WNOHANG 若由 pid 指定的子进程并不是立即可用的,则 waitpid 不阻塞,此时返回值为 0.
WUNTRACED 若某实现支持作业控制,而由 pid 指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。

关于 options 用于作业控制 的两个 常量 WCONTINUED 和 WUNTRACED,我们这里不学习,我们只关心常量 WNOHANG。我们来看一个例子。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h> int
main(void)
{
pid_t pid1, pid2;
pid_t waitpidRes; if ((pid1 = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid1 == ) {
sleep();
printf("child process %d exit\n", getpid());
exit();
} if ((pid2 = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid2 == ) {
printf("child process %d exit\n", getpid());
exit();
} if ((waitpidRes = waitpid(pid1, NULL, )) == pid1) {
printf("get terminated child process %d.\n", waitpidRes);
} else if (waitpidRes < ) {
printf("waitpid error: %s\n", strerror(errno));
exit(-);
} else {
printf("waitpid return 0\n");
}
printf("parent process exit\n"); exit();
}

waitpiddemo.c

在上面的程序中,我们在第一个子进程中 sleep(3) 让该子进程睡眠 3秒,以便在父进程调用 waitpid 函数时该子进程尚未结束。编译该程序,生成并执行 waitpiddemo 文件,

lienhua34:demo$ gcc -o waitpiddemo waitpiddemo.c
lienhua34:demo$ ./waitpiddemo
child process exit
child process exit
get terminated child process .
parent process exit

从上面的运行结果,我们可以看到父进程阻塞等待子进程 2971 终止。我们如果把上面程序的 waitpid 函数第三个参数 options 改为 WNOHANG,看一下其实际运行结果。

lienhua34:demo$ gcc -o waitpiddemo waitpiddemo.c
lienhua34:demo$ ./waitpiddemo
waitpid return
parent process exit
child process exit
lienhua34:demo$ child process exit

从上面的运行结果,我们可以看出父进程调用 waitpid 函数时,子进程2981 尚未终止,于是 waitpid 函数没有阻塞父进程,直接返回 0.

3 避免调用大量WAIT函数来防止僵尸进程的技巧

前面讲到僵尸进程时,我们提到要编写一个长期运行的程序,要避免出现大量的僵尸情况,就需要每次 fork 出一个子进程时都需要调用 wait 函数来等待子进程的结束以便处理该子进程的终止状态信息。但是,我们每次 fork 都要调用一个 wait 函数,实在是太麻烦了。

于是,我们就希望每次调用 fork 时不需要 wait 等待子进程终止,也不希望子进程处于僵死状态直到程序结束。这里提供一个实现此要求的技巧:调用 fork 两次。我们来看下面的例子:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h> int
main(void)
{
pid_t pid; if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid > ) {
printf("first child process: %d, parent process: %d\n", getpid(), getppid());
exit();
} sleep();
printf("second child process: %d, parent process: %d\n", getpid(), getppid());
exit();
}
if (wait(NULL) < ) {
printf("wait error: %s\n", strerror(errno));
exit(-);
}
printf("parent process %d exit\n", getpid());
exit();
}

nozombiedemo.c

在上面程序中,在第一个子进程中 fork 处第二个子进程之后并终止第一个子进程。编译该程序,生成并执行文件 nozombiedemo,

lienhua34:demo$ gcc -o nozombiedemo nozombiedemo.c
lienhua34:demo$ ./nozombiedemo
first child process: , parent process:
parent process exit
lienhua34:demo$ second child process: , parent process:

从上面的运行结果,我们看到第一个子进程 2471 终止后,其子进程2472 的父进程 ID 变成了 1(即 init 进程)。前面我们提到过,父进程为 init进程的所有进程在终止时都会被 init 进程获取其终止状态,从而不会变成僵尸进程。于是,通过上面的 fork 两次的技巧,我们就可以实现创建一个新进程,不需要等待该新进程终止,也不担心该新进程会变成僵尸进程。

(done)

UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数的更多相关文章

  1. UNIX环境编程学习笔记(22)——进程管理之system 函数执行命令行字符串

    lienhua342014-10-15 ISO C 定义了 system 函数,用于在程序中执行一个命令字符串.其声明如下, #include <stdlib.h> int system( ...

  2. UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲

    lienhua342014-10-05 1 进程控制三部曲概述 UNIX 系统提供了 fork.exec.exit 和 wait 等基本的进程控制原语.通过这些进程控制原语,我们即可完成对进程创建.执 ...

  3. UNIX环境编程学习笔记(20)——进程管理之exec 函数族

    lienhua342014-10-07 在文档“进程控制三部曲”中,我们提到 fork 函数创建子进程之后,通常都会调用 exec 函数来执行一个新程序.调用 exec 函数之后,该进程就将执行的程序 ...

  4. UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习

    lienhua342014-10-07 在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程.但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork ...

  5. UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念

    lienhua342014-10-05 1 main 函数是如何被调用的? 在编译 C 程序时,C 编译器调用链接器在生成的目标可执行程序文件中,设置一个特殊的启动例程为程序的起始地址.当内核执行 C ...

  6. UNIX环境编程学习笔记(15)——进程管理之进程终止

    lienhua342014-10-02 1 进程的终止方式 进程的终止方式有 8 种,其中 5 种为正常终止,它们是 1. 从 main 返回. 2. 调用 exit. 3. 调用_exit 或_Ex ...

  7. UNIX环境编程学习笔记(24)——信号处理进阶学习之信号集和进程信号屏蔽字

    lienhua342014-11-03 1 信号传递过程 信号源为目标进程产生了一个信号,然后由内核来决定是否要将该信号传递给目标进程.从信号产生到传递给目标进程的流程图如图 1 所示, 图 1: 信 ...

  8. UNIX环境编程学习笔记(26)——多线程编程(一):创建和终止线程

    lienhua342014-11-08 在进程控制三部曲中我们学习了进程的创建.终止以及获取终止状态等的进程控制原语.线程的控制与进程的控制有相似之处,在表 1中我们列出了进程和线程相对应的控制原语. ...

  9. UNIX环境编程学习笔记(23)——信号处理初步学习

    lienhua342014-10-29 1 信号的概念 维基百科中关于信号的描述是这样的: 在计算机科学中,信号(英语:Signals)是 Unix.类 Unix 以及其他 POSIX 兼容的操作系统 ...

随机推荐

  1. jquery开发的”天才笨笨碰“游戏

    前段时间湖南卫视的快乐大本营里有一款“天才笨笨碰”游戏非常火.这款游戏主要是考选手的声母联想词语的能力. 小篇在看完这个节目后用jquery制作了“天才笨笨碰”网页游戏.先上效果图: 游戏规则: 1. ...

  2. PHP重载以及Laravel门面Facade

    目录 重载的概念 魔术方法中的重载 属性重载 方法重载 Laravel中的Facade 扩展 谈谈__invoke Laravel提供了许多易用的Facade,让我们用起来特步顺手,那么这些Facad ...

  3. corntab被黑记录

    多出来的corntab 最近服务器经常出现负载过高的情况,经过运维排查,出现了一个corntab定时任务 不是开发的锅,别背 一开始运维认为是这个定时任务是我们开发的. 排查后,明确了服务器是被黑了. ...

  4. [转]tppabs是什么?如何去除tppabs?

    原文地址:http://www.cnblogs.com/gdsblog/archive/2017/03/25/6616561.html 不得不说,一款伟大的软件,就是用来解放人类双手的,Telepor ...

  5. <孤独者生存(小小辛巴投资手记)>读书笔记

    书在这里 让投机客梦醒的办法就是让资产损失至少一半 天不会塌下来,世界末日也不会这么快就降临,经济也许会萧条但不会崩溃,人们还得穿衣吃饭.休息劳作 每个笨蛋都会从自己的错误中吸取教训,聪明的人则从别人 ...

  6. RequireJS模块化之循环依赖

    如果你定义一个循环依赖关系 (a 依赖b 并且 b 依赖 a),那么当b的模块构造函数被调用的时候,传递给他的a会是undefined. 但是b可以在a模块在被引入之后通过require(‘a’)来获 ...

  7. 配置nginx

    咱不玩服务器,只在把人家的配置拷贝一份,建个自己的测试服务器 1. 如果nginx已配置(相当于windows在环境变量中配置了path吧) 查找nginx配置路径: whereis nginx 一般 ...

  8. Should I expose synchronous wrappers for asynchronous methods?

    In a previous post Should I expose asynchronous wrappers for synchronous methods?, I discussed " ...

  9. 关于Unity中的光照(七)

    全局光照 GI 这里所说的反射就是,一个红色的物体,当太阳照射它的时候,它周围的物体也会变得有点红. 1:Realtime每帧都会计算光照,实时光照是不会反射的,所以它的光影显得单调;2:Baked ...

  10. lapacke svd实例

    参考 intel MTK实例 https://software.intel.com/sites/products/documentation/doclib/mkl_sa/11/mkl_lapack_e ...