Python处理僵尸进程

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,也就是没有子进程等待 。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大帅不是我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值