当运行一个进程时,会产生一个父进程和一些子进程;
当子进程执行完毕后,会发送 Exit 信号然后消亡,然后它的父进程会调用(wait / waitpid)来读取它的退出状态,
如果读取成功,则将这个子进程从进程表中删除,
否则无法从进程表中删除,那么它就变成了一个僵尸进程。
当用 ps 命令观察进程状态时,可以看到这些进程的状态为 defunct。
ps aux | grep Z
如果僵尸进程数量巨大且长期存在的话,就相当于进程表中残留有大量僵尸进程的信息,
而这些信息是需要存储在内存的,所以会浪费资源。
由于僵尸进程已经死掉了(只保留了 task_struct 结构体),而死掉的进程是无法直接 kill 的,
所以一般通过杀掉父进程来间接干掉僵尸进程。
把父进程杀掉,僵尸进程会变成孤儿进程,然后过继给1号进程,而1号进程会扫描名下子进程,把 Z 状态进程回收;
ps -ef | grep 66046
qtest 66046 12321 99 Apr07 ? 992-23:20:31 [kvsvr] <defunct>
kill -9 12321
杀父进程之前,建议评估操作风险, 看看父进程还有哪些关联进程,是否能容忍被杀。
如果僵尸进程的父进程是1号进程(ppid=1),
ps -ef | grep 66046
qtest 66046 1 99 Apr07 ? 992-23:20:31 [kvsvr] <defunct>
那么 kill 就不管用了,只能通过 重启服务器 解决;
重启这招比较简单粗暴但也有效,
建议根据上面服务能否容忍重启影响来决定是否可以靠重启解决。
在每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,
看看是否有进程是刚刚结束的这个进程的子进程,
如果有的话, 就由 Init 进程来接管它, 成为它的父进程,从而保证每个进程都会有一个父进程。
通常来说,
一旦 init进程接管了 Z 状态的进程,就会调用 wait 将其回收的,
因此理论上被 Init 接管的所有进程都不会变成僵尸进程。
那么究竟为何会出现 ppid 是 1 的僵尸进程呢?
回到僵尸进程产生的根源 “进程退出” 这里 ,这里尝试推测一种可能性:
进程结束会调用内核函数 do_exit,这个函数有两个关键逻辑:
do_exit()
->exit_notify()
-> do_notify_parent()
如果要退出的进程时多线程进程,则可以将子进程托付给自己的兄弟线程,
如果没有这样的线程,则托付给init进程;
总之init进程会兜底。
对于单线程的进程来说,这个过程也比较简单;
但对于多线程的进程就略复杂:
因为只有线程组的主线程才有资格通知父进程,
线程组的其他线程终止时,并不会通知父进程,甚至都没必要保留资源进入僵尸状态,直接调用 release_task 函数释放所有的资源就完事了。
由于父进程只认子进程的主线程,所以在线程组中,如果主线程终止了,但是如果线程组还有其他线程在运行,那么就不会通知父进程为自己释放task_struct了,直到线程组中最后一个线程退出时才会释放。
因此在用户态,是可以调用pthread_exit让主线程先退出的,但在内核态中,主线程的task_struct可能因为线程组还有其他线程在运行而需要继续留住;这种情况下,主线程就会变为Z僵尸状态,纵使init接管了主线程,也不会改变。
于是便出现了 “僵尸进程”的 ppid 是 1 的现象。