【问题标题】:How ensure subprocess is killed on timeout when using `run`?使用`run`时如何确保子进程在超时时被杀死?
【发布时间】:2022-11-23 04:34:46
【问题描述】:

我正在使用以下代码启动子进程:

# Run the program
subprocess_result = subprocess.run(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=False,
                timeout=timeout,
                cwd=directory,
                env=env,
                preexec_fn=set_memory_limits,
            )

启动的子进程也是一个 Python 程序,带有一个 shebang。 此子进程可能持续时间超过指定的timeout。 子进程进行大量计算并将结果写入文件,并且不包含任何信号处理程序。

根据文档https://docs.python.org/3/library/subprocess.html#subprocess.runsubprocess.run 杀死超时的孩子:

超时参数传递给 Popen.communicate()。如果超时 过期,子进程将被杀死并等待。这 TimeoutExpired 异常将在子进程完成后重新引发 终止。

当我的子进程超时时,我总是收到 subprocess.TimeoutExpired 异常,但有时子进程没有被杀死,因此仍在消耗我机器上的资源。

所以我的问题是,我在这里做错了什么吗?如果是,是什么,如果不是,为什么我会遇到这个问题,我该如何解决?

注意:我在 Ubuntu 22_04 上使用 Python 3.10

【问题讨论】:

  • @S.B 我的脚本做了一些繁重的计算并将结果写入文件,没有任何信号处理程序。不,不幸的是我无法确定脚本在超时后仍在运行的情况。您的评论是否表明我对文档的理解是正确的,因此理论上应该终止子进程?
  • 什么可以正在发生的是您的子进程实际上正在产生一个单独的进程来执行计算。 subprocess.run会杀了孩子,但孙辈会由1继承。如果没有看到您正在运行的实际进程,这是不可能诊断的,但是鉴于您所说的(这些是“繁重的计算”),多处理似乎已经到位。
  • 解决这个问题的方法是修改您的子进程以接受信号以执行适当的清理,或者编写一个包装脚本来简单地接收信号,杀死它的所有后代,然后死去
  • 谢谢你的 cmets @Bakuriu 我会从那边看,但事实是子进程不是我的 :) 而且我认为即使他们不再启动子进程,其中一些也不会被杀死,但我需要检查那个。
  • @ManuelSelva 好的。查看 subprocess.run 的源代码,它使用 .kill() method on timeoutsends SIGKILLcannot be handled。所以我相信在你的情况下你真的不能做太多。不要使用timeout并以其他方式实现超时

标签: python subprocess kill-process


【解决方案1】:

您看到的行为最有可能的罪魁祸首是您生成的子进程可能正在使用多处理并生成自己的子进程。杀死父进程确实不是自动杀死整套后代。孙子被init进程(即PID为1的进程)继承,并将继续运行。

你可以从suprocess.run 的源代码中验证:

with Popen(*popenargs, **kwargs) as process:
    try:
        stdout, stderr = process.communicate(input, timeout=timeout)
    except TimeoutExpired as exc:
        process.kill()
        if _mswindows:
            # Windows accumulates the output in a single blocking
            # read() call run on child threads, with the timeout
            # being done in a join() on those threads.  communicate()
            # _after_ kill() is required to collect that and add it
            # to the exception.
            exc.stdout, exc.stderr = process.communicate()
        else:
            # POSIX _communicate already populated the output so
            # far into the TimeoutExpired exception.
            process.wait()
        raise
    except:  # Including KeyboardInterrupt, communicate handled that.
        process.kill()
        # We don't call process.wait() as .__exit__ does that for us.
        raise

在这里你可以看到 at line 550 超时设置在 communicate 调用上,如果它触发 at line 552 the subprocess is .kill()edThe kill method 发送 SIGKILL 立即终止子进程而不进行任何清理。这是一个子进程无法捕获的信号,因此子进程不可能以某种方式忽略它。

TimeoutException 然后重新引发 at line 564,因此如果您的父进程看到此异常,则子进程已经死了。

然而,这并没有说明孙进程。这些将继续作为 PID 1 的子进程运行。

我看不出有什么方法可以自定义 subprocess.run 处理子进程终止的方式。例如,如果它使用SIGTERM而不是SIGKILL,您可以修改您的子进程或编写一个包装进程来捕获信号并正确杀死其所有后代。但是 SIGKILL 不会给你这种奢侈。

所以我相信对于您的用例,您不能使用 subprocess.run 外观,但您应该直接使用 Popen。您可以查看 subprocess.run 实现并只获取您需要的东西,也许会放弃对您不使用的平台的支持。


注意:在极少数情况下,子进程不会在收到 SIGKILL 时立即终止。我相信发生这种情况的唯一情况是子进程正在执行一个很长的系统调用或其他内核操作,这些操作可能不会立即被中断。如果操作处于死锁状态,这可能会阻止进程永远终止。但是我不认为这是你的情况,因为你没有提到进程卡住了什么都不做,但从你所说的来看,进程似乎只是继续运行。

【讨论】:

    猜你喜欢
    • 2010-12-08
    • 2011-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-09
    • 2011-05-08
    • 1970-01-01
    • 2013-09-19
    相关资源
    最近更新 更多