点我查看操作系统秘籍连载


僵尸进程

进程从退出开始,到它的退出状态信息被父进程的wait/waitpid读走,在这个阶段中,进程将仍然存在于内核进程表中,这个阶段的进程的状态是终止态或称为僵尸态。而僵尸态进程称为僵尸进程,而父进程读子进程退出状态信息的动作则称为”reap”(这是一个术语,含义是收割、收走),即为子进程收尸。

虽然说僵尸进程听上去很可怕,但实际上僵尸进程的影响并不大。首先,它不占用任何CPU资源,因为它已经执行完成了,操作系统将不会调度到它(因为不在就绪队列中);其次,它占用的内存资源也非常少,仅占用一个进程表项的内存而已;最后,对于其它资源的占用(例如打开的文件描述符),也都基本完全释放了。所以,僵尸进程就算过多,也不会降低操作系统的性能。僵尸进程最大的影响是,当僵尸进程太多时,可能会占满Linux的进程表空间,使得无法再创建新进程,因为每个用户能够创建的进程数是有限的(ulimit -u命令可查看)。

在Shell下运行的所有命令,只要该命令不会产生子进程,就一定不会成为永久的僵尸进程,因为Shell启动命令的方式是fork+exec+wait,它会等待它的子进程终止并为子进程收尸。

但是,像Daemon类进程,这类进程一般会脱离终端脱离Shell进程,而且通常还会产生子进程,那么当子进程退出时,如果程序编码不完善,一直不去给子进程收尸,那么它的子进程将变成永久的僵尸进程。

解决僵尸进程的方式非常简单,杀掉僵尸进程的父进程即可。

孤儿进程

当进程的父进程先退出,那么这个子进程就会成为孤儿进程。所有的孤儿进程都会被pid=1的init进程收养,使得它们的父进程变成init进程,即PPID=1。

实际上,Shell下生成孤儿进程是非常容易的事情:

1
2
3
4
5
6
7
# (子)Shell中执行后台命令,(子)Shell退出后它就是孤儿进程
$ ( sleep 30 & )
$ ps -o pid,ppid,comm
PID PPID COMMAND
117281 117279 bash
122065 1 sleep # 这就是孤儿进程
122066 117281 ps

为了描述清楚子进程变成孤儿进程的过程,这里使用Shell伪代码描述下孤儿进程。

1
2
3
4
5
6
7
8
9
10
pid=$(fork)  # 从此开始,有两个进程分支

if [ $pid -eq 0 ];then
echo "Child Process" # 子进程
sleep 10 # 子进程睡眠10秒
exit
fi

# 父进程睡眠3秒
sleep 3

上面代码中的父进程睡眠3秒后退出,退出后其子进程仍然在执行,它会转移到init进程之下被init收养,7秒之后子进程退出。由于该子进程的父进程变成init进程,于是init进程负责为该子进程收尸。

现在回头想想,为什么(sleep 30 &)的sleep进程为什么会变成孤儿进程?

如果,某个父进程下已经有永久的僵尸子进程,当这个父进程退出后,这些子进程都将被pid=1的init进程收养,而init进程有一个特性,它会在接收到子进程的时候检查检查子进程是否是僵尸进程,是的话就收尸。