使用subprocess.run时如何确保超时子进程被杀死?
我用以下Python代码启动子进程:
# 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, )
启动的子进程是带shebang的Python程序,会执行大量计算并写入文件,没有任何信号处理逻辑,可能会超出指定的timeout时长。
根据subprocess.run官方文档,超时后子进程会被杀死:
The timeout argument is passed to Popen.communicate(). If the timeout
expires, the child process will be killed and waited for. The
TimeoutExpired exception will be re-raised after the child process has
terminated.
但实际情况是,超时后我总会收到subprocess.TimeoutExpired异常,但偶尔子进程没被杀死,还在消耗机器资源。请问我操作有误吗?如果有误问题出在哪?如果操作没问题,这个问题的原因是什么,该怎么解决?
注:我使用Ubuntu 22.04系统与Python 3.10版本。
你的操作本身没有错误,出现这种情况主要有以下几个原因及对应解决办法:
可能原因
中间shell进程导致子进程脱离控制
带shebang的脚本会触发/bin/sh来执行,此时实际的Python计算进程是sh的子进程。默认情况下subprocess.run超时后只向直接启动的sh进程发送终止信号,若sh退出后,其子进程(Python进程)会被init进程接管,继续运行。preexec_fn的潜在干扰
你使用的set_memory_limits函数如果修改了进程组设置或信号相关逻辑,可能会干扰subprocess的超时清理流程,导致子进程脱离父进程的控制范围。
解决办法
1. 统一管控进程组
启动子进程时通过start_new_session=True创建新会话,或在preexec_fn中显式设置进程组,确保超时后信号能覆盖整个进程组的所有进程:
import os subprocess_result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, timeout=timeout, cwd=directory, env=env, preexec_fn=lambda: (set_memory_limits(), os.setpgrp()), start_new_session=True )
2. 跳过shell直接启动Python进程
如果可以,把cmd从指向脚本的路径改为直接调用Python解释器加脚本路径,避免中间shell进程的产生:
# 把cmd从 ["/path/to/script.py"] 改成 cmd = ["python3", "/path/to/script.py"] subprocess_result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, timeout=timeout, cwd=directory, env=env, preexec_fn=set_memory_limits, )
3. 捕获异常后手动清理进程
捕获TimeoutExpired异常后,主动检查并杀死主进程及其所有子进程,可借助psutil库实现彻底清理:
import signal import psutil try: subprocess_result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, timeout=timeout, cwd=directory, env=env, preexec_fn=set_memory_limits, ) except subprocess.TimeoutExpired as e: # 尝试杀死主进程 if e.poll() is None: try: os.kill(e.pid, signal.SIGKILL) os.waitpid(e.pid, 0) except OSError: pass # 清理所有子进程 try: parent_proc = psutil.Process(e.pid) for child in parent_proc.children(recursive=True): child.kill() parent_proc.kill() except (psutil.NoSuchProcess, ImportError): pass raise
(使用前需安装psutil:pip install psutil)
4. 检查set_memory_limits函数
确认该函数没有执行阻止信号传递的操作,比如修改不可捕获的SIGKILL逻辑,或设置资源限制导致进程进入不可中断的IO阻塞状态。
内容的提问来源于stack exchange,提问作者Manuel Selva

