该文章基于 python3.7,部分功能只有python3.7能实现
分布式 进程 线程 系列文章 //www.greatytc.com/nb/39184979
在前面两章,我们介绍了multiprocessing
模块的使用,它是一个跨平台的多进程模块,今天介绍的是另外一个创建多进程的模块subprocess
.
这两个模块都可以在当前主进程中创建子进程,但不同的是multiprocessing
中的进程主要是运行python
代码,而subprocess
中的进程是运行已经编写好的程序
,或者说是shell
命令。
举个栗子,我们在程序运行中想要知道当前主机的IP
,那么使用subprocess
模块运行ifconfig
(Unix平台) 或 ipconfig
(Windows平台),然后再获取子进程的输出信息,从输出信息中就可以解析出当前IP
.
再或者,我用Java
写了一套程序,现在在python
中需要这部分功能,那么可以在python子进程中执行两个命令:
javac HelloWorld.java
java HelloWorld
并且获取Java
程序的输出信息。
备注:前提是已经安装了Java
,并且正确配置环境变量。
一、通过便捷函数run()
执行 shell
首先我们需要了解文本流,不然后面的可能会看着很吃力。
linux 文本流 //www.greatytc.com/p/62abe469ecc6
计算机中有“万物皆文本流”
的说法,任何标准的程序/进程
都有标准输入流
、标准输出流
、错误输出流
,在subprocess
模块中尤为重要,只有获取了输入流、输出流,我们才能从流
中输入指令,获取打印信息。(如果你执行了ls
命令,确不能获取输出信息,那有什么意义呢)。
【在multiprocessing
模块中,进程主要是执行代码,所以输入、输出就不怎么重要,也没有设置的参数,默认重定向到父进程的输入、输出。】
1) 便捷函数 run()
subprocess.run(args, *, stdin=None, input=None, stdout=None,
stderr=None, capture_output=False, shell=False, cwd=None,
timeout=None, check=False, encoding=None, errors=None, text=None,
env=None, universal_newlines=None)
别怕,这个run()
函数很长、很长,但并不是所有都需要的,我们必要设置的只有第一项args
,也就是shell
命令
例如运行ifconfig
,如果你是Windows系统,可以使用ipconfig
代替。
import subprocess
subprocess.run(args=['ifconfig',])
执行结果如下:
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
nd6 options=201<PERFORMNUD,DAD>
……
args
args
参数传入一个列表或者元组,如['ls','-l']
,python会自动拼接成shell
命令.[第一个参数是执行的程序,其余的是参数]
也可以直接就是一个str
命令行,如果如果传入的是shell
命令,则需要另外添加一个参数shell=True
。input
有的时候运行的程序需要输入参数,例如我们写个一个python程序,文件目录为当前目录下的test.py
# test.py
print('请输入一个参数数字:')
p = input()
print(p * 2)
在子进程中使用shell命令执行
import subprocess
subprocess.run(args='python ./test.py', input=b'4', shell=True)
执行结果
请输入一个参数数字:
44
这里的input
就是程序的输入,被传递给 Popen.communicate()
以及子进程的标准输入. 如果使用此参数, 它必须是一个字节序列.如果指定了 encoding
或 errors
或者将text
设置为 True, 那么也可以是一个字符串.
-
check
进程正常运行完毕是以状态码为0
退出。
如果 check 设为 True, 则会检查退出状态码,进程以非零状态码退出时, 一个CalledProcessError
异常将被抛出.
例如:
subprocess.run("exit 1", shell=True, check=True)
则在主进程中会抛出异常
subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1
timeout
参数将被传递给Popen.communicate()
。如果发生超时,子进程将被杀死并等待。TimeoutExpired
异常将在子进程中断后被抛出。encoding
被指定, 或者text
被设为 True, 标准输入, 标准输出和标准错误的文件对象将通过指定的 encoding 以文本模式打开, 默认是通过二进制模式打开
import subprocess
subprocess.run(args='python ./test.py', input='4', shell=True, encoding='utf-8')
和上面传入input=b'4'
效果相同。
-
stdin
,stdout
和stderr
分别指定了执行的程序的标准输入、输出和标准错误文件句柄。合法的值有PIPE
、DEVNULL
、 一个现存的文件描述符(一个正整数)、一个现存的文件对象以及None
。默认是None
,标准输出、标准错误继承自父进程,也就是当前的终端。
推荐使用PIPE
,作为标准输出、错误输出流,这是一个信号,(它就是-1
),表明由程序自动创建管道,在run()运行完毕后,可以从从返回结果中获取信息,省去我们自己创建、关闭流的过程。
stdin
与input
只能选择一个,区别是input
中能输入一次,而stdin
是一个输入流,可以传入多个输入数据。
这里采用一个文件流in.txt
作为输入流【stdin
只能自己创建输入流,不能使用PIPE
信号】。
# in.txt
4
运行如下程序
import subprocess
from subprocess import PIPE
f = open('./in.txt', 'rb')
r = subprocess.run(args='python ./test.py', shell=True, encoding='utf-8', stdout=PIPE, stdin=f)
print(r.stdout)
f.close()
r.stdout
保存了子进程的输出信息,也就是请输入一个参数数字: 44
r
是subprocess.run()
的运行结果.
2) run()
的返回对象 class subprocess.CompletedProcess
CompletedProcess
是run()
的返回值, 代表一个进程已经结束.具有以下方法
-
args
返回被用作启动进程的参数. 可能是一个列表或字符串. -
returncode
子进程的退出状态码. 通常来说, 一个为 0 的退出码表示进程运行正常.
一个负值 -N 表示子进程被信号 N 中断 (仅 POSIX). -
stdout
从子进程捕获到的标准输出. 一个字节序列, 或一个字符串, 如果run()
是设置了 encoding, errors 或者text=True
来运行的. 如果未有捕获, 则为None
. -
stderr
捕获到的子进程的标准错误. 一个字节序列, 或者一个字符串, 如果run()
是设置了参数 encoding, errors 或者text=True
运行的. 如果未有捕获, 则为None
. -
check_returncode()
如果returncode
非零, 抛出CalledProcessError
.
二、子进程对象 Popen
一般来说,我们执行一条shell
命令,使用subprocess.run()
这个便捷函数就可以了,它足以满足我们的大部分需求,如果你需要对子进程有更细致的控制,那么可以通过构造Popen
对象来运行shell
,run()
函数也是通过构造Popen
来运行shell
命令的。
1)Popen
构造函数
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None,
stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None,
universal_newlines=None, startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False, pass_fds=(), *, encoding=None,
errors=None, text=None)
此函数中大部分功能与 上面的run()
是相同的,其中stdin
与run()
不同,这里的stdin
可以使用PIPE
信号,这样程序会自己创建一个输入流,并返回这个输入流。
想一想为什么这里可以,而run()
不可以?
仔细的想,run()
函数是不够透明的,它只呈现开始和结束状态,也就是说run()
返回的是程序已经结束的状态,如果由程序自己创建一个输入流,并在程序结束后返回给你,那完全没有意义(程序都结束了还输入啥子)
所以在run()
中要么你使用input
直接输入一条输入
,要么通过stdin
指定一个已经存在的(并且有内容的)输入流。
通过子进程对象 Popen
就不一样了,我们可以从始至终控制该子进程,进行输入输出,那这个时候让程序帮我们创建输入流(并管理流的关闭),我们就可以通过输入流和子进程交互了。
2)子进程Popen
常用方法
-
args
args 参数 -
kill()
杀死子进程。在 Posix 操作系统上,此函数给子进程发送 SIGKILL 信号。在 Windows 上,kill()
是terminate()
的别名
terminate()
停止子进程。在 Posix 操作系统上,此方法发送 SIGTERM。在 Windows,调用 Win32 API 函数 TerminateProcess() 来停止子进程。poll()
检查子进程是否已被终止。设置并返回returncode
属性。否则返回None
。wait(timeout=None)
等待子进程被终止。设置并返回returncode
属性。
如果进程在 timeout 秒后未中断,抛出一个 TimeoutExpired
异常,可以安全地捕获此异常并重新等待。
-
communicate(input=None, timeout=None)
与进程交互:向 stdin 传输数据。从 stdout 和 stderr 读取数据,直到文件结束符。等待进程终止。可选的 input 参数应当未被传输给子进程的数据,如果没有数据应被传输给子进程则为 None。如果流以文本模式打开, input 必须为字符串。否则,它必须为字节。
communicate()
返回一个(stdout_data, stderr_data)
元组。如果文件以文本模式打开则为字符串;否则字节。
注意如果你想要向进程的 stdin 传输数据,你需要通过 stdin=PIPE 创建此 Popen 对象。类似的,要从结果元组获取任何非 None 值,你同样需要设置 stdout=PIPE 或者 stderr=PIPE。
如果进程在 timeout 秒后未终止,一个TimeoutExpired
异常将被抛出。捕获此异常并重新等待将不会丢失任何输出。
如果超时到期,子进程不会被杀死,所以为了正确清理一个行为良好的应用程序应该杀死子进程并完成通讯。
from subprocess import Popen, TimeoutExpired, PIPE
# 这里必须使用信号 PIPE 而不是自己创建的文本流,这样才能使用 communicate 函数
proc = Popen(args='python ./test.py', shell=True, encoding='utf-8', stdin=PIPE, stdout=PIPE, stderr=PIPE)
try:
outs, errs = proc.communicate(input='4',timeout=15)
print(outs)
except TimeoutExpired:
proc.kill()
打印结果
请输入一个参数数字:
44
注意communicate()
只能使用一次,也就是只能输入一个数据,上面提到的便捷函数run()
的input
参数就是在这里调用了communicate()
如果我们想入输入多个input
,那就不能使用communicate()
,而是自己控制输入流。
一种方法是在构造函数中将stdin
设置为已经输入好的文本流/二进制流。
但有的时候我们需要输入的内容是不确定的,还要根据程序运行的情况动态输入多个数据,那么可以将stdin
设置为PIPE
,从进程中获取输入流句柄,自己往输入流中写数据。
(需要自己控制细节,如读取一行的结束符,何时将流中的数据从缓存中刷新到子进程中,关闭输入输出管道,而使用communicate()
则不接触管道/流,就不需要关注这些)
如下:
#test.py
# 需要输入两个数据
print('请输入一个参数数字:')
p = input()
print(p * 2)
p2 = input()
print(p2 * 3)
执行子进程
from subprocess import Popen, TimeoutExpired, PIPE
with Popen(args='python ./test.py', shell=True, encoding='utf-8', stdin=PIPE, stdout=PIPE, stderr=PIPE) as proc:
try:
stdin = proc.stdin
# 以\n结束,程序才能判断这是一次完整的读取 效果和`'1\n2\n'`相同
stdin.write('1\n')
stdin.write('2\n')
# 将输入从缓存刷新到子进程中
stdin.flush()
s = proc.stdout.readlines()
print(s)
except TimeoutExpired:
proc.kill()
执行结果如下:
['请输入一个参数数字:\n', '11\n', '222\n']
这里使用了上下文管理器//www.greatytc.com/p/a97e6aeca3fb
来管理管道的关闭,所以没有手动的去执行close()函数。
[这里Popen实现了上下文管理器协议,并且只有使用了PIPE
信号创建的管道才可以这样做]
pid
子进程的进程号
注意如果你设置了 shell 参数为 True,则这是生成的子 shell 的进程号。returncode
此进程的退出码,由poll()
和wait()
设置(以及直接由communicate()
设置)。一个None
值 表示此进程仍未结束。
[意思是说通过poll()
和wait()
判断进程状态时,我们可以杀死进程,这样进程的状态码就非0
]
一个负值-N
表示子进程被信号N
中断 (仅 POSIX).