Python避免孤儿进程

3

我正在使用Python对某些东西进行基准测试。这可能需要很长时间,因此我想设置一个(全局)超时时间。我使用以下脚本(摘要):

class TimeoutException(Exception):
    pass
def timeout_handler(signum, frame):
    raise TimeoutException()

# Halt problem after half an hour
signal.alarm(1800)
try:
    while solution is None:
        guess = guess()
        try:
            with open(solutionfname, 'wb') as solutionf:
                solverprocess = subprocess.Popen(["solver", problemfname], stdout=solutionf)
                solverprocess.wait()
        finally:
            # `solverprocess.poll() == None` instead of try didn't work either
            try:
                solverprocess.kill()
            except:
                # Solver process was already dead
                pass
except TimeoutException:
    pass
# Cancel alarm if it's still active
signal.alarm(0)

但是有时它会产生孤儿进程,但我无法可靠地重现情况。有没有人知道防止这种情况的正确方法?


第一行不应该是一个类吗? - user3058846
@RenaeLider 是的,在创建示例时复制错误。 - dtech
Python: 如何在父进程死亡时终止子进程(es)? - jfs
3个回答

2
kill()方法的文档说明如下:

杀死子进程。在 POSIX 操作系统上,该函数向子进程发送 SIGKILL 信号。 在 Windows 上,kill()terminate() 的别名。

换句话说,如果你不在 Windows 上,你只是向子进程发送一个信号。 这将创建一个僵尸进程,因为父进程没有读取子进程的返回值。 kill()terminate() 方法只是 send_signal(SIGKILL)send_signal(SIGTERM) 的快捷方式。
尝试在 kill() 后添加对 wait() 的调用。甚至在communicate()的文档示例中都有这样的提示。
proc = subprocess.Popen(...)
try:
    outs, errs = proc.communicate(timeout=15)
except TimeoutExpired:
    proc.kill()
    outs, errs = proc.communicate()

请注意在 kill() 后调用 communicate()。这相当于调用 wait() 并读取子进程的输出信息。


我想澄清一件事情:你似乎不完全理解什么是“僵尸进程”。僵尸进程是一个已经终止的进程。内核将该进程保留在进程表中,直到父进程读取其退出状态。我相信子进程使用的所有内存实际上都被重复使用了;内核只需要跟踪此类进程的退出状态。
因此,您看到的僵尸进程并没有在运行。它们已经完全死亡,这就是为什么它们被称为“僵尸”。它们在进程表中“存在”,但实际上根本没有运行。
调用 wait() 正好可以做到这一点:等待子进程结束并读取其退出状态。这允许内核从进程表中删除子进程。

你说得没错,我是指“孤儿进程”。它们肯定没有死因为它们不断地消耗着CPU和RAM。我会尝试你和丹尼尔的建议。 - dtech
@dtech 这是完全不同的情况。当父进程死亡时,孤儿进程就会出现。然后它就成为 init 的子进程。在您的示例中,我真的看不出这些可能来自哪里。需要更多关于您启动的子进程的信息。另外,您确定要 kill 掉该子进程吗?可能会导致孤儿进程的创建,因为 kill 不允许子进程执行任何清理操作。您应该首先尝试调用 terminate,然后如果失败了,再调用 kill - Bakuriu
你说的对,SIGTERM可能更好一些,但我不知道SIGKILL怎么会导致进程无法被杀死并变成孤儿进程。这个子进程是一个SAT求解器。我会尝试使用terminate,在一段时间后再调用kill。 - dtech
@dtech Sat求解器可能会生成一些子进程来执行实际计算。您的SIGKILL仅杀死主进程。使用SIGTERM可能允许主进程在退出之前终止其子进程,这取决于它的实现方式。 - Bakuriu

2
在Linux上,您可以使用python-prctl
定义一个preexec函数,例如:
def pre_exec():
    import signal
    prctl.set_pdeathsig(signal.SIGTERM)

将您的Popen调用传递给它。
subprocess.Popen(..., preexec_fn=pre_exec)

就是这么简单。现在,如果父进程死亡,子进程将会结束而不是变成孤儿进程。

如果你不喜欢依赖于python-prctl,你也可以使用旧的prctl。代替方式如下:

prctl.set_pdeathsig(signal.SIGTERM)

你会有。
prctl.prctl(prctl.PDEATHSIG, signal.SIGTERM)

2
您只需要在终止进程后等待即可。

那么如果调用 p.kill(),但在 Python 退出之前 p 尚未退出,那么 p 不会被杀死吗?我会测试一下是否正确,但这是为什么呢?据我所知,p.kill() 会向 p 发送 SIGKILL,即使 Python 退出,也应该导致 p 被终止。 - dtech
1
@dtech 进程已经被杀死,但是内核没有将其删除,因为它正在等待父进程读取其状态。 - Bakuriu
@dtech:进程已经死亡(SIGKILL总是有效的),但僵尸进程仍然存在,直到被收割。如果原始父进程已经死亡,则专用进程(例如init 1)将收集状态。注意:孤儿进程必须按定义保持活动状态(其父进程已死)。您的代码创建了僵尸进程,而不是孤儿进程。 - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接