绝大多数场景用 subprocess.run(),它自动等待、捕获输出、处理超时;仅需持续交互(如边写入边读取)时才用 subprocess.popen();shell=true 有安全风险,应避免拼接用户输入;捕获输出需正确设置 stdout/stderr 参数;timeout 后需确保子进程真正终止。

subprocess.run() 和 subprocess.Popen() 到底该用哪个
绝大多数场景直接用 subprocess.run() 就够了,它封装了常见需求,自动等待、捕获输出、处理超时;只有当你需要和子进程持续交互(比如边写入边读取、非阻塞检查状态、或复用同一个进程多次通信)时,才必须上 subprocess.Popen()。
容易踩的坑:有人一看到“更底层”就觉得 Popen 更“高级”,结果写了十几行代码手动管理 stdin/stdout 的 communicate()、poll()、wait(),其实 run() 加个 timeout 和 capture_output=True 就能覆盖 90% 的脚本调用需求。
-
run()默认阻塞,适合“执行完就走”的命令,比如git status、curl -s http://api -
Popen()返回对象可反复调用.stdin.write()、.stdout.readline(),适合跟gdb、python -i或自定义 CLI 工具交互 - 如果用
Popen却忘了调用.wait()或.communicate(),子进程可能变成僵尸进程(尤其在循环中反复 spawn 时)
shell=True 是便利还是隐患
加 shell=True 能直接写 "ls *.py | head -n 3" 这种带管道、通配符的命令,但会启动一个 shell 解释器(通常是 /bin/sh),带来安全与兼容性双重风险。
真实问题:你在 Web 后端拼接用户输入进 subprocess.run(f"grep {user_input} file.txt", shell=True) —— 这等于把 os.system() 又请回来了,user_input = "test; rm -rf /" 就直接执行。
立即学习“Python免费学习笔记(深入)”;
- 只要命令是固定字符串且不含 shell 特性(如
|、&、$VAR),一律用shell=False(默认值),参数传 list:["ls", "-l", "/tmp"] - 真要管道,拆成多个
run()+PIPE中转,或改用shlex.split()安全解析后再传 list - Windows 下
shell=True启的是cmd.exe,不认单引号、$(())等 bash 语法,跨平台脚本尽量避免
捕获输出时 stdout/stderr 的常见误判
很多人以为 stdout=PIPE 就能拿到所有输出,结果发现子进程卡住或返回空——根本原因是没处理好缓冲和重定向组合。
典型现象:subprocess.run(["python", "-c", "print('a'); import time; time.sleep(2); print('b')"], stdout=subprocess.PIPE) 会等满 2 秒才返回,但如果你加了 stderr=STDOUT 却漏掉 stdout=PIPE,那 stderr 会被丢弃而不是合并。
- 想同时捕获 stdout 和 stderr 并区分:用
stdout=PIPE, stderr=PIPE,然后看result.stdout和result.stderr - 想合并输出(比如日志统一处理):用
stdout=PIPE, stderr=subprocess.STDOUT,此时result.stderr是None,所有输出都在result.stdout - 如果子进程输出量大(比如
tar -cf - /bigdir),又只设stdout=PIPE不读取,内核 pipe buffer 满了就会阻塞子进程 —— 这时必须用run(..., capture_output=True)或手动调.communicate()
timeout 不生效?大概率是信号没传过去
subprocess.run(cmd, timeout=5) 超时后抛出 TimeoutExpired,但你会发现子进程还在后台跑着 —— 因为 Python 默认只给子进程发 SIGTERM(Linux/macOS)或 CTRL_BREAK_EVENT(Windows),而很多程序忽略它。
更麻烦的是:如果子进程 fork 出了孙子进程(比如 bash -c "sleep 10 &"),kill 父进程根本杀不掉孙子,导致资源泄露。
- 确保超时后真正清理干净:用
subprocess.run(..., timeout=5, kill_after_timeout=True)(Python 3.12+),或自己捕获TimeoutExpired后调proc.kill() - 对 shell 启动的复杂命令,考虑用
start_new_session=True,这样超时 kill 时整个进程组(包括子子孙孙)都会被干掉 - Windows 上
timeout对某些 GUI 程序(如notepad.exe)无效,因为它们不响应控制台信号,得换用任务管理器级的强制终止逻辑
子进程最难搞的从来不是怎么启动,而是怎么确认它真死了、输出真拿到了、错误真报出来了——每个 run() 调用背后,都得想清楚这三件事有没有被覆盖到。










