1. 僵尸进程概念
在类UNIX操作系统中,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束
僵尸进程:一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。
危害:如果子进程在exit()之后,父进程没有来得及处理,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
总结地说:子进程退出了,但并没有通知主线程回收,子进程残留的资源存放于内核之中。(附:kill命令清除不了僵尸进程的,因为kill命令只是用来终止进程的,而僵尸进程已经是终止的了,但是我们可以kill掉僵尸进程的父进程,这样僵尸进程的父进程就变为init进程,init进程自然会调用wait或者waitpid清除这个僵尸进程。)
2. python解决
先模拟一下出现僵尸进程的情况,如下代码:
启动了一个子进程执行func1函数,此函数执行了os._exit(3)方法,会直接退出python解释器,不释放此子进程的资源,导致此子进程成为僵尸进程。随后主线程睡眠100秒,在这100秒聂
# coding:utf-8
import os
import time
import signal
import logging
from multiprocessing import Process, current_process
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)-15s - %(levelname)s - %(message)s'
)
def func1():
logging.info(f"func1 start, pid is {current_process().pid}")
time.sleep(5)
logging.info("func1 exit")
# _exit(), 该方法直接退出 Python 解释器, 不抛异常, 不执行相关清理工作,其后的代码都不执行
# 执行此方法会导致僵尸进程出现
os._exit(3)
if __name__ == '__main__':
logging.info(f"main process start, pid is {current_process().pid}")
p = Process(target=func1)
p.start()
time.sleep(10)
logging.info("main process exit")
执行现象:
5秒后子进程成为僵尸进程,在父进程sleep(10)期间都会一直占用资源
python处理僵尸进程有两个方法,推荐使用os.waitpid
os.
wait
()
调用该方法,会一直阻塞,直到找到一个僵尸进程将它销毁才进行返回。等待子进程执行完毕,返回一个元组,包含其 pid 和退出状态指示
os.waitpid(pid, options)
该方法是os.wait()的升级版,在 Unix 上:等待进程号为 pid 的子进程执行完毕,返回一个元组,内含其进程 ID 和退出状态指示(编码与 wait() 相同)。调用的语义受整数 options 的影响,常规操作下该值应为 0。
其中pid参数有以下几种选值:
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去;
pid=0时,则获取当前进程所在进程组中的所有子进程的状态,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid<0时,表示回收指定进程组内的任意子进程,比如-8888表示回收进程组id为8888的任意子进程
附:一次wait或waitpid调用只能清理掉一个子进程,清理多个子进程应该使用循环调用多次
其中options参数有以下几种选值:
os.WNOHANG,表示如果没有立即可用的子进程状态,则立即返回。在这种情况下,函数返回 (0, 0)。
os.WUNTRACED,已停止的子进程,如果自停止以来尚未报告其当前状态,则此选项将报告这些子进程。
其他参数值参考:os --- 多种操作系统接口 — Python 3.10.2 文档
函数的返回值位一个元组,一般用cpid,status=os.waitpid(pid,options)来接收
其中cpid是终止的子进程ID,当options设置成os.WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回cpid=0;status是终止子进程的状态,正常退出值为0,异常时返回 -1 时,将抛出带有错误码的 OSError 异常,可以查看errno的值。
另外介绍一下signal模块中的方法
signal.signal(signal.SIGCHLD, handle_func)
该方法会在子进程退出时调用handle_func函数进行处理,signal.SIGCHLD表示遇到子进程退出时触发信号,handle_func相当一个回调函数。(注:只能在主线程中使用signal.signal)
针对以上僵尸进程的例子,我们使用os.waitpid进程回收进程。
# coding:utf-8
import os
import time
import signal
import logging
from multiprocessing import Process, current_process
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)-15s - %(levelname)s - %(message)s'
)
def func1():
logging.info(f"func1 start, pid is {current_process().pid}")
time.sleep(5)
logging.info("func1 exit")
# _exit(), 该方法直接退出 Python 解释器, 不抛异常, 不执行相关清理工作,其后的代码都不执行
# 执行此方法会导致僵尸进程出现
os._exit(3)
# 需要接受两个参数,写成*args即可
def handle_func(*args):
logging.info("子进程退出时触发该信号")
# -1 表示监控任何子进程的退出
# os.WNOHANG 如果没有立即可用的子进程状态,则立即返回 (0,0)
cpid, status = os.waitpid(-1, os.WNOHANG)
logging.info(f"cpid: {cpid}, status: {status}")
if __name__ == '__main__':
logging.info("main process start")
# 添加一个处理信号的程序,每当有子进程退出,都会执行handle_func函数
signal.signal(signal.SIGCHLD, handle_func)
p = Process(target=func1)
p.start()
time.sleep(20)
logging.info("main process exit")
执行现象:5s后触发信号函数handle_func,调用os.waitpid处理了退出的子进程
可以看到子进程已经正常退出了
接下来稍微改动一下我们的代码,新增func2函数,不执行os._exit()方法,让func2函数正常退出,我们看看现象:
# coding:utf-8
import os
import time
import signal
import logging
from multiprocessing import Process, current_process
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)-15s - %(levelname)s - %(message)s'
)
def func1():
logging.info("func1 start, pid is {}".format(current_process().pid))
time.sleep(5)
logging.info("func1 exit")
# _exit(), 该方法直接退出 Python 解释器, 不抛异常, 不执行相关清理工作,其后的代码都不执行
# 执行此方法会导致僵尸进程出现
os._exit(3)
def func2():
logging.info("func2 start, pid is {}".format(current_process().pid))
time.sleep(5)
logging.info("func2 exit")
# 需要接受两个参数,写成*args即可
def handle_func(*args):
logging.info("子进程退出时触发该信号")
# -1 表示监控任何子进程的退出
# os.WNOHANG 如果没有立即可用的子进程状态,则立即返回 (0,0)
# 父进程调用waitpid方法回收子进程
cpid, status = os.waitpid(-1, os.WNOHANG)
logging.info("cpid: {}, status: {}".format(cpid, status))
if __name__ == '__main__':
logging.info("main process start")
# 添加一个处理信号的程序,每当有子进程退出,都会执行handle_func函数
signal.signal(signal.SIGCHLD, handle_func)
f1 = Process(target=func1)
f1.start()
f2 = Process(target=func2)
f2.start()
time.sleep(20)
logging.info("main process exit")
现象:
因为func2是正常退出的,所以status的值为0,hanlde_func这个函数我们可以看到执行了两次,说明signal.signal()这个方法会持续监控子进程,对每个子线程退出,都会触发信号。
注:python3 和 python2的执行结果不同
python2 调用一次signal.signal时,会在主程序退出阻塞,直接执行下面的代码,所以可以看到python2最后一句打印也是在5s之后,
对于python2的环境,可以修改一下代码:
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
SIG_IGN :忽略的处理方式,这个方式和默认的忽略是不一样的语意,暂且我们把忽略定义为SIG_IGN,
在这种方式下, 子进程状态信息会被丢弃,也就是被内核自动回收了,所以不会产生僵尸进程,但是问题也就来了,
wait,waitpid却无法捕捉到子进程状态信息了, 如果你随后调用了wait,那么会阻塞到所有的子进程结束,并返
回错误ECHILD,也就是没有子进程等待 。