僵尸进程,大家都会对其嗤之以鼻,敬而远之,毕竟臭名在外。
孤儿进程,大家对其都很宽容,甚至可以说是放纵,只因系统会收留。
然而,在实际应用中,孤儿进程虽然不会给系统造成直接性的危害,但更多时候会对业务造成一些影响,如当子进程为一个基于tcp的socket服务时,会造成主进程再次启动时无法启动,端口被占用。主进程退出了,子进程会因为无法获得某些资源,而变成业务上的"僵尸进程",这实际也是资源浪费。对于一些有进程监控的服务来说,可能会造成业务主服务无法重启,或是进程不可控。
鉴于这些情况下,很多时候是不希望产生孤儿进程的,子进程应随父进程结束而结束。
本文就小说一把如何做一个有担当的"父亲",不要不负责任的"一走了之",随意丢弃自己的"孩子们"。
什么是孤儿进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成中止后的资源回收工作
通过下面的具体例子,具体看看
centralized_in_out 服务会启动8个子进程,父进程ID为5310,子进程ID为5312-5319
将父进程(5310)kill掉,可以看到子进程5312-5319全由ID为1的进程接管
如何做
上面看到子进程5312-5319被init进程接管了,但这不是我想要的结果,当前业务中,会再次拉起centralized_in_out服务,会再启动8个子进程,这样进程数太多,会失控,不符合业务需求。
我需要的是”父子共进退“,如何做呢?
豆瓣的工程师们,已经给出了解决办法,具体参见:
https://github.com/douban/CaoE
修改代码,用起来,效果如下
为什么
豆瓣工程师给出了解决办法,不能只拿来用用,得问几个为什么?通过什么实现的?为什么要这么做呢?
下面具体分析下实现方法:
1. 方法概述
实现思路是通过创建一个子进程和孙子进程,子进程会监控父进程的状态,当检测到父进程退出后,会给进程组发送信号通知杀死孙子进程及其子进程。
这里涉及到进程组和信号两个重要概念,下面具体阐述。
2. 概念阐述
进程组:每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程。进程组会有一个领导进程 (process group leader),领导进程的PID成为进程组的ID (process group ID, PGID),用来标识进程组。
如下图所示,centralized_in_out服务父进程的ID为5538(它的PGID为5538),子进程ID为5540(它的PGID为5540),孙子进程的ID为5541(它的PGID为5540),孙孙进程5542-5549的PGID都为5541
信号:具体概念这里不多说了,有些大,而且晦涩难懂。主要涉及信号定义和处理函数的注册绑定,后面结合代码具体说明
3. 实现详解
def install(fork=True, sig=SIGTERM):
def _reg(gid):
handler = make_quit_signal_handler(gid, sig)
signal(SIGINT, handler)
signal(SIGQUIT, handler)
signal(SIGTERM, handler)
signal(SIGCHLD, make_child_die_signal_handler(gid, sig))
if not fork:
_reg(os.getpid())
return
pid = os.fork()
if pid == 0:
# child process
os.setpgrp()
pid = os.fork()
if pid != 0:
# still in child process
exit_when_parent_or_child_dies(sig)
# grand child process continues...
else:
# parent process
gid = pid
_reg(gid)
while True:
pause()
通过两次fork,创建子进程(ID:5540)和孙进程(ID:5541),
其中子进程中有重要的一步,os.setpgrp()将子进程的进程组ID(5540)设为当前进程组的ID,后面孙进程和孙孙进程的进程组ID都为5540。
子进程在exit_when_parent_or_child_dies方法中循环等待父进程状态,当PPID为1时,说明父进程已退出,通过killpg()将进程组中的所有进程(孙孙进程)杀死,然后自己退出。
def exit_when_parent_or_child_dies(sig):
gid = os.getpgrp()
signal(SIGCHLD, make_child_die_signal_handler(gid))
try:
import prctl
signal(SIGHUP, make_quit_signal_handler(gid))
# give me SIGHUP if my parent dies
prctl.set_pdeathsig(SIGHUP)
while True:
pause()
except ImportError:
# fallback to polling status of parent
while True:
if os.getppid() == 1:
# parent died, suicide
signal(SIGTERM, SIG_DFL)
os.killpg(gid, sig)
sys.exit()
time.sleep(PARENT_POLL_INTERVAL)
到此,整个流程就清晰了,通过设置孙进程和孙孙进程的进程组ID为子进程的进程ID,当主进程退出,子进程被init进程接管时,通过killpg将同一个进程组ID的孙进程和孙孙进程中止。
但如果仔细看代码,
exit_when_parent_or_child_dies方法中:
if os.getppid() == 1: 永远执行不到,因为父进程退出时,捕获如下信号
signal(SIGINT, handler)
signal(SIGQUIT, handler)
signal(SIGTERM, handler)
signal(SIGCHLD, make_child_die_signal_handler(gid, sig))
而这些信号处理方法中都会通过killpg杀死进程组,子进程也属于这个进程组,也会被kill掉。