zoukankan      html  css  js  c++  java
  • Linux-进程描述(3)之进程状态僵尸进程与孤儿进程

    进程状态

    进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。为了弄明正正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有多个状态(在Linux内核中,进程有时候也叫做任务)。下面的状态在 fs/proc/array.c 文件中定义:

    复制代码
    /*
    * The task state array is a strange "bitmap" of
    * reasons to sleep. Thus "running" is zero, and
    * you can test for combinations of others with
    * simple bit tests.
    */
    static const char * const task_state_array[] = {
    "R (running)", /* 0 */
    "S (sleeping)", /* 1 */
    "D (disk sleep)", /* 2 */
    "T (stopped)", /* 4 */
    "t (tracing stop)", /* 8 */
    "X (dead)", /* 16 */
    "Z (zombie)", /* 32 */
    };
    复制代码

    运行状态(running)并不意味着进程时刻都一定在运行中,它表明进程要么是在运行中要么在运行队列里。只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。

    睡眠状态(sleeping)意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。

    磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是TASK_INTERRUPTIBLE状态。在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。
        可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT 信号让进程继续运行。例如,可以用下面的方法来停止或继续运行进程:

    1 kill -SIGSTOP <pid>
    2 kill -SIGCONT <pid>

    向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)。(SIGSTOP与SIGKILL信号一样,是非常强制的。不允许用户进程通过signal系列的系统调用重新设置对应的信号处理函数。)向进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态。可以使用gdb终止进程来实现跟踪终止状态。
    X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁。而进程在退出过程中也可能不会保留它的task_struct。比如这个进程是多线程程序中被detach过的进程 。或者父进程通过设置SIGCHLD信号的handler为SIG_IGN,显式的忽略了SIGCHLD信号。(这是posix的规定,尽管子进程的退出信号可以被设置为SIGCHLD以外的其他信号。)此时,进程将被置于EXIT_DEAD退出状态,这意味着接下来的代码立即就会将该进程彻底释放。所以EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到。

    Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且子进程(使用wait()系统调用)没有读取到父进程退出的返回代码时就会产生僵死进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
    孤儿进程。一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

    僵尸进程与孤儿进程比较

    在linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。如同一个人在外意外死亡,不管是什么原因,总之在警方调查完了之后会通知家属为他办理后事,在这种 还无人收尸的状态就可以称之为僵死状态,如果家属一直不来收尸,那他将一直处于僵死状态。只要父进程不退出,这个僵尸状态的子进程就一直存在。那么如果父进程退出了呢,谁又来给子进程“收尸”?当进程退出的时候,会将它的所有子进程都托管给别的进程(使之成为别的进程的子进程)。托管给谁呢?可能是退出进程所在进程组的下一个进程(如果存在的话),或者是1号进程。所以每个进程、每时每刻都有父进程存在。除非它是1号进程。1号进程,pid为1的进程,又称init进程。linux系统启动后,第一个被创建的用户态进程就是init进程。它有两项使命:
    1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙);
    2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作;
    init进程不会被暂停、也不会被杀死(这是由内核来保证的)。它在等待子进程退出的过程中处于TASK_INTERRUPTIBLE状态,“收尸”过程中则处于TASK_RUNNING状态。
    这里有一个僵死进程例子:

    复制代码
     1 #include <stdio.h>
     2 #include <unistd.h>
     3 #include <errno.h>
     4 #include <stdlib.h>
     6 int main()
     7 {
     8     pid_t pid;
     9     pid = fork();
    10     if (pid < 0)
    11     {
    12         perror("fork error:");
    13         exit(1);
    14     }
    15     else if (pid == 0)
    16     {
    17         printf("I am child process.I am exiting.
    ");
    18         exit(0);
    19     }
    20     printf("I am father process.I will sleep two seconds
    ");
    21     //等待子进程先退出
    22     sleep(2);
    23     //输出进程信息
    24     system("ps -o pid,ppid,state,tty,command");
    25     printf("father process is exiting.
    ");
    26     return 0;
    27 }
    复制代码

    孤儿进程测试程序如下所示:

    复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <unistd.h>
    
    int main()
    {
        pid_t pid;
        //创建一个进程
        pid = fork();
        //创建失败
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //子进程
        if (pid == 0)
        {
            printf("the child process.
    ");
            //输出进程ID和父进程ID
            printf("pid: %d	ppid:%d
    ",getpid(),getppid());
            printf("sleep five seconds.
    ");
            //睡眠5s,保证父进程先退出
            sleep(5);
            printf("pid: %d	ppid:%d
    ",getpid(),getppid());
            printf("child process is exited.
    ");
        }
        //父进程
        else
        {
            printf("father process.
    ");
            //父进程睡眠1s,保证子进程输出进程id
            sleep(1);
            printf("father process is  exited.
    ");
        }
        return 0;
    }
    复制代码

    输出结果

    问题及危害

    在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

    孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

    僵尸进程测试2:父进程循环创建子进程,子进程退出,造成多个僵尸进程,程序如下所示:

    复制代码
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <unistd.h>
     4 #include <errno.h>
     5 
     6 int main()
     7 {
     8     pid_t  pid;
     9     //循环创建子进程
    10     while(1)
    11     {
    12         pid = fork();
    13         if (pid < 0)
    14         {
    15             perror("fork error:");
    16             exit(1);
    17         }
    18         else if (pid == 0)
    19         {
    20             printf("I am a child process.
    I am exiting.
    ");
    21             //子进程退出,成为僵尸进程
    22             exit(0);
    23         }
    24         else
    25         {
    26             //父进程休眠20s继续创建子进程
    27             sleep(20);
    28             continue;
    29         }
    30     }
    31     return 0;
    32 }
    复制代码

     

    僵尸进程危害场景:

    例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。

    僵尸进程解决办法

    原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。测试程序如下所示:

    复制代码
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <unistd.h>
     4 #include <errno.h>
     5 int main()
     6 {
     7     pid_t  pid;
     8     //创建第一个子进程
     9     pid = fork();
    10     if (pid < 0)
    11     {
    12         perror("fork error:");
    13         exit(1);
    14     }
    15     //第一个子进程
    16     else if (pid == 0)
    17     {
    18         //子进程再创建子进程
    19         printf("I am the first child process.pid:%d	ppid:%d
    ",getpid(),getppid());
    20         pid = fork();
    21         if (pid < 0)
    22         {
    23             perror("fork error:");
    24             exit(1);
    25         }
    26         //第一个子进程退出
    27         else if (pid >0)
    28         {
    29             printf("first procee is exited.
    ");
    30             exit(0);
    31         }
    32         //第二个子进程
    33         //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
    34         sleep(3);
    35         printf("I am the second child process.pid: %d	ppid:%d
    ",getpid(),getppid());
    36         exit(0);
    37     }
    38     //父进程处理第一个子进程退出
    39     if (waitpid(pid, NULL, 0) != pid)
    40     {
    41         perror("waitepid error:");
    42         exit(1);
    43     }
    44     exit(0);
    45     return 0;
    46 }
  • 相关阅读:
    pip 更新下载源
    Image-match
    Laravel之事务与锁
    Laravel之vue操作
    html 固定居中
    vue状态管理(vuex)
    vue兄弟组件的相互通讯(vuex方式)
    vue兄弟组件之间的通信(bus.js)方法
    webapi使用过滤器拦截客户端传来的参数
    webapi跨域,服务器上使用session
  • 原文地址:https://www.cnblogs.com/tangshiguang/p/6735306.html
Copyright © 2011-2022 走看看