车祸现场
今天下午,笔者正在认真搬砖,日志集群中有一台机器忽然报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命令下则会带上"<defunct>"标记。
很显然,如果僵尸进程一直不被销毁,那么它们将永远占用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
民那晚安晚安。
网友评论