僵尸进程与孤儿进程

父进程、子进程

在unix/linux系统中,大多情况下,子进程是通过父进程fork创建的。(系统调用fork,是一个比较有意思系统调用,它调用一次,返回两个值,失败返回-1,成功时在子进程返回0,父进程返回所创建子进程的pid。)

子进程创建后,子进程的结束和父进程的运行是一个异步过程,也就是说父进程自身是无法预测子进程结束时间的。 当一个子进程完成它的工作终止之后,其父进程需要调用wait()或waitpid()去获取子进程的终止状态。

僵尸进程、孤儿进程

孤儿进程

首先,聊一下孤儿进程,所谓的孤儿进程,即当一个进程的父进程生命周期已结束,这个进程自身生命周期还没有结束,那么这个进程会成为孤儿进程。孤儿进程会被init进程(进程号为1)收养,在子进程运行结束时init进程会负责它的状态收集工作,因此一般来说,孤儿进程并不会有什么危害。

下面看一个关于孤儿进程的例子:在main函数中,创建子进程,然后让父进程睡眠1s,让子进程先运行打印出其进程id(pid)以及父进程id(ppid);随后子进程睡眠3s(此时会调度到父进程运行直至结束),目的是让父进程先于子进程结束,让子进程有个孤儿的状态;最后子进程再打印出其进程id(pid)以及父进程id(ppid);观察两次打印 其父进程id(ppid)的区别。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <assert.h>
int main(int argc, char **argv)
{
    pid_t pid;
    pid = fork(); //创建子进程
    if (pid < 0)  //创建失败
    {
        perror("fork failed!!!");
        exit(1);
    }
    if (pid == 0) //子进程
    {
        printf("I am the child process.\n");
        //打印进程ID和父进程ID
        printf("pid:%d, ppid:%d\n", getpid(), getppid());
        printf("I will sleep three seconds.\n");
        //子进程睡眠3s,保证父进程先退出,此后子进程成为孤儿进程
        sleep(3);
        //注意查看孤儿进程的父进程变化
        printf("after sleep, pid:%d, ppid:%d\n", getpid(), getppid());
        assert(getppid() == 1);
        printf("child process is exited.\n");
    }
    else //父进程
    {
        printf("I am father process.\n");
        //为保证子进程先运行,让父进程睡眠1s
        sleep(1);
        printf("father process is exited.\n");
    }
    return 0;
}


运行结果表明:当父进程结束后,子进程成为了孤儿进程。因为它的父进程id(ppid)变成了1,即init进程成为该子进程的父进程了。

僵尸进程

僵尸进程简介

僵尸进程:一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait/waitpid获取子进程的状态信息,那么子进程的某些信息(如进程描述符)仍然保存在系统中。这样的进程被称之为僵尸进程。

下面,来看一个关于僵尸进程的例子:在main函数中,创建子进程,然后让父进程睡眠10s,让子进程先终止(注意和孤儿进程例子的区别);这里子进程结束后父进程没有调用wait/waitpid函数获取其状态,用ps查看进程状态可以看出子进程为僵尸状态。

#include <stdio.h>
#include <unistd.h>
#include <error.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork failed!!!");
        exit(1);
    }
    if (pid == 0)
    {
        printf("I am child process, I am exited\n");
        exit(0);
    }
    if (pid > 0)
    {
        printf("I am fathrer process.\n");
        //父进程睡眠10s等待子进程退出,且没有调用wait/waitpid获取其状态
        //子进程会成为僵尸进程
        sleep(10);
        printf("Father process is exited.\n");
    }

    return 0;
}

开启一个终端运行程序:

在子进程结束,父进程睡眠(还没退出)的时候,再开一个终端用PS查看进程状态

  1. 任何一个子进程(init除外)在exit()之后,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段
  2. 如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
  3. 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
  4. 如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是系统中有时候会有很多僵尸进程的原因。

概括

  1. 孤儿进程:父进程已亡,子进程成为孤儿,被init进程收养,由init进程对它们完成状态收集工作,因此一般没有坏的影响。
  2. 僵尸进程:子进程已亡,父进程没给子进程收尸,子进程成为僵尸进程,占用系统资源

僵尸进程问题及危害

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等),直到父进程通过wait / waitpid来取时才释放。

但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

如何杀死僵尸进程

首先,kill命令并不可行,defunct状态下的僵尸进程是不能直接使用kill -9 <pidof defunct process>命令杀掉的(否则就不叫僵尸进程了)。

消灭僵尸进程有两种方法:

  1. 重启服务器电脑,简单、易用,不过若该服务器运行其他程序那代价就大了。
  2. 找到僵尸进程的父进程,将该僵尸进程的父进程杀掉,则此defunct进程将自动消失
    至于查找僵尸进程的父进程其实很简单,一方面父子进程的名字相同,另一方面ps -ef | grep <pidof defunct process>相邻的一列就是其父进程pid.

如何避免出现僵尸进程

前面提到过,为了正常销毁子进程,父进程应主动请求获得子进程的状态信息,具体来说就是调用wait/waitpid函数。

  1. wait函数的声明
#include <sys/wait.h>
//成功时返回终止的子进程ID,失败时返回-1。
//终止的子进程相关信息将保存到status变量,但是包含的信息不止一种,此处需要借助WIFEXITED和WEXITSTATUS两个宏进行分离
pid_t wait(int * statloc);

但是, 调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking),直到有子进程终止,因此需要谨慎调用该函数。
2. wait函数会引起程序阻塞,我们可以考虑waitpid函数

#include <sys/wait.h>
//成功时返回终止的子进程ID(或0),失败时返回-1
pid_t waitpid(pid_t pid, int * statloc, int options);
  - pid: 等待终止的目标子进程id;若传-1,则与wait函数相同,可以等待任意子进程终止。
  - statloc: 与wait函数的statloc含义相同
  - options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并推出函数

借助waitpid函数可以非阻塞的消灭僵尸进程,但是子进程何时终止是未知的,如果用while循环检查过于浪费CPU资源,且影响父进程自身的功能。
如何更完美的消灭僵尸进程呢?
子进程终止的识别主体是操作系统,因此我们借助操作系统的帮助岂不是更好。子进程结束时,操作系统检测到该事件,然后通过信号处理机制将子进程结束的消息告诉父进程,父进程自定义与该消息相关的函数处理这个事件。
(1) 利用signal函数消灭僵尸进程

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

void read_childproc(int sigNum)
{
    printf("child process terminate signal num = %d \n", sigNum);
    int status;
    //调用wait函数,之前终止的子进程相关信息将保存到status变量,同时相关子进程被完全销毁
    wait(&status);
    if (WIFEXITED(status)) // 子进程正常终止返回true
    {
        printf("Normal termination!!!\n");
        printf("Child pass num: %d \n", WEXITSTATUS(status)); //子进程的返回值
    }
}

int main()
{
    signal(SIGCHLD, read_childproc);

    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork failed!!!\n");
        exit(1);
    }
    if (pid == 0)
    {
        printf("I am child process, I am exited\n");
        exit(100);
    }
    if (pid > 0)
    {
        printf("I am fathrer process.\n");
        //子进程退出,父进程立马收到OS传来的SIGCHLD信号,从sleep中苏醒过来,调用read_childproc信号处理函数
        sleep(3);
        printf("Father process is exited.\n");
    }

    return 0;
}


signal函数在UNIX系列的不同操作系统中可能存在区别,因此不太稳定,sigaction函数则在UNIX系统中表现完全相同,且其功能完全可以替代signal函数。
(2) 利用sigaction函数消灭僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/time.h>

void read_childproc(int sig)
{
   int status;
   pid_t id = waitpid(-1, &status, WNOHANG);
   if (WIFEXITED(status)) // 子进程正常终止返回true
   {
       printf("removed proc id :%d \n", id);
       printf("Child send: %d \n", WEXITSTATUS(status)); //子进程的返回值
   }
}

int main(int argc, char **argv)
{
   pid_t pid;

   struct sigaction act;
   act.sa_handler = read_childproc;
   sigemptyset(&act.sa_mask);
   act.sa_flags = 0;
   // sigaction第三个参数用于获取之前注册的信号处理函数指针,若不需要则传0
   sigaction(SIGCHLD, &act, 0);

   pid = fork();
   if (pid < 0)
   {
       perror("fork failed!!!\n");
       exit(1);
   }
   if (pid == 0)
   {
       printf("I am child process.\n");
       sleep(10);
       return 12;
   }
   if (pid > 0)
   {
       printf("first child process id:%d \n", pid);
       pid = fork();
       if (pid == 0) //另一子程序执行区域
       {
           printf("I am child process. \n");
           sleep(10);
           return 24;
       }
       else if (pid > 0)
       {
           printf("second child proc id : %d \n", pid);
           struct timeval tmstart, tmend;
           //单位:秒
           double use_time;
           gettimeofday(&tmstart, NULL);
           //为了等待发生SIGCHLD信号,使父进程共暂停5次,每次间隔5秒。
           //发生信号时,父进程将被唤醒,因此实际暂停时间不到25秒
           for (size_t i = 0; i < 5; i++)
           {
               printf("wait...\n");
               sleep(5);
           }
           gettimeofday(&tmend, NULL);
           use_time = ((tmend.tv_sec - tmstart.tv_sec) * 1000000 + tmend.tv_usec - tmstart.tv_usec) / 1000000;
           printf("Use time : %f seconds \n ", use_time);
       }
   }

   return 0;
}

posted @ 2022-07-20 11:42  时间的风景  阅读(341)  评论(0编辑  收藏  举报