subprocess.Popen之preexec_fn参数
1、场景
Python 标准库 subprocess.Popen
是 shellout 一个外部进程的首选,它在 Linux/Unix 平台下的实现方式是 fork
产生子进程然后 exec
载入外部可执行程序。
于是问题就来了,如果我们需要一个类似“夹具”的子进程(比如subprocess子进程中还run子子进程), 那么就需要在退出上下文的时候清理现场,也就是结束被跑起来的子进程。
2、简单粗暴的做法
def process_fixture(shell_args):
proc = subprocess.Popen(shell_args)
try:
yield
finally:
# 无论是否发生异常,现场都是需要清理的
proc.terminate()
# 必须wait,如果子进程被中止了而父进程继续运行, 子进程就会一直占用 pid 而成为僵尸进程,直到父进程也中止了才被托孤给 init进程 清理掉
proc.wait()
if __name__ == '__main__':
with process_fixture(['python', 'SimpleHTTPServer', '8080']) as proc:
print('pid %d' % proc.pid)
print(urllib.urlopen('http://localhost:8080').read())
这个简单粗暴版对简单的情况可能有效,但是被运行程序可能会再 fork
一些子进程来工作,自己则只monitor —— 这是不少 Web Server 的做法。 对这种被运行程序如果简单地 terminate
,也即对其 pid
发 SIGTERM
, 那就相当于谋杀了monitor进程,真正的工作进程也就因此被托孤给 init
,变成畸形的守护进程。
3、使用preexec_fn参数
这个问题稍微有点棘手,因为自从被运行程序 fork
以后,产生的子进程都享有独立的进程空间和 pid
,也就是它超出了我们触碰的范围。
subprocess.Popen
有个 preexec_fn
参数,它接受一个回调函数,并在 fork
之后 exec
之前的间隙中执行它。我们可以利用这个特性对被运行的子进程做出一些修改,比如执行 setsid()
成立一个独立的进程组。
Linux 的进程组是一个进程的集合,任何进程用系统调用 setsid
可以创建一个新的进程组,并让自己成为首领进程。首领进程的子子孙孙只要没有再调用 setsid
成立自己的独立进程组,那么它都将成为这个进程组的成员。 之后进程组内只要还有一个存活的进程,那么这个进程组就还是存在的,即使首领进程已经死亡也不例外。 而这个存在的意义在于,我们只要知道了首领进程的 pid
(同时也是进程组的 pgid
), 那么可以给整个进程组发送 signal
,组内的所有进程都会收到。
因此利用这个特性,就可以通过 preexec_fn
参数让 Popen
成立自己的进程组, 然后再向进程组发送 SIGTERM
或 SIGKILL
,中止 subprocess.Popen
所启动进程的子子孙孙。当然,前提是这些子子孙孙中没有进程再调用 setsid
分裂自立门户成立各自的进程组。
import signal
import os
import subprocess
def process_fixture(shell_args):
# 成立一个进程组,并由自己作为master进程
proc = subprocess.Popen(shell_args, preexec_fn=os.setsid)
try:
yield
finally:
proc.terminate()
proc.wait()
try:
os.killpg(proc.pid, signal.SIGTERM)
except OSError as e:
print(e)
python 3.2 之后 subprocess.Popen
新增了一个选项 start_new_session
, Popen(args, start_new_session=True)
即等效于 preexec_fn=os.setsid
。
4、总结分析
这种利用进程组来清理子进程的后代的方法,比简单地中止子进程本身更加“干净”。基于 Python 实现的 Procfile 进程管理工具 Honcho 也采用了这个方法。
当然,因为不能保证被运行进程的子进程一定不会调用 setsid
, 所以这个方法不能算“通用”,只能算“相对可用”。
如果真的要百分之百通用,那么像 systemd 那样使用 cgroups
来追溯进程创建过程也许是唯一的办法。也难怪说 systemd 是第一个能正确地关闭服务的 init 工具。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
2020-11-24 py文件头声明注释