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.\n");
18         exit(0);
19     }
20     printf("I am father process.I will sleep two seconds\n");
21     //等待子进程先退出
22     sleep(2);
23     //输出进程信息
24     system("ps -o pid,ppid,state,tty,command");
25     printf("father process is exiting.\n");
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.\n");
        //输出进程ID和父进程ID
        printf("pid: %d\tppid:%d\n",getpid(),getppid());
        printf("sleep five seconds.\n");
        //睡眠5s,保证父进程先退出
        sleep(5);
        printf("pid: %d\tppid:%d\n",getpid(),getppid());
        printf("child process is exited.\n");
    }
    //父进程
    else
    {
        printf("father process.\n");
        //父进程睡眠1s,保证子进程输出进程id
        sleep(1);
        printf("father process is  exited.\n");
    }
    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.\nI am exiting.\n");
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\tppid:%d\n",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.\n");
30             exit(0);
31         }
32         //第二个子进程
33         //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
34         sleep(3);
35         printf("I am the second child process.pid: %d\tppid:%d\n",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 }

 本文参考http://www.cnblogs.com/Anker/p/3271773.html










posted @ 2017-04-18 11:30  滴巴戈  阅读(5387)  评论(0编辑  收藏  举报