聊聊Linux系统中的僵尸进程

车祸现场

今天下午,笔者正在认真搬砖,日志集群中有一台机器忽然报init进程占用100% CPU。strace之,发现疯狂输出如下系统调用。

~ strace -p 1
rt_sigprocmask(SIG_BLOCK, ~[RTMIN RT_1], [], 8) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7cc15789d0) = -1 ENOMEM (Cannot allocate memory)
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
close(9)                                = 0
close(10)                               = 0
pipe([9, 10])                           = 0
rt_sigprocmask(SIG_BLOCK, ~[RTMIN RT_1], [], 8) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7cc15789d0) = -1 ENOMEM (Cannot allocate memory)
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
close(9)                                = 0
close(10)                               = 0
pipe([9, 10])                           = 0
rt_sigprocmask(SIG_BLOCK, ~[RTMIN RT_1], [], 8) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7cc15789d0) = -1 ENOMEM (Cannot allocate memory)
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
close(9)                                = 0
close(10)                               = 0
pipe([9, 10])                           = 0

然后top和ps发现大量PPID为1的僵尸进程,说明init进程不知为何不再回收僵尸进程了。

原因未查明(高度怀疑CentOS 6系统/内核bug,或是硬件问题?下图是查找到的一种可能性),并且僵尸进程还在不断增多,急呼运维同学reboot,遂恢复正常。

折腾了半天,今晚简单说说僵尸进程吧。

僵尸进程、产生原因及危害

"zombie process"或者"defunct process",是类Unix系统中的概念,指那些实际运行已经完成或终止[如通过exit()系统调用,或者发生错误、收到终止信号],但是在系统进程表中仍然残留着对应的进程项没有完全被清理的进程。僵尸进程已经释放了除进程表项外的所有内存空间,无法再被调度执行,只是在等待其他进程来收集它的退出状态信息而已。

在正常情况下,父进程会通过wait()waitpid()系统调用进行善后处理,即获取其子进程的退出状态。一旦成功获取,该僵尸子进程就会被销毁(reaped)而不复存在,其进程表项也会被释放。所以子进程退出时,应该只会在exit()wait()之间很短暂地处于僵尸状态。

但是,如果父进程没有通过wait()waitpid()系统调用获取其子进程的退出状态,也没有显式地处理或者忽略掉SIGCHLD信号[该信号是子进程结束时发送给父进程的],并且父进程保持运行,那么它的子进程结束后就一直处于僵尸状态了,此时就可以被用户观察到。在top命令下,僵尸进程的状态会显示为"Z",在ps命令下则会带上""标记。

很显然,如果僵尸进程一直不被销毁,那么它们将永远占用PID和进程表中的空间。Linux系统中的PID取值范围是有限制的(在/proc/sys/kernel/pid_max下,默认值32768),过多的僵尸进程会导致系统PID耗尽,无法再创建新的进程。

如何手动清理?

僵尸进程是无法被直接kill掉的,而造成僵尸进程无法销毁的罪魁祸首是它的父进程不做善后工作。所以,我们可以通过kill命令发送SIGKILL/SIGTERM信号直接干掉父进程,它的子进程就会成为所谓“孤儿进程”(orphan process)。孤儿进程会被根进程init收养,并且init进程会在后台执行wait()waitpid()系统调用,代替它们的父进程完成清理工作。

但是在极端情况下,仍然有可能出现init出现大量僵尸子进程的情况(比如本文开头),这时就只能干掉init进程——即重启系统了。

如何避免?

父进程一定要尽职尽责,避免出现长时间僵尸进程的方法有:

  • signal()系统调用中设定SIGCHLD信号的handler回调,并显式wait()处理之。
  • signal()系统调用中设定handler为SIG_IGN,即显式忽略该信号,子进程的回收会由内核直接负责。
  • 在产生子进程时做两次fork(),并立即杀掉一级子进程,令二级子进程(即真正的子进程)成为孤儿并被init收养。没那么“负责任”,但是也比较安全。

The End

民那晚安晚安。

版权声明:
作者:感冒的梵高
链接:https://www.techfm.club/p/45779.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>