为了让大家更好的理解僵尸这个概念先给大家看下百度百科对僵尸的定义:
僵尸(Jiang Shi): 顾名思义,僵硬的尸体;又名跳尸、移尸。在中华民间传说中,特指人类死后,尸体因为阴气过重而变异而成的鬼怪,毫无人性,丧失理智,双手向前横着伸直展开,且用双腿不停跳跃,从而移动的行尸走肉,除了头部和四肢,身子其他部位难以运动。加上由于近代影视的设想,往往会拥有超自然力量,比如力大无穷、刀枪不入、抗腐化等;甚至会使用武器和武功攻击人类。
所以,僵尸已经死了,不可能被杀死,不存在再死一次的问题。死的对立面是活,死者已死。只有活着的人才可能被杀死。
什么是僵尸进程?
首先要明确一点,僵尸进程的含义是:子进程已经死了,但是父进程还没有回收(wait)它的一个中间状态,这个时候子进程是一个僵尸。正常情况下子进程挂了,父进程通过wait回收子进程资源,清理掉子进程的task_struct,释放子进程的PID。看下面一个代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid, wait_pid;
int status;
pid = fork();
if (-1 == pid)
{
perror("fork");
exit(1);
}
else if (0 == pid)
{
printf("child pid = %d\n", getpid());
pause();
}
else
{
wait_pid = waitpid(pid, &status, 0);
if (WIFSIGNALED(status))
{
printf("child process is killed by signal %d\n", WTERMSIG(status));
}
exit(0);
}
return 0;
}
编译后运行:
# gcc zombie_process.c -o zombie_process
# ./zombie_processchild
pid = 16904
用ps命令查看进程的状态:
杀死子进程16904:
# kill -2 16904
父进程打印如下:
child process is killed by signal 2
之后,子进程16904会消失,因为父进程执行到了waitpid,也知道了子进程是被信号2杀掉的。程序没有任何问题。
对代码稍作修改。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid, wait_pid;
int status;
pid = fork();
if (-1 == pid)
{
perror("fork");
exit(1);
}
else if (0 == pid)
{
printf("child pid = %d\n", getpid());
pause();
}
else
{
while (1);
}
return 0;
}
我们重新编译运行,通过ps查看进程的状态:
用同样的方法杀死子进程17022:
kill -2 17022
然而,当我们再次使用ps查看进程的状态时,看到了不一样的现象。我们发现17022成为一个僵尸,状态变为Z,名字上也加了一个棺材[],成为[zombie_process]:
僵尸进程不可能被杀死。
看到上面17022是个僵尸很不爽,所以我们想把它干掉,据说Linux有个信号9,就是SIGKILL,这个信号是神一般的存在,神挡杀神,佛挡杀佛,我们现在来用kill -9干掉17022:
kill -9 17022
再次使用ps查看进程的状态:
即使我们把17022用kill -9捅了好多刀,但是最后看17022还是僵尸,一直没有消失。因为僵尸已经是死了,它不可能再次被杀死,你给它捅一万刀,它也是个死人,不可能再次死! 僵尸不可能被杀死,因为它已经死了!只能等父进程用wait来清理尸体了。这个时候我们能够把僵尸清理掉的方法,就是杀死僵尸进程的父进程17021。
一个僵尸可以被杀死的假象!
下面的这个程序证明“僵尸可以被杀死”:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void *mythread(void *arg)
{
while (1);
}
int main()
{
int ret;
pthread_t tid;
ret = pthread_create(&tid, NULL, mythread, NULL);
if (-1 == ret)
{
perror("pthread_create");
exit(1);
}
usleep(1000);
pthread_exit(0);
return 0;
}
在主线程里面,pthread_create()创建线程后,pthread_exit()退出。编译代码并运行:
#gcc zombie_thread.c -o zombie_thread -lpthread
#./zombie_thread
再打开一个终端,通过ps查看进程的状态,zombie_thread显示为一个僵尸:
这个时候我们来杀死17082这个僵尸:
kill -9 17082
我们会惊奇地发现,17082真的会从ps命令里面消失!
我们把时间轴拉回调用 kill -9 17082 之前。刚才我们“看起来”能杀死僵尸的本质原因是,当主线程17082调用pthread_exit()退出后,主线程17082的状态确实是僵尸了,但是该进程里面的17083线程,却没有死:
下面分别看下两个线程的状态:
17082
17083
17083是活着的,证明整个进程并没有挂。所以17082的退出,只是让整个进程 半死。 而由于ps命令的误会,17082凑巧又是整个进程的PID,它显示好像整个17082成了僵尸一样。
那么,根据POSIX标准关于信号(signal)的定义,当我们执行kill -9 17082(17082是17082和17083的TGID,也是整个进程用户态视角的PID)的时候,是要杀死整个17082进程的,所以这个时候17082被我们杀死,整个进程就都死了,这个时候,相当于执行到父进程的wait逻辑,导致僵尸消失。所以,在本例中,kill -9 17082看起来是"杀死了僵尸”,实际是杀死了17082整个进程(包括里面的每个线程),导致整个进程死。在此之前,整个进程实际还是活的。
讲了这么多,就是要告诉大家,写代码的时候, 一定要注意进程或者线程的回收问题,创建的每一个进程或者线程在代码的某个地方都要回收资源,有始有终,避免bug出现。
更多精彩视频、文章、嵌入式学习资料,微信关注公众号 『学益得智能硬件』
,