Python中subprocess模块使用后常见问题与解决方案
用了 subprocess 的 Popen,发现主进程都退出了,子进程连用户注销之后都还能跑…
有木有办法一块都退出了
Python中subprocess模块使用后常见问题与解决方案
你要知道进程 id 呀,主进程只负责起,不负责灭,他主进程不知道紫禁城什么时候干完活了啊。要控制的话,要进程间通讯才行
Python中subprocess模块使用后常见问题与解决方案
subprocess模块是Python调用外部命令的核心工具,但用起来经常踩坑。下面我总结几个最常见的问题和对应的解决方案。
1. 命令执行后卡住,程序不继续(阻塞/死锁)
这是最经典的问题。当你用subprocess.Popen()执行一个产生大量输出或需要交互的命令,并且没有正确处理标准输出/错误流时,就会发生。
import subprocess
# ❌ 错误示例:如果命令输出很大,这会卡死
proc = subprocess.Popen(['some_command_that_outputs_a_lot'], stdout=subprocess.PIPE)
output = proc.stdout.read() # 可能会一直等,直到缓冲区满或进程结束
# ✅ 正确做法:使用communicate(),它会处理缓冲
proc = subprocess.Popen(['ls', '-la'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = proc.communicate() # 这会读取所有输出,避免死锁
print(stdout)
关键:对于需要读取输出的命令,总是用communicate()。它一次性收集所有输出,内部处理了缓冲问题。
2. 命令执行失败,但Python没报错 你调用了一个不存在的命令或命令返回非零退出码,但你的Python脚本默默继续了。
import subprocess
# ❌ 错误示例:命令失败但程序继续
result = subprocess.run(['git', 'log', '--oneline']) # 如果不在git仓库,git会失败
print("继续执行...") # 这行还是会执行
# ✅ 正确做法1:检查返回码
result = subprocess.run(['git', 'log', '--oneline'], capture_output=True, text=True)
if result.returncode != 0:
print(f"命令失败: {result.stderr}")
# 这里可以raise异常或处理错误
# ✅ 正确做法2:让subprocess在失败时直接抛出异常
try:
result = subprocess.run(
['git', 'log', '--oneline'],
check=True, # ← 关键参数:非零退出码时抛出CalledProcessError
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print(f"命令执行失败,退出码: {e.returncode}")
print(f"错误输出: {e.stderr}")
关键:要么用check=True让subprocess自动抛异常,要么手动检查returncode。
3. 路径/环境变量问题 在子进程中,环境变量可能和你的Python环境不同。
import subprocess
import os
# ❌ 可能找不到命令
subprocess.run(['my_custom_script.sh']) # 如果脚本不在PATH里,会失败
# ✅ 指定完整路径
subprocess.run(['/usr/local/bin/my_script.sh'])
# ✅ 修改子进程的环境变量
my_env = os.environ.copy()
my_env['MY_VAR'] = 'some_value'
subprocess.run(['echo', '$MY_VAR'], env=my_env, shell=True)
# ✅ 使用shell=True时注意安全性(有命令注入风险)
# 如果必须用shell=True,避免直接拼接用户输入
user_input = "hello" # 假设来自用户
# ❌ 危险!
# subprocess.run(f'echo {user_input}', shell=True) # 如果user_input是"hello; rm -rf /"就完了
# ✅ 相对安全:使用列表形式
subprocess.run(['echo', user_input], shell=False)
关键:尽量用绝对路径,谨慎使用shell=True,特别是处理用户输入时。
4. 编码问题(Windows特别常见) Windows的控制台默认编码经常不是UTF-8,导致输出乱码。
import subprocess
# ❌ 在Windows上可能乱码
result = subprocess.run(['dir'], capture_output=True, shell=True)
print(result.stdout) # 可能是乱码
# ✅ 指定编码
result = subprocess.run(
['dir'],
capture_output=True,
shell=True,
encoding='cp936', # Windows中文版控制台常用编码
# 或者用 'utf-8' 如果命令输出是UTF-8
# encoding='utf-8',
errors='replace' # 替换无法解码的字符
)
print(result.stdout)
关键:明确指定encoding参数,根据实际情况选择编码。
5. 超时控制 有些命令可能执行时间过长,需要设置超时。
import subprocess
import signal
# ✅ 简单超时
try:
result = subprocess.run(
['sleep', '10'],
timeout=5, # 5秒后超时
capture_output=True
)
except subprocess.TimeoutExpired:
print("命令执行超时")
# ✅ 更复杂的超时处理(包括清理子进程)
proc = None
try:
proc = subprocess.Popen(['some_long_running_task'])
proc.wait(timeout=30) # 等待30秒
except subprocess.TimeoutExpired:
print("超时,终止进程")
proc.terminate() # 发送SIGTERM
proc.wait(timeout=5) # 给5秒优雅退出
if proc.poll() is None: # 如果还在运行
proc.kill() # 强制杀死
关键:使用timeout参数,对于Popen对象,用terminate()和kill()来清理。
总结建议
根据你的需求选择合适的函数:简单命令用run(),复杂交互用Popen(),记得处理好输出流和错误处理。
不可能啊,主进程退出了,子进程也会强制退出啊。。
主进程退出的时候 Popen 调用 kill,( windows 的话 taskkill )手动杀了子进程
一般都是父进程记录子进程的 pid,然后父进程退出时把子进程都 kill 一遍
subprocess 就是有这个问题,Popen 的时候把 pid 返回来保存,结束的时候直接杀掉
proc = subprocess.Popen(…)
try:
outs, errs = proc.communicate(timeout=15)
except TimeoutExpired:
proc.kill()
outs, errs = proc.communicate()
手动 ps aux | grep xxx | awk ‘{print $2}’ | kill 吧,
我前几天用 multiprocess 的 pool 的时候出了一堆 D 进程,
D 进程只能手动关闭,父进程 加上 兄弟进程同时关,一般就能杀掉了
有个参数的可以开启进程组,好像是 pexec 或者 new_session。,你可以查下文档
用 psutil,把子进程全 kill 了:
https://gist.github.com/xcjiang/bd19224a4b6cc79470118337cc3bdc8b
这些其实都不是 python 的知识
好好把操作系统信号,和进程 fork 相关看一看
这些基础知识不熟悉,multiprocess、subprocess 各种问题你都不知道原因,各种标准的处理方法你都不知道
不熟悉的就不要瞎回答
比如上面有人说这种完全错误的话
~
不可能啊,主进程退出了,子进程也会强制退出啊。。
强调一下!!!这些其实都不是 python 的问题!!
上面的直接 kill 都不是正确做法
如果你子进程没处理信号的话,只能用信号 9,这种做法是非常不标准的
Linux 平台的话,可以用 prctl PR_SET_PDEATHSIG
你可以用 atexit 注册个 exit handler,然后每次 fork 的时候记录下来,调用 handler 的时候向子进程发送 SIGTERM 信号,子进程也可以注册 signal handler 来进行退出的收尾工作。
菜鸟研究的不深入,这块实在不太懂。
保留子进程 PID 的做法确实有效,只有一个致命缺陷,其实类似 -9 那种干掉进程的做法,有些信号什么的根本来挤不发出去,不保险。
我看了那个 prexec 什么的参数,说是 fork 之后,启动之前的一个钩子函数,然后文档里直接 NOTE 说了这个参数不保险,😅
你可以找例子跑跑看,不要在终端 ctrl + C,试试在进程管理器里把主进程停止。
这都是高级玩家了,我还没太深入研究这一块。
我最后用 shell 脚本写了,放弃这种主进程、子进程的做法。
试试 start_new_session 参数,其实本质是 Linux 进程组和会话的概念。
问题下面我补充了,我看到过这种进程组的玩法。我这边命令不多,直接写 shell 脚本了
你担心主进程被强杀的话,那你可以改变策略在子进程里监视主进程啊,主进程退出的话就退出
如果子进程的程序不是你写的程序没法改代码加监视逻辑的话…… emmm ……
解决办法其实有……不过算有点骚操作,你可以往子进程里注入远程代码啊哈哈哈
在注入的代码里做主进程监视就行了,完美
不需要子进程监控主进程,有个很标准的做法是用管道
主进程 fork 前开管道
子进程开一个线程去读管道,后面紧跟强制退出代码 os._exit(1)
因为这个读取是阻塞的,当主进程挂掉的时候阻塞读能返回空,这样就知道主进程挂了
规范程序都按标准写法写的, 不懂把 openstack 的 luanch 那部分读透了就都明白了
的确用 shell 实现也是很简单,cmd &放到后台,然后可以用 wait 来等待,不过我又想起来一个方法,进程 fork 后除非显式调用 setpgid 函数更改进程组 id 否则会继承父进程的进程组 id,这样父进程退出前用 kill 函数,pid 参数指定为 0 就可以发送给同一进程组的所有进程了。
6 楼正解啊
楼主可以 wx 搜下我的号 程序员的梦呓指南 看下最近的 python 进程篇, 讲 os.system subprocess.Popen 家族裸用的原罪, 怎么处理子进程\孙子进程的问题
一篇是这个: Python 踩坑之旅其一杀不死的 Shell 子进程 mp.weixin.qq.com/s?__biz=MzUxMjIzODQ3Mg==&mid=2247483675&idx=1&sn=4338aef5e4268d01d9197cdbb515b301&chksm=f96637fcce11beea8bbbb617359152311e968b0023f844d90164943235db412286572dc5973e&token=863711821&lang=zh_CN#rd
终极篇是这个: Python 踩坑之旅进程篇其三 pgid 是个什么鬼
mp.weixin.qq.com/s?__biz=MzUxMjIzODQ3Mg==&mid=2247483701&idx=1&sn=20aad7b2593e06c6aa4839eb22b446e3&chksm=f96637d2ce11bec44e9968d3912449eb62c8787492ee6a1a22a7f70142f0e2b5e04351b7f909&token=863711821&lang=zh_CN#rd
pgid = os.getpgid(self.proc.pid)
os.killpg(pgid, signal.SIGTERM)

