如何在Python中超时杀死子进程及其所有后代?

问题描述 投票:0回答:2

我有一个Python程序,它启动这样的子进程:

subprocess.run(cmdline, shell=True)

子进程可以是任何可以从 shell 执行的东西:Python 程序、任何其他语言的二进制文件或脚本。

cmdline
类似于
export PYTHONPATH=$PYTHONPATH:/path/to/more/libs; command --foo bar --baz 42

一些子进程会启动自己的子进程,理论上这些子进程可以再次启动子进程。对于后代进程可以有多少代没有硬性限制。子进程的子进程是第三方工具,我无法控制它们如何启动其他进程。

该程序需要在 Linux(目前仅基于 Debian 的发行版)以及一些 FreeBSD 衍生版本上运行 - 因此它需要在各种类 Unix 操作系统之间移植,而在可预见的将来可能不需要 Windows 兼容性。它应该通过操作系统包管理器安装,因为它附带操作系统配置文件,并且目标系统上的其他所有内容也使用它。这意味着我不想为我的程序或任何依赖项使用 PyPI。

如果子进程挂起(可能是因为它正在等待挂起的后代),我想实现一个超时来杀死子进程以及以子进程作为祖先的任何内容。

subprocess.run()
上指定超时不起作用,因为只有直接子进程会被终止,但其子进程会被 PID 1 采用并继续运行。另外,从
shell=True
开始,子进程就是 shell,实际的命令作为 shell 的子进程,将愉快地继续。后者可以通过传递适当的
args
env
并跳过 shell 来解决,这将杀死命令的实际进程,但不会杀死它的任何子进程。

然后我尝试直接使用

Popen

with Popen(cmdline, shell=True) as process:
    try:
        stdout, stderr = process.communicate(timeout=timeout)
    except subprocess.TimeoutExpired:
        killtree(process)    # custom function to kill the whole process tree

除了,为了编写

killtree()
,我需要列出
process
的所有子进程,并递归地对每个子进程执行相同的操作,然后一一杀死这些进程。
os.kill()
提供了一种方法来终止任何进程(给定其 PID),或向其发送我选择的信号。但是,
Popen
没有提供任何枚举子进程的方法,我也不知道有任何其他方法。

其他一些答案建议psutil,但这需要我安装 PyPI 软件包,我想避免这样做。

TL;DR:考虑到上述限制,有没有办法从 Python 启动一个进程,并且超时会杀死整个进程树,从所述进程一直到它的最后一个后代?

python linux subprocess freebsd
2个回答
0
投票

适用于 Linux 和 FreeBSD 的快速而简单的解决方案:

with Popen(cmdline, shell=True) as process:
    try:
        stdout, stderr = process.communicate(timeout=timeout)
    except subprocess.TimeoutExpired:
        killtree(process.pid)
        process.wait()

def killtree(pid):
    args = ['pgrep', '-P', str(pid)]
    try:
        for child_pid in subprocess.check_output(args).decode("utf8").splitlines():
            killtree(child_pid)
    except subprocess.CalledProcessError as e:
        if e.returncode != 1:
            print("Error: pgrep exited with code {0}".format(e.returncode))
    os.kill(int(pid), signal.SIGKILL)

我们只依赖操作系统的命令行工具 - Linux 和 FreeBSD 都提供

pgrep -P PID
来枚举给定进程的所有子进程。如果没有子进程,
pgrep
退出并返回代码 1,引发我们需要处理的
CalledProcessError

依赖外部工具来终止子进程可能不是最好的解决方案,但它确实有效,并且相关代码库已经依赖外部命令行工具来完成其工作,因此依赖

pgrep
并不能显着提高效果比实际情况更糟糕。

subprocess.run()
一样,我们需要在杀死子进程后调用
process.wait()
。 (这适用于 POSIX – 在 Windows 上我们会调用
process.communicate()
,但无论如何这个代码片段可能与 Windows 不兼容。)


0
投票

这将使用

ps
作为外部进程,并从当前(或给定)进程收集有关子进程的所有信息,并将这些信息作为字符串化的 PID 列表返回:

import os

def flatten(tree, start):
    children = tree.get(start, [])
    grandchildren = []
    for child in children:
        grandchildren.extend(flatten(tree, child))
    return children + grandchildren

def process_tree(parent_pid=None):
    if parent_pid is None:
        parent_pid = str(os.getpid())
    process_data = list(os.popen("ps -ef --forest"))
    tree = {}
    for line in process_data[1:]:
        if not line.strip():
            continue
        _, pid, ppid, *_ = line.split()
        tree.setdefault(ppid, []).append(pid)
    return flatten(tree, parent_pid)
© www.soinside.com 2019 - 2024. All rights reserved.