天天看点

31-wait 大战僵尸

按照正常的逻辑,应该讲讲 vfork 的(专为 exec 而定制)。不过鉴于 vfork 现在已经很少使用了,而且现在的 fork 也完全可以替代 vfork,所以讲 vfork 有点重复的意思。当然了不排除面试或者考试会有人问到 vfork,这里稍微提两笔。vfork 采用了类似读时共享的机制,但是其不保证写时复制,它产生的子进程和父进程共享进程空间,所以,如果在 vfork 后没有使用 exec 或者 _exit 函数,其行为将是未定义的。

好了,点到为止,而且实际开发中,也不希望使用 vfork(如果你对vfork感兴趣,请自行 man 或者查阅 《apue》)。下面来造僵尸吧 ^_^。

1. 僵尸进程

如果你对父子进程还不理解,赶紧先跳到前面的章节。

“如果子进程运行结束了而父进程还没有,就会产生僵尸进程。”

这很容易理解,比如你 fork 了一个子进程后,父进程由于耗时任务还没结束,子进程却未老先衰,父进程还对它视而不见,它就死不瞑目。

1.1 造 5 个僵小鱼

下面的代码演示了僵尸进程的制造方法。其原理很简单,就是保证“白发人送黑发人”这一条件就OK。

  • 代码
// mywait.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
  printf("before fork\n");

  pid_t pid, n = 5;

  // 父进程生出 5 个子进程
  while(n--) {
    pid = fork();
    if (pid == 0) break;
    else if (pid < 0) {
      perror("fork");
      return 1;
    }   
  }

  // 子进程打印一句话就死了。
  if (pid == 0) {
    printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
    return 0;
  }


  // 父进程永远在这打印。
  while(1) {
    sleep(3);
    printf("hello, I'm father %d\n", getpid());
  }
  return 0;
}      
  • 编译
$ gcc mywait.c -o mywait      
  • 运行
$ ./mywait      
  • 结果
before fork
hello, I'm child 7923; my father is 7918
hello, I'm child 7922; my father is 7918
hello, I'm child 7921; my father is 7918
hello, I'm child 7920; my father is 7918
hello, I'm child 7919; my father is 7918
hello, I'm father 7918
hello, I'm father 7918
hello, I'm father 7918
hello, I'm father 7918      

1.2 查看僵尸

再开启一个终端,使用 ​

​ps -af​

​命令,结果如下:

UID        PID  PPID  C STIME TTY          TIME CMD
……
allen     7918  4565  0 14:19 pts/1    00:00:00 ./mywait
allen     7919  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7920  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7921  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7922  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7923  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>      

可以看到,这里有 5 个进程,名字被加了方括号,后面还跟着 ​

​<defunct>​

​ 字样。可是子进程明明已经结束了呢?为什么还死不瞑目?

实际上,子进程在死的时候,通知了它父亲:父亲我要死了,快来给我收尸吧!(发送 SIGCHILD信号给父进程)

可是前面的代码父亲除了一直在喊:我是父亲 7918, 我是父亲 7918,……好像没有干其它任何事情。子进程未等到父亲的回复,所以在那死不瞑目。除非子进程的父亲也死了,这时候会有 init 进程来替代原来的父亲替这些子进程收尸。

2. wait 函数——让逝者安息

弄明白了僵尸进程的由来,我们就能想出对策来清理这些僵尸进程。有人说,不清理不行吗?这样说吧,如果僵尸进程的数量非常少,其实对系统造不成什么威胁,如果僵尸进程越来越多,最后就会造成资源耗尽。所以,代码的健壮性很重要!!!特别是服务器开发领域中,通常一运行都上数月甚至好几年,如果代码不健壮,就会造成系统中的僵尸进程越来越多,最后瘫痪(虽然重启也可以解决,但是这治标不治本)。

前面讲到,子进程在死的时候会通知父亲(给父进程发送 SIGCHILD 信号,信号的概念后面才会讲到,这里只要知道就行了),所以父进程只要妥尚处理好子进程的通知就行了,需要用到的函数就是 wait.

wait 函数原型如下:

// 参数保存子进程退出通知码,返回 -1 表示没有子进程或者错误。否则返回子进程的进程 id 号。
pid_t wait(int *status);      

前面的代码改成如下的样子:

  • 代码
// wipeoutzombie.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

int main() {
  printf("before fork\n");

  pid_t pid, n = 5;
  while(n--) {
    pid = fork();
    if (pid == 0) break;
    else if (pid < 0) {
      perror("fork");
      return 1;
    }   
  }

  if (pid == 0) {
    printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
    return 0;
  }


  while(1) {
    sleep(3);
    pid = wait(NULL); // 忽略子进程通知码
    if (pid == -1) {
      perror("wait");
      sleep(10);
      printf("I'm father %d; I have wiped out all zombies\n", getpid());
      return 1;
    }   
    printf("Hello, I'm father %d; child %d, getpid(), pid);
  }
  return 0;
}      
before fork
hello, I'm child 8092; my father is 8087
hello, I'm child 8091; my father is 8087
hello, I'm child 8090; my father is 8087
hello, I'm child 8089; my father is 8087
hello, I'm child 8088; my father is 8087
Hello, I'm father 8087; child 8088 exit
Hello, I'm father 8087; child 8089 exit
Hello, I'm father 8087; child 8090 exit
Hello, I'm father 8087; child 8091 exit
Hello, I'm father 8087; child 8092 exit
wait: No child processes
I'm father 8087; I have wiped out all      

3. 总结

  • 理解什么是僵尸进程
  • 知道子进程退出时会给父进程发送 SIGCHILD 信号
  • 学会使用 wait 函数来清理僵尸进程

继续阅读