当前位置:   article > 正文

Linux学习_进程(4)——僵尸进程与孤儿进程_linux看孤儿进程

linux看孤儿进程

监视子进程

        在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视,下面来学习如何通过系统调用 wait()以及其它变体来监视子进程的状态改变。

wait()函数

        对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程 的终止状态信息,其函数原型如下所示:

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

函数参数和返回值含义如下:

        status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。

        返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。

        系统调用 wait()将执行如下动作:

        1、调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终止;

        2、如果进程调用 wait(),但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。        

       3、如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用wait() 也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”,关于这个问题后面再给大家进行介绍。所以在调用 wait() 函数之前,已经有子进程终止了,意味着正等待着父进程为其“收尸”,所以调用 wait()将不会阻塞,而是会立即替该子进程“收尸”、处理它的“后事”,然后返回到正常的程序流程中,一次 wait() 调用只能处理一次。

        参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中, 可以通过以下宏来检查 status 参数:

  1. WIFEXITED(status):如果子进程正常终止,则返回 true;
  2. WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit() 时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过 WEXITSTATUS 宏转换;
  3. WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
  4. WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号。
  5. WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;

使用示例

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/wait.h>
  6. #include <errno.h>
  7. int main(void){
  8. int status;
  9. int ret;
  10. int i;
  11. /* 循环创建 3 个子进程 */
  12. for (i = 1; i <= 3; i++) {
  13. switch (fork()) {
  14. case -1:
  15. perror("fork error");
  16. exit(-1);
  17. case 0:
  18. /* 子进程 */
  19. printf("子进程<%d>被创建\n", getpid());
  20. sleep(i);
  21. _exit(i);
  22. default:
  23. /* 父进程 */
  24. break;
  25. }
  26. }
  27. sleep(1);
  28. printf("~~~~~~~~~~~~~~\n");
  29. for (i = 1; i <= 3; i++) {
  30. ret = wait(&status);
  31. if (-1 == ret) {
  32. if (ECHILD == errno) {
  33. printf("没有需要等待回收的子进程\n");
  34. exit(0);
  35. }else{
  36. perror("wait error");
  37. exit(-1);
  38. }
  39. }
  40. printf("回收子进程<%d>, 终止状态<%d>\n", ret,
  41. WEXITSTATUS(status));
  42. }
  43. exit(0);
  44. }

         通过 for 循环创建了 3 个子进程,父进程中循环调用 wait()函数等待回收子进程,并将本次回收的子进程进程号以及终止状态打印出来,编译测试结果如下:

                

 waitpid()函数

        使用 wait()系统调用存在着一些限制,这些限制包括如下:

  1. 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
  2. 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
  3. 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。

而设计 waitpid()则可以突破这些限制,waitpid()系统调用函数原型如下所示:

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

函数参数和返回值含义如下:

pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:

如果 pid 大于 0,表示等待进程号为 pid 的子进程;
如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。

        status:与 wait()函数的 status 参数意义相同。

        options:稍后介绍。

        返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍。

        参数 options 是一个位掩码,可以包括 0 个或多个如下标志:

  1. WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
  2. WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
  3. WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。

使用示例

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/wait.h>
  6. #include <errno.h>
  7. int main(void){
  8. int status;
  9. int ret;
  10. int i;
  11. /* 循环创建 3 个子进程 */
  12. for (i = 1; i <= 3; i++) {
  13. switch (fork()) {
  14. case -1:
  15. perror("fork error");
  16. exit(-1);
  17. case 0:
  18. /* 子进程 */
  19. printf("子进程<%d>被创建\n", getpid());
  20. sleep(i);
  21. _exit(i);
  22. default:
  23. /* 父进程 */
  24. break;
  25. }
  26. }
  27. sleep(1);
  28. printf("~~~~~~~~~~~~~~\n");
  29. for (i = 1; i <= 3; i++) {
  30. ret = waitpid(-1, &status, 0);
  31. if (-1 == ret) {
  32. if (ECHILD == errno) {
  33. printf("没有需要等待回收的子进程\n");
  34. exit(0);
  35. }else {
  36. perror("wait error");
  37. exit(-1);
  38. }
  39. }
  40. printf("回收子进程<%d>, 终止状态<%d>\n", ret,
  41. WEXITSTATUS(status));
  42. }
  43. exit(0);
  44. }

        将 wait(&status)替换成了 waitpid(-1, &status, 0),通过上面的介绍可知,waitpid()函数的这种参数配置情况与 wait()函数是完全等价的。

僵尸进程与孤儿进程

        当一个进程创建子进程之后,它们俩就成为父子进程关系,父进程与子进程的生命周期往往是不相同的,这里就会出现两个问题:

         1、父进程先于子进程结束。

         2、子进程先于父进程结束。 

        孤儿进程父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程,换言之,某一子进程的父进程结束后,该子进程调用 getppid()将返回 1,init 进程变成了该进程的“养父”;这是判定某一子进程的“生父”是否还“在世”的方法之一,通过下面代码进行测试:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. int main(void){
  5. /* 创建子进程 */
  6. switch (fork()) {
  7. case -1:
  8. perror("fork error");
  9. exit(-1);
  10. case 0:
  11. /* 子进程 */
  12. printf("子进程<%d>被创建, 父进程<%d>\n", getpid(), getppid());
  13. sleep(3); //休眠 3 秒钟等父进程结束
  14. printf("父进程<%d>\n", getppid());//再次获取父进程 pid
  15. _exit(0);
  16. default:
  17. /* 父进程 */
  18. break;
  19. }
  20. sleep(1);//休眠 1 秒
  21. printf("父进程结束!\n");
  22. exit(0);
  23. }

        在上述代码中,子进程休眠 3 秒钟,保证父进程先结束,而父进程休眠 1 秒钟,保证子进程能够打印出第一个 printf(),也就是在父进程结束前,打印子进程的父进程进程号;子进程 3 秒休眠时间过后,再次打印父进程的进程号,此时它的“生父”已经结束了。

        

        可以发现,打印结果并不是 1,意味着并不是 init 进程,而是 1263,这是怎么回事呢?通过"ps -axu"查询可知,进程号1263对应的是 upstart 进程,如下所示:

         事实上,/sbin/upstart 进程与 Ubuntu 系统图形化界面有关系,是图形化界面下的一个后台守护进程,可负责“收养”孤儿进程,所以图形化界面下,upstart 进程就自动成为了孤儿进程的父进程,这里笔者是在 Ubuntu 16.04 版本下进行的测试,可能不同的版本这里看到的结果会有不同。

        既然在图形化界面下孤儿进程的父进程不是 init 进程,那么我们进入 Ubuntu 字符界面,按 Ctrl + Alt + F1 进入,如下所示:

 输入 Linux 用户名和密码登录,我们再运行一次:

        字符界面模式下无法显示中文,所以出现了很多白色小方块,从打印结果可以发现,此时孤儿进程的父进程就成了 init 进程,大家可以自己测试下,Ctrl + Alt + F7 回到 Ubuntu 图形化界面。

僵尸进程

        进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用 wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。

        如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。子进程结束后其父进程并没有来得及立马给它“收尸”,子进程处于“曝尸荒野”的状态,在这么一个状态下,我们就将子进程称为僵尸进程。

        当父进程调用 wait()(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。 另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程。

        如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用 wait()回收子进程,此时子进程变成一个僵尸进程。首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以, 在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程。

示例代码

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. int main(void){
  5. /* 创建子进程 */
  6. switch (fork()) {
  7. case -1:
  8. perror("fork error");
  9. exit(-1);
  10. case 0:
  11. /* 子进程 */
  12. printf("子进程<%d>被创建\n", getpid());
  13. sleep(1);
  14. printf("子进程结束\n");
  15. _exit(0);
  16. default:
  17. /* 父进程 */
  18. break;
  19. }
  20. for ( ; ; )
  21. sleep(1);
  22. exit(0);
  23. }

        在上述代码中,子进程已经退出,但其父进程并没调用 wait()为其“收尸”,使得子进程成为一个僵尸进程,使用命令"ps -aux"可以查看到该僵尸进程,测试结果如下: 

         通过命令可以查看到子进程 18257依然存在,可以看到它的状态栏显示是“Z+”(zombie), 表示它是一个僵尸进程。僵尸进程无法被信号杀死,大家可以试试,要么等待其父进程终止、要么杀死其父进程,让 init 进程来处理,当我们杀死其父进程之后,僵尸进程也会被随之清理。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/948897
推荐阅读
相关标签
  

闽ICP备14008679号