操作系统修炼秘籍(30):Linux僵尸进程和孤儿进程
僵尸进程
进程从退出开始,到它的退出状态信息被父进程的wait/waitpid读走,在这个阶段中,进程将仍然存在于内核进程表中,这个阶段的进程的状态是终止态或称为僵尸态。而僵尸态进程称为僵尸进程,而父进程读子进程退出状态信息的动作则称为”reap”(这是一个术语,含义是收割、收走),即为子进程收尸。
虽然说僵尸进程听上去很可怕,但实际上僵尸进程的影响并不大。首先,它不占用任何CPU资源,因为它已经执行完成了,操作系统将不会调度到它(因为不在就绪队列中);其次,它占用的内存资源也非常少,仅占用一个进程表项的内存而已;最后,对于其它资源的占用(例如打开的文件描述符),也都基本完全释放了。所以,僵尸进程就算过多,也不会降低操作系统的性能。僵尸进程最大的影响是,当僵尸进程太多时,可能会占满Linux的进程表空间,使得无法再创建新进程,因为每个用户能够创建的进程数是有限的(ulimit -u
命令可查看)。
在Shell下运行的所有命令,只要该命令不会产生子进程,就一定不会成为永久的僵尸进程,因为Shell启动命令的方式是fork+exec+wait,它会等待它的子进程终止并为子进程收尸。
但是,像Daemon类进程,这类进程一般会脱离终端脱离Shell进程,而且通常还会产生子进程,那么当子进程退出时,如果程序编码不完善,一直不去给子进程收尸,那么它的子进程将变成永久的僵尸进程。
解决僵尸进程的方式非常简单,杀掉僵尸进程的父进程即可。
孤儿进程
当进程的父进程先退出,那么这个子进程就会成为孤儿进程。所有的孤儿进程都会被pid=1的init进程收养,使得它们的父进程变成init进程,即PPID=1。
实际上,Shell下生成孤儿进程是非常容易的事情:
1 | (子)Shell中执行后台命令,(子)Shell退出后它就是孤儿进程 |
为了描述清楚子进程变成孤儿进程的过程,这里使用Shell伪代码描述下孤儿进程。
1 | pid=$(fork) # 从此开始,有两个进程分支 |
上面代码中的父进程睡眠3秒后退出,退出后其子进程仍然在执行,它会转移到init进程之下被init收养,7秒之后子进程退出。由于该子进程的父进程变成init进程,于是init进程负责为该子进程收尸。
现在回头想想,为什么(sleep 30 &)
的sleep进程为什么会变成孤儿进程?
如果,某个父进程下已经有永久的僵尸子进程,当这个父进程退出后,这些子进程都将被pid=1的init进程收养,而init进程有一个特性,它会在接收到子进程的时候检查检查子进程是否是僵尸进程,是的话就收尸。