我有一个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 启动一个进程,并且超时会杀死整个进程树,从所述进程一直到它的最后一个后代?
适用于 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 不兼容。)
这将使用
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)