[实践OK]僵尸进程和僵死进程有什么区别?以及在Python的Kafka客户端凋用PHP时在高并发时产生僵尸进程。 不指定

jackxiang 2018-2-7 15:54 | |
Linux 下 popen 函数引起的僵尸进程 defunct 以及解决办法:http://blog.csdn.net/sky_qing/article/details/22296827

概览:任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”(首先:它是wait了的,其次,子进程运行完毕,再次,父亲进程没有来得及处理(繁忙容易出现,应该还有其它也会出现..,总之父进程来不及处理。)。 以上三个状态均满足,才会有Z,否则缺一个的情况下是没有Z状态的,下面的实验没有出现繁忙,也就没有看到Z,要看到Z在Kafka的Python调用PHP,在进行压力测试时也就实实在在的看到了Z状态。)。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。

##如何避免产生僵尸进程

我们知道了僵尸进程产生的原因和危害,那么如何避免产生僵尸进程呢?

一般,为了防止产生僵尸进程,在fork子进程之后我们都要wait它们;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。如下代码所示:

void sig_chld( int signo ) {
    pid_t pid;
    int stat;
    pid = wait(&stat);    
    printf( "child %d exit\n", pid );
    return;
}

int main() {
    signal(SIGCHLD,  &sig_chld);
}
SIGCHLD,在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号。

二、开始实践研究:
1.学习一个命令行查看父子关系的Ps命令:ps -f -C
2.Python调用PHP产生僵尸进程的缘故,继承关系如下:
1号进程生下python的主进程35923,而Python产生了8个子进程(35926,35927,35928,35929,35930,35931,35932,35933),
而这8个子进程呢,又去调用了PHP可执行文件,就拿其中一个Python的了进程PID=35928,它调用PHP进程产生了多个PHP的PID(6770,6773,6774,6776,6818
),而这些因为PHP运行太快,进而导致出现了僵尸的状态,而

摘自:https://www.zhihu.com/question/26432067
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

/tmp/defunct.c
make defunct


ps -f -C defunct
UID        PID  PPID  C STIME TTY          TIME CMD
root     36158 34442  0 16:05 pts/12   00:00:00 ./defunct
root     36159 36158  0 16:05 pts/12   00:00:00 [defunct] <defunct>

而运行是先SSHD从1号进程继承子进程,后再 su mamingyao,再su xiangdong,一层层的继承,进而实现运行上面的进程:
上面的进程表明,只要defunc父进程不退出,且它没有wait_pid,那么它就是僵尸态,而退出后,这个僵尸态也就没有了。


结论:经实践,发现如果父进程一直运行不退出,且父进程只Fork不进行等待子进程pid = wait(&stat);   而这个僵尸的状态一直存在的,正如前面所讲,子进程运行完退出,此时如果父进程退出,这个子进程的僵尸会被1号进程清理掉,如果父进程一直不退出,则这个僵尸态一直存在,并不会被清理掉,如果调用了pid = wait(&stat);   应该在父进程不繁忙时,会立即回收,如果繁忙,可能还是会短暂出现僵尸态的情况发生,下面两段增强版本代码得到了验证:
1)父进程一直运行,不退出,僵尸一直在,退出后被1号进程清理实验:
vim /tmp/defunct/defunct.c

运行:
#./defunct
查看:
#ps -f -C defunct
UID        PID  PPID  C STIME TTY          TIME CMD
root     55767 55416  0 09:51 pts/11   00:00:00 ./defunct
root     55768 55767  0 09:51 pts/11   00:00:00 [defunct] <defunc

ps -efL  |grep defunct
root     55767 55416 55767  0    1 09:51 pts/11   00:00:00 ./defunct
root     55768 55767 55768  0    1 09:51 pts/11   00:00:00 [defunct] <defunct>
僵尸是有的,但没有看到Z状态,呵呵,

2)父进程等待:SIGCHLD,在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号。,等待子进程pid = wait(&stat);发现在父进程不繁忙时,并没有出现僵尸的情况,何为繁忙,我估计在高并发时可能也就是所谓的繁忙,在下面对Kafaka进行压力测试时的确发现有僵尸但一会又回收了,也就是说Python也调用了wait等待子进程了,但是太繁忙,Top看时其 N zombie,导致并没有立即清理 ,出现时而僵尸进程多时而被回收的情况属于正常情况。
vim /tmp/defunct/defunct.c

运行:
#./defunct
查看:
ps -f -C defunct
UID        PID  PPID  C STIME TTY          TIME CMD
root     57355 55416  0 10:03 pts/11   00:00:00 ./defunct

#ps -efL  |grep defunct
root     58663 55416 58663  0    1 10:10 pts/11   00:00:00 ./defunct
不存在僵尸进程的标识,被wait干掉了。


附带说下孤儿进程,孤儿进程(即使父亲等待子进程signal(SIGCHLD,  &sig_chld);,父亲死了,还是会成孤儿):
vim /tmp/defunct/defunct.c



./defunct    
I am father process.I will sleep two seconds
pid=59405
I am child process.I am exiting.
pid=59406


子进程也一直While(1),不通出,此时看这两个进程正常:
#ps -efL  |grep defunct
root     59303 55416 59303  0    1 10:15 pts/11   00:00:00 ./defunct
root     59304 59303 59304  0    1 10:15 pts/11   00:00:00 ./defunct

#ps -f -C defunct
UID        PID  PPID  C STIME TTY          TIME CMD
root     59303 55416  0 10:15 pts/11   00:00:00 ./defunct
root     59304 59303  0 10:15 pts/11   00:00:00 ./defunct

关键一步,杀死父PID,59405是父进程,杀死父亲(注意:如果用直接对启动进程杀死的Ctrl+c是会整个都退出,得用Kill命令才能出现孤儿进程):
kill -9 59405

出现子进程PID=59406,被1号进程收养,也就是所谓的孤儿进程:
ps -efL  |grep defunct      
root     59406     1 59406  0    1 10:15 pts/11   00:00:00 ./defunct




转回正题说到僵尸进程:
=========================================================================
如下:Kafaka用Python写的多进程调用PHP出现僵尸进程:
说明啥,说明如果主进程是守护进程不退出,子进程去运行PHP的清况,而不wait_pid, 会产生一堆的僵尸进程,如果有一个Python子进程死了,PHP的僵尸也就死了。或者Python有WaitPID,也就会出现僵尸态存在,一会就好了的情况,这个所谓繁忙,我估计是涉及到阻塞、网络阻塞、队列阻塞啥的引起,导致没有及时回调wait而出现了短暂的僵尸。





======================Kafka的Client用Python写的调用了PHP===============================
[root@ilv-api_php_bj_szq_1*_70_33_140 ~]# ps -f -C php
UID        PID  PPID  C STIME TTY          TIME CMD
www       6770 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6773 35928  0 11:50 ?        00:00:00 [php] <defunct>


[root@ilv-api_php_bj_szq_1*_70_33_140 ~]# ps -ef|grep 35928
www       6770 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6773 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6774 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6776 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6778 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6779 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6784 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6789 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6790 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6793 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6796 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6798 35928  0 11:50 ?        00:00:00 [php

PID和PPID:
1、PID(process ID):
PID是程序被操作系统加载到内存成为进程后动态分配的资源。
每次程序执行的时候,操作系统都会重新加载,PID在每次加载的时候都是不同的。
2、PPID(parent process ID):PPID是程序的父进程号。

grep 35928
www      35928 35923  2 11:05 ?        00:06:47 ms-survey-kafka-client
[root@ilv-api_php_bj_szq_1*_70_33_140 ~]# ps -f -C php
UID        PID  PPID  C STIME TTY          TIME CMD
www       6770 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6773 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6774 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6776 35928  0 11:50 ?        00:00:00 [php] <defunct>
www       6818 35928  0 11:50 ?        00:00:00 [php] <defunct>
                               ...

www       7501 35926  0 11:50 ?        00:00:00 [php] <defunct>
www       7504 35926  0 11:50 ?        00:00:00 [php] <defunct>
www       7505 35926  0 11:50 ?        00:00:00 [php] <defunct>
www       7506 35926  0 11:50 ?        00:00:00 [php] <defunct>
www       7507 35926  0 11:50 ?        00:00:00 [php] <defunct>
                               ...

父亲进程:
www      35928 35923  2 11:05 ?        00:06:47 ms-survey-kafka-client
www      35926 35923  2 11:05 ?        00:06:53 ms-survey-kafka-client

ps -f -C ms-survey-kafka-client
UID        PID  PPID  C STIME TTY          TIME CMD
www      35923     1  0 11:05 ?        00:00:00 ms-survey-kafka-client                                          
www      35926 35923  2 11:05 ?        00:06:54 ms-survey-kafka-client                                          
www      35927 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35928 35923  2 11:05 ?        00:06:48 ms-survey-kafka-client                                          
www      35929 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35930 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35931 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35932 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client    
www      35933 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client


ps -ef|grep 35923
root     31083 28863  0 15:43 pts/0    00:00:00 grep 35923
www      35923     1  0 11:05 ?        00:00:00 ms-survey-kafka-client                                          
www      35926 35923  2 11:05 ?        00:06:54 ms-survey-kafka-client                                          
www      35927 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35928 35923  2 11:05 ?        00:06:48 ms-survey-kafka-client                                          
www      35929 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35930 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35931 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35932 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client                                          
www      35933 35923  0 11:05 ?        00:00:19 ms-survey-kafka-client


脑补:
摘自:https://michaelyou.github.io/2015/03/12/孤儿进程与僵尸进程/
##前言
孤儿继承和僵尸进程是APUE里面的一个重要概念,之前看书不仔细,也没有总结,所以这两个概念一直很模糊,只知道是父进程和子进程有一个退了,至于到底是父进程退还是子进程退会产生孤儿进程和僵尸进程,一直是我的一块心病啊。今天有空,来认真总结一下。

##基本概念

在unix/linux中,子进程是通过父进程创建的(fork)。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

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

我们再来分析一下:

孤儿的意思是什么?被父母抛弃了,或者没有父母。所以孤儿进程就是父进程不在了,留下子进程在继续运行。但孤儿不能自己存在啊,所以总会有好心人收养他,在unix系统里,这个好心人就是init进程,init进程会收养所有的孤儿进程,代替父进程手机子进程的终止状态。

同理,什么是僵尸?如果你死了,你就有可能成为僵尸。子进程挂了之后,有一个重要的步骤就是父进程应该调用wait或者waitpid来获取它的终止状态,让它入土为安的。但有些父母非常不负责,他没有做。所以子进程不能入土,就只能继续在系统里飘荡,成了僵尸。这不怪他们啊,都是父进程害的!

##危害

有的人就说了,那干嘛一定要父进程调用wait和waitpid来回收子进程啊,子进程挂了就让他挂好了,父进程不要回收,让系统自己把回收的事干了。我也是这样想的,可是你知道父母的通病在哪里吗?就是他们对自己的小孩,都有旺盛的控制欲!

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是:

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号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进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

##如何避免产生僵尸进程

我们知道了僵尸进程产生的原因和危害,那么如何避免产生僵尸进程呢?

一般,为了防止产生僵尸进程,在fork子进程之后我们都要wait它们;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。如下代码所示:

void sig_chld( int signo ) {
    pid_t pid;
    int stat;
    pid = wait(&stat);    
    printf( "child %d exit\n", pid );
    return;
}

int main() {
    signal(SIGCHLD,  &sig_chld);
}
先在main函数中给SIGCHLD信号注册一个信号处理函数(sig_chld),然后在子进程退出的时候,内核递交一个SIGCHLD的时候就会被主进程捕获而进入信号处理函数sig_chld,然后再在sig_chld中调用wait,就可以清理退出的子进程。这样退出的子进程就不会成为僵尸进程。

但是,这种方法并不是完美的,有时候还是会有漏网之鱼,下面是就是一个例子:

我们假设有一个client/server的程序,对于每一个连接过来的client,server都启动一个新的进程去处理来自这个client的请求。然后我们有一个client进程,在这个进程内,发起了多个到server的请求(假设5个),则server会fork 5个子进程来读取client输入并处理(同时,当客户端关闭套接字的时候,每个子进程都退出);当我们终止这个client进程的时候 ,内核将自动关闭所有由这个client进程打开的套接字,那么由这个client进程发起的5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个。server端接受到这5个FIN的时候,5个子进程基本在同一时刻终止。这就又导致差不多在同一时刻递交5个SIGCHLD信号给父进程,而最终结果大家将会发现,我们没有能够回收所有的5个进程,有僵尸进程产生了。

wait函数不能处理这种情况的原因是:所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般是不排队的。 更为严重的是,本问题是不确定的,依赖于客户FIN到达服务器主机的时机,信号处理函数执行的次数并不确定。

这种情况的正确的解决办法是调用waitpid而不是wait,方法为:信号处理函数中,在一个循环内调用waitpid,以获取所有已终止子进程的状态。我们必须指定WNOHANG选项,他告知waitpid在有尚未终止的子进程在运行时不要阻塞。(我们不能在循环内调用wait,因为没有办法防止wait在尚有未终止的子进程在运行时阻塞,wait将会阻塞到现有的子进程中第一个终止为止)。

##产生了僵尸进程怎么办

如果系统中出现了僵尸进程,如何打僵尸呢?



僵尸进程用kill命令是无法杀掉的,但是我们可以结果掉僵尸进程的爸爸,僵尸daddy挂了之后,僵尸进程就成了孤儿进程,会被init程序收养,然后init程序将其回收

作者:jackxiang@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:http://jackxiang.com/post/9626/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!


最后编辑: jackxiang 编辑于2018-6-28 11:48
评论列表
发表评论

昵称

网址

电邮

打开HTML 打开UBB 打开表情 隐藏 记住我 [登入] [注册]