博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Linux-进程描述(3)之进程状态僵尸进程与孤儿进程
阅读量:5732 次
发布时间:2019-06-18

本文共 7466 字,大约阅读时间需要 24 分钟。

进程状态

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。为了弄明正正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有多个状态(在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 
2 kill -SIGCONT

向进程发送一个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 
2 #include
3 #include
4 #include
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 
#include
#include
#include
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 
2 #include
3 #include
4 #include
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 else25 {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 
2 #include
3 #include
4 #include
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



转载于:https://www.cnblogs.com/33debug/p/6725115.html

你可能感兴趣的文章
Address already in use: JVM_Bind解决办法之一
查看>>
Java基础学习总结(9)——this关键字
查看>>
rman中的crosscheck
查看>>
Java基础学习总结(18)——网络编程
查看>>
Linux HA (一)
查看>>
如何组建超过254台计算机的局域网
查看>>
美程序员低龄化 12岁开发98款游戏
查看>>
RabbitMQ学习总结(6)——消息的路由分发机制详解
查看>>
看别人是怎么挣钱的,你就知道生活中 思路决定成功
查看>>
《WebSphereMQ基础教程》MQI笔记
查看>>
ubunut下virtualbox启用USB控制器的方法
查看>>
Drozer使用简介
查看>>
python---list列表与字符串 list()、split()、join()
查看>>
SQL: MSVS2015连接mysql
查看>>
如何记录文件服务器上的文件操作日志
查看>>
c#打造的一款简易新闻阅读器
查看>>
loonAndroid----Android懒人框架开发日记
查看>>
shell编程(八)---- 字符串测试
查看>>
高并发学习笔记(二)
查看>>
rip的工作原理
查看>>