UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数
2017-09-04 18:12
871 查看
当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD信号。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。
在文档“进程控制三部曲”中,我们讲的第三部曲是使用 wait 函数来获取终止子进程的终止状态。那么,有几个问题我们这里需要详细的学习一下。
1. 父进程一定能够获取到子进程的终止状态吗?如果子进程在父进程调用 wait 函数前就终止了,怎么办?
2. 如果父进程没有获取子进程的终止状态,那会发生什么?
3. 如果父进程有多个子进程,那么获取的是哪个子进程的终止状态呢?
对于第一个问题的回答是:内核为每个终止进程保存了一定量的信息,包括进程 ID、该进程的终止状态、以及该进程使用的 CPU 时间总量。所以,当终止进程的父进程调用 wait 或者 waitpid 函数,即可获取到这些信息。当父进程获取终止进程的终止信息之后,内核就可以释放终止进程所使用的所有存储区、关闭其所有打开的文件。
在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的相关信息)的进程被称为僵尸进程(zombie)。如果编写一个长期运行的程序,调用 fork 产生子进程之后,需要调用 wait 来获取这些子进程的终止状态,否则这些子进程在终止之后将会变成僵尸进程。(后面会讲到用一个技巧以避开父进程调用 wait 获取所有子进程的终止状态。)
那么如果那些被 init 进程领养的进程在终止之后会不会也变成僵尸进程?答案是:不会。因为 init 进程无论何时只要有一个子进程终止,init 就会调用 wait 函数获取其终止状态。
那么关于上面的第三个问题,我们得通过详细学习 wait 和 waitpid 函数才能都做出回答了。
#include <sys/wait.h>
pid_t wait(int *statloc);
返回值:若成功则返回终止进程的ID,若出错则返回-1
参数 statloc 是一个整形指针。如果 statloc 不是一个空指针,则终止进程的终止状态将存储在该指针所指向的内存单元中。如果不关心终止状态,可以将 statloc 参数设置为空。
调用 wait 函数时,调用进程将会出现下面的情况:
• 如果其所有子进程都还在运行,则阻塞。
• 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回。
• 如果没有任何子进程,则立即出错返回。
wait 函数获取的终止状态是一个 int 型数值,那我们如何得到具体的终止信息呢?POSIX.1 规定终止状态用定义在 <sys/wait.h> 中的各个宏来参看。有四个互斥的宏可以用来得到进程的终止原因。这四个宏见表 1,
表 1: 检查终止状态的宏
下面我们来看一下打印终止进程状态说明的例子,
编译该程序,生成并运行 waitdemo 文件,
下面我们再来看一个产生僵尸进程的示例,
我们在父进程最后 sleep(5) 让父进程睡眠 5 秒钟是避免父进程太早退出,我们观察不到僵尸进程。我们编译该程序文件,生成并执行文件
ps 命令打印的进程中,Z 表示僵尸进程。从上面的运行结果,我们看到父进程(ID:2961)fork 了两个子进程(ID:2962 和 2963),然后调用了 wait 函数获取了子进程 2963 的终止状态,于是子进程 2962 便成为了僵尸进程。但是,当父进程也退出时,生成僵尸进程的子进程 2962 也被内核释放。
只要有一个子进程终止,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 常量
关于 options 用于作业控制 的两个 常量 WCONTINUED 和 WUNTRACED,我们这里不学习,我们只关心常量 WNOHANG。我们来看一个例子。
在上面的程序中,我们在第一个子进程中 sleep(3) 让该子进程睡眠 3秒,以便在父进程调用 waitpid 函数时该子进程尚未结束。编译该程序,生成并执行 waitpiddemo 文件,
从上面的运行结果,我们可以看到父进程阻塞等待子进程 2971 终止。我们如果把上面程序的 waitpid 函数第三个参数 options 改为 WNOHANG,看一下其实际运行结果。
从上面的运行结果,我们可以看出父进程调用 waitpid 函数时,子进程2981 尚未终止,于是 waitpid 函数没有阻塞父进程,直接返回 0.
前面讲到僵尸进程时,我们提到要编写一个长期运行的程序,要避免出现大量的僵尸情况,就需要每次 fork 出一个子进程时都需要调用 wait 函数来等待子进程的结束以便处理该子进程的终止状态信息。但是,我们每次 fork 都要调用一个 wait 函数,实在是太麻烦了。
于是,我们就希望每次调用 fork 时不需要 wait 等待子进程终止,也不希望子进程处于僵死状态直到程序结束。这里提供一个实现此要求的技巧:调用 fork 两次。我们来看下面的例子:
在上面程序中,在第一个子进程中 fork 处第二个子进程之后并终止第一个子进程。编译该程序,生成并执行文件 nozombiedemo,
从上面的运行结果,我们看到第一个子进程 2471 终止后,其子进程2472 的父进程 ID 变成了 1(即 init 进程)。前面我们提到过,父进程为 init进程的所有进程在终止时都会被 init 进程获取其终止状态,从而不会变成僵尸进程。于是,通过上面的 fork 两次的技巧,我们就可以实现创建一个新进程,不需要等待该新进程终止,也不担心该新进程会变成僵尸进程。
(done)
在文档“进程控制三部曲”中,我们讲的第三部曲是使用 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,
宏 | 说明 |
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()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { exit(8); } if (wait(&status) != pid) { printf("wait error: %s\n", strerror(errno)); exit(-1); } print_exit(status); if ((pid = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { abort(); } if (wait(&status) != pid) { printf("wait error: %s\n", strerror(errno)); exit(-1); } print_exit(status); exit(0); } 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 文件,
lienhua34:demo$ gcc -o waitdemo waitdemo.c lienhua34:demo$ ./waitdemo normal termination, exit status = 8 abnormal termination, signal number =6
下面我们再来看一个产生僵尸进程的示例,
#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()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { exit(0); } printf("fork child process:%d\n", pid); if ((pid = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { exit(0); } printf("fork child process:%d\n", pid); if ((pid = wait(NULL)) < 0) { printf("wait error: %s\n", strerror(errno)); exit(-1); } printf("get child process(%d) termination status\n", pid); sleep(5); printf("parent process exit\n"); exit(0); }
我们在父进程最后 sleep(5) 让父进程睡眠 5 秒钟是避免父进程太早退出,我们观察不到僵尸进程。我们编译该程序文件,生成并执行文件
lienhua34:demo$ ps -A -ostat,pid | grep -e '[Zz]' Z 1725 lienhua34:demo$ gcc -o zombiedemo zombiedemo.c lienhua34:demo$ ./zombiedemo & [1] 2961 lienhua34:demo$ fork child process:2962 fork child process:2963 get child process(2963) termination status ps -A -ostat,pid | grep -e '[Zz]' Z 1725 Z 2962 lienhua34:demo$ parent process exit ps -A -ostat,pid | grep -e '[Zz]' Z 1725 [1]+ 完成 ./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 中各常量或运算的结果。
常量 | 说明 |
WCONTINUED | 若实现支持作业控制,那么由 pid 指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态。 |
WNOHANG | 若由 pid 指定的子进程并不是立即可用的,则 waitpid 不阻塞,此时返回值为 0. |
WUNTRACED | 若某实现支持作业控制,而由 pid 指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。 |
#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()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid1 == 0) { sleep(3); printf("child process %d exit\n", getpid()); exit(0); } if ((pid2 = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid2 == 0) { printf("child process %d exit\n", getpid()); exit(0); } if ((waitpidRes = waitpid(pid1, NULL, 0)) == pid1) { printf("get terminated child process %d.\n", waitpidRes); } else if (waitpidRes < 0) { printf("waitpid error: %s\n", strerror(errno)); exit(-1); } else { printf("waitpid return 0\n"); } printf("parent process exit\n"); exit(0); }
在上面的程序中,我们在第一个子进程中 sleep(3) 让该子进程睡眠 3秒,以便在父进程调用 waitpid 函数时该子进程尚未结束。编译该程序,生成并执行 waitpiddemo 文件,
lienhua34:demo$ gcc -o waitpiddemo waitpiddemo.c lienhua34:demo$ ./waitpiddemo child process 2972 exit child process 2971 exit get terminated child process 2971. parent process exit
从上面的运行结果,我们可以看到父进程阻塞等待子进程 2971 终止。我们如果把上面程序的 waitpid 函数第三个参数 options 改为 WNOHANG,看一下其实际运行结果。
lienhua34:demo$ gcc -o waitpiddemo waitpiddemo.c lienhua34:demo$ ./waitpiddemo waitpid return 0 parent process exit child process 2982 exit lienhua34:demo$ child process 2981 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()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { if ((pid = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid > 0) { printf("first child process: %d, parent process: %d\n", getpid(), getppid()); exit(0); } sleep(2); printf("second child process: %d, parent process: %d\n", getpid(), getppid()); exit(0); } if (wait(NULL) < 0) { printf("wait error: %s\n", strerror(errno)); exit(-1); } printf("parent process %d exit\n", getpid()); exit(0); }
在上面程序中,在第一个子进程中 fork 处第二个子进程之后并终止第一个子进程。编译该程序,生成并执行文件 nozombiedemo,
lienhua34:demo$ gcc -o nozombiedemo nozombiedemo.c lienhua34:demo$ ./nozombiedemo first child process: 2471, parent process: 2470 parent process 2470 exit lienhua34:demo$ second child process: 2472, parent process: 1
从上面的运行结果,我们看到第一个子进程 2471 终止后,其子进程2472 的父进程 ID 变成了 1(即 init 进程)。前面我们提到过,父进程为 init进程的所有进程在终止时都会被 init 进程获取其终止状态,从而不会变成僵尸进程。于是,通过上面的 fork 两次的技巧,我们就可以实现创建一个新进程,不需要等待该新进程终止,也不担心该新进程会变成僵尸进程。
(done)
相关文章推荐
- UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数
- UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习
- UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习
- UNIX环境编程学习笔记(15)——进程管理之进程终止
- UNIX环境编程学习笔记(22)——进程管理之system 函数执行命令行字符串
- UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习
- UNIX环境编程学习笔记(15)——进程管理之进程终止
- UNIX环境编程学习笔记(16)——进程管理之进程环境变量
- UNIX环境编程学习笔记(20)——进程管理之exec 函数族
- UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念
- UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲
- UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念
- UNIX环境编程学习笔记(16)——进程管理之进程环境变量 http://www.cnblogs.com/lienhua34/p/4005367.html
- UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲
- UNIX环境编程学习笔记(20)——进程管理之exec 函数族
- 深入浅出---unix多进程编程之wait()和waitpid()函数
- Unix环境编程学习笔记------在进程间传送文件描述符
- 深入浅出---unix多进程编程之wait()和waitpid()函数
- UNIX环境编程学习笔记------编程实例-----对于 函数 inet_ntop()函数的第四个参数的理解
- Unix环境高级编程学习笔记之进程环境(1)