在 Linux 系统中,我们经常会编写 shell 脚本来执行任务,或者调用系统的一些命令,这对于 python 来讲,属于执行外部命令,python 标准库里面提供了一些接口,这里做一个介绍。
1 subprocess
subprocess 是 Python 标准库中用于 创建和管理子进程 的核心模块。它允许你从 Python 程序中启动外部命令(如 shell 脚本、可执行程序、系统工具等),并与它们进行交互(传递参数、读取输出、捕获错误、控制超时等)。
应用场景:
- 执行 shell 脚本或系统命令(
bash,ffmpeg,rsync等) - 调用其他语言编写的程序(如 R、MATLAB 脚本)
- 自动化运维(备份、部署、监控)
- 与 Docker、Git、AWS CLI 等工具集成
- 并行任务调度(配合
concurrent.futures)
subprocess 主要提供了两个接口:run()和Popen()
| 特性 | subprocess.run() |
subprocess.Popen() |
|---|---|---|
| 引入版本 | Python 3.5(推荐新代码使用) | Python 2.4(旧但功能强大) |
| 阻塞行为 | 同步阻塞:调用后立即等待子进程结束 | 异步非阻塞:启动后立即返回,不等待 |
| 使用复杂度 | 简单、高层、一行搞定 | 复杂、底层、需手动管理 |
| 适用场景 | 一次性执行命令,获取结果 | 需要精细控制、实时交互、长时间运行 |
| 返回值 | CompletedProcess 对象 |
Popen 对象(可后续操作) |
1.1 核心函数 run
1.1.1 用法
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None,
capture_output=False, timeout=None, check=False, text=None,
encoding=None, errors=None, env=None, cwd=None, ...)
同步阻塞:调用后立即等待子进程结束
常用参数说明:
| 参数 | 作用 |
|---|---|
args |
要执行的命令,推荐使用列表形式:["ls", "-l"](更安全);也可用字符串 "ls -l"(但需 shell=True) |
capture_output |
若为 True,自动设置 stdout=PIPE, stderr=PIPE,便于获取输出 |
text=True(或 universal_newlines=True) |
返回字符串而非 bytes |
timeout |
超时时间(秒),超时则抛出 TimeoutExpired |
check=True |
如果子进程返回非零退出码,自动抛出 CalledProcessError |
cwd |
指定子进程的工作目录 |
env |
设置环境变量(字典) |
1.1.2 举例说明
import subprocess
result1 = subprocess.run(["ls", "-l"],
capture_output=True,
text=True
)
print(" 返回码:", result1.returncode)
print(" 标准输出:\n", result1.stdout)
print(" 标准错误:\n", result1.stderr)
result2 = subprocess.run(["sort"],
input="banana\napple\ncherry",
text=True,
capture_output=True
)
print(result2.stdout)
超时与异常处理:
try:
result = subprocess.run(["sleep", "10"],
timeout=2,
check=True
)
except subprocess.TimeoutExpired:
print(" 命令执行超时!")
except subprocess.CalledProcessError as e:
print(f" 命令失败,返回码: {e.returncode}")
1.1.3 返回值对象
subprocess.run() 返回一个 CompletedProcess 对象,包含:
.args:原始命令.returncode:退出状态码(0 通常表示成功).stdout:标准输出(bytes 或 str).stderr:标准错误.check_returncode():手动检查是否失败(类似check=True)
1.2 高级函数 Popen
subprocess.Popen 是 Python subprocess 模块中最底层、最灵活的子进程管理接口。
它允许你 启动一个子进程后立即返回,非阻塞式,并在后续对其执行精细控制(如读写标准流、检查状态、发送信号、等待结束等)。
适用于需要 实时交互、长时间运行或并发管理多个进程 的场景。
1.2.1 用法
subprocess.Popen(args, bufsize=-1, stdin=None, stdout=None, stderr=None, shell=False,
cwd=None, env=None, text=None, encoding=None, errors=None, ...
)
常用参数说明:
| 参数 | 说明 |
|---|---|
args |
命令参数,推荐列表形式:["cmd", "arg1", "arg2"];若用字符串需设 shell=True |
stdin, stdout, stderr |
控制标准流: - None:继承父进程(默认) - subprocess.PIPE:创建管道,可读写 - subprocess.DEVNULL:丢弃 - 文件对象:重定向到文件 |
shell |
是否通过 shell 执行(如 bash)。慎用,有安全风险 |
cwd |
子进程工作目录 |
env |
环境变量字典(若为 None 则继承父进程) |
text(或 universal_newlines) |
若为 True,以字符串而非 bytes 读写流 |
bufsize |
缓冲策略: - 0:无缓冲(仅二进制模式) - 1:行缓冲(文本模式) - -1:系统默认(推荐) |
1.2.2 举例说明
import subprocess
import sys
proc = subprocess.Popen(["ping", "-c", "5", "google.com"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1 # 行缓冲
)
# 实时打印每一行
for line in proc.stdout:
print(line.strip())
sys.stdout.flush() # 确保立即输出
proc.wait() # 务必调用
print("Ping 结束,返回码:", proc.returncode)
不要丢弃 Popen 对象而不等待:会导致僵尸进程(zombie process)。
始终确保调用
.wait()、.communicate()或检查.poll(),以回收子进程资源。
1.2.3 返回值对象
subprocess.Popen() 返回一个 Popen 对象(类型为 subprocess.Popen),它是对底层操作系统子进程的 封装句柄,允许你与该子进程进行交互、查询状态、读写数据、发送信号等。
1. 主要属性:
| 属性 | 类型 | 说明 |
|---|---|---|
.args |
list 或 str |
启动时传入的命令参数 |
.pid |
int |
子进程的操作系统进程 ID(PID) |
.returncode |
int 或 None |
- None:进程仍在运行 - 整数(如 0, 1):进程已结束,表示退出码 |
2. 主要方法
| 方法 | 说明 |
|---|---|
.poll() |
检查子进程是否结束,不阻塞。 - 返回 None:仍在运行 - 返回整数:退出码 |
.wait(timeout=None) |
阻塞等待 子进程结束,返回退出码。 超时抛 TimeoutExpired |
.communicate(input=None, timeout=None) |
一次性 发送输入 + 读取输出 + 等待结束。 避免死锁(比直接读 .stdout.read() 安全) |
.send_signal(signal) |
发送信号(如 signal.SIGTERM) |
.terminate() |
发送 SIGTERM(优雅终止) |
.kill() |
发送 SIGKILL(强制杀死) |
1.2.4 返回 pid
| 调用方式 | .pid对应的进程 |
|---|---|
Popen(["./script.sh"]) |
./script.sh(前提是它有 #!/bin/bash 且可执行) |
Popen(["/bin/bash", "script.sh"]) |
/bin/bash 进程 |
Popen("script.sh", shell=True) |
系统默认 shell(如 /bin/sh)进程 |
无论哪种方式,
.pid始终是Popen直接创建的那个“父进程”的 PID,而不是脚本内部启动的某个子命令的 PID。
1.3 应用示例
假设有一个任务如下:用 python 提交一个 shell 脚本,这个 shell 脚本执行一个外部命令 cad -f test.rule。这个外部命令是一个耗时的命令。应该怎么做?
import subprocess
import concurrent.futures
import time
import signal
import os
WORK_DIR = "/path/to/your/workdir" # cad 所需的工作目录
def run_cad_task(task_id):
try:
# 启动子进程
proc = subprocess.Popen(["./run_cad.sh"],
cwd=WORK_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
preexec_fn=os.setsid # 创建新进程组(便于后续杀死整个进程树)
)
# 等待完成或超时
try:
stdout, stderr = proc.communicate(timeout=3600*24)
returncode = proc.returncode
except subprocess.TimeoutExpired:
# 超时:杀死整个进程组
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM) # 发送终止信号 signal.SIGTERM
proc.wait(timeout=10)
except:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL) # 发送强杀信号 signal.SIGKILL
proc.wait()
return (task_id, False, f" 任务超时 ")
# 检查结果
if returncode == 0:
return (task_id, True, " 成功完成 ")
else:
return (task_id, False, f" 失败,退出码: {returncode}\nstderr: {stderr[-500:]}") # 截断长错误
except Exception as e:
return (task_id, False, f" 异常: {str(e)}")
def main():
task_ids = [f"task_{i}" for i in range(10)] # 示例:10 个任务
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# 提交所有任务
future_to_task = {
executor.submit(run_cad_task, tid): tid
for tid in task_ids
}
# 按完成顺序处理结果
for future in concurrent.futures.as_completed(future_to_task):
task_id, success, msg = future.result()
if success:
print(f"[{task_id}] {msg}")
else:
print(f"[{task_id}] {msg}")
if __name__ == "__main__":
main()
为什么用 ThreadPoolExecutor?
subprocess.Popen启动的是 独立 OS 进程,Python 线程只是在“等待”。- 线程池开销小,适合管理大量外部命令。
ProcessPoolExecutor会 fork Python 进程,造成不必要的内存复制。
2 os.system
os.system() 是 Python 标准库 os 模块中的一个 简单 的函数,用于在子 shell 中执行操作系统命令。
自 Python 2.4(2004 年)引入 subprocess 后,os.system() 就已 被标记为遗留接口。
import os
os.system("command") # 将字符串 command 传递给操作系统的默认 shell 执行
命令的 输出会直接打印到终端,无法在 Python 程序中捕获或处理。
命令的执行过程
- 创建一个子进程;
- 子进程启动系统 shell;
- shell 解析并执行你传入的命令字符串;
- 父进程(Python)阻塞等待子进程结束;
- 返回子进程的退出状态。
欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net