本文内容均基于 python 2.7 版本,整理一下这个模块的一些平常套路和 一些要注意的坑,省去你们读官方文档的时间。但仍是推荐认真读下 官方文档,并不长html
subprocess 是 python 标准库中的一个模块,用于建立子进程和与子进程交互python
该模块替换了一些过期的模块和函数linux
os.system os.spawn* os.popen* popen2.* commands.*
共计四个函数,包括:shell
call
, check_call
, check_output
)Popen
,是上面三个简化函数的基础,也就是说上面三个简化版函数是封装了这个方式的一些特定用法而已如下为详细说明api
先引入包,为了让下面函数看起来清晰一些,包名临时简化一下为 's'缓存
import subprocess as s
s.call()
建立一个子进程并等待其结束,返回一个 returncode(即 exit code,详见 Linux 基础)安全
# 函数定义 s.call(args, *, stdin=None, stdout=None, stderr=None, shell=False) # 举例 >>> subprocess.call(["ls", "-l"]) 0 >>> subprocess.call("exit 1", shell=True) 1
s.check_call()
跟上面的 s.call() 相似,只不过变成了 returncode 若是不是 0 的话就抛出异常 subprocess.CalledProcessError,这个对象包含 returncode 属性,能够用 try...except... 来处理(参考 Python 异常处理)app
# 函数定义 s.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False) # 举例 >>> subprocess.check_call(["ls", "-l"]) 0 >>> subprocess.check_call("exit 1", shell=True) Traceback (most recent call last): ... subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1
s.check_output()
建立一个子进程并等待其结束,返回子进程向 stdout 的输出结果。若是 returncode 不为 0,则抛出异常 subprocess.CalledProcessError函数
# 函数定义 s.check_output(args, *, stdin=None, stderr=None, shell=False, universal_newlines=False) # 举例 >>> subprocess.check_output(["echo", "Hello World!"]) 'Hello World!\n' >>> subprocess.check_output("exit 1", shell=True) Traceback (most recent call last): ... subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1
s.Popen()
这是整个 subprocess 模块最重要也是最基础的类,以上三个简化方法均是基于这个类。具体每一个参数什么含义请看 官方文档性能
# 类定义 class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
Popen.poll()
检查子进程是否已经结束,返回 returncode
Popen.wait()
阻塞父进程,直到子进程结束,返回 returncode
Popen.communicate(input=None)
阻塞父进程,直到子进程结束。并能够与子进程交互:发送数据(即参数 input,类型必须为 string)到 stdin,以及从 stdout、stderr 读取信息
先来个开胃菜
以上函数(类)的首个参数 args
,有两种使用方式
['ls', '-al']
,通常 推荐 这种方式'ls -al'
,这种方式以上函数中的 shell
参数必须为 True。这种方式存在安全风险,具体请看官方文档 再来一盘
要使父进程和子进程能够通讯,必须将对应的流的管道(PIPE,自行搜索 Linux 管道)打开,如 stdout=subprocess.PIPE
。其中 subprocess.PIPE
是一个特定的值(源码中 hardcode PIPE = -1
),当参数列表中某个流被置为了这个值,则将会在建立子进程的同时为这种流建立一个管道对象(调用系统 api,如 p2cread, _ = _winapi.CreatePipe(None, 0)
),用于父子进程间的通讯
而管道这个东西,其实就是一段内存区间,每一个系统内核对其的大小都有所限制,如 linux 上默认最大为 64KB,能够在终端用 ulimit -a
来查看
可选类型还有:
开始正餐
咱们先来看这样一个需求:
脚本 A 需调用一个外部程序 B 来作一些工做,并获取 B 的 stdout 输出
实现方式一:
import subprocess child = subprocess.Popen(['./B'], stdout=subprocess.PIPE) child.wait() print child.stdout
这种方式官方文档中再三警告,会致使死锁问题,不怕坑本身的话就用吧,good luck
致使死锁的缘由:
如上文提到,管道的大小是有所限制的,当子进程一直向 stdout 管道写数据且写满的时候,子进程将发生 I/O 阻塞;
而此时父进程只是干等子进程退出,也处于阻塞状态;
因而,GG。
实现方式二(加粗:推荐):
import subprocess child = subprocess.Popen(['./B'], stdout=subprocess.PIPE) stdout, stderr = child.communicate()
这是官方推荐的方式,可是也有坑:
看 communicate 的实现
for key, events in ready: if key.fileobj is self.stdin: chunk = input_view[self._input_offset : self._input_offset + _PIPE_BUF] try: self._input_offset += os.write(key.fd, chunk) except BrokenPipeError: selector.unregister(key.fileobj) key.fileobj.close() else: if self._input_offset >= len(self._input): selector.unregister(key.fileobj) key.fileobj.close() elif key.fileobj in (self.stdout, self.stderr): data = os.read(key.fd, 32768) if not data: selector.unregister(key.fileobj) key.fileobj.close() self._fileobj2output[key.fileobj].append(data)
其实 communicate 就是帮咱们作了循环读取管道的操做,保证管道不会被塞满;因而子进程能够很爽地写完本身要输出的数据,正常退出,避免了死锁
这里有注意点:
communicate 实际上是循环读取管道中的数据(每次 32768 字节)并将其存在一个 list 里面,到最后将 list 中的全部数据链接起来(b''.join(list)
) 返回给用户。因而就出现了一个坑:若是子进程输出内容很是多甚至无限输出,则机器内存会被撑爆,再次 GG
第三种实现
第三种的思路其实很简单,就是当子进程的可能输出很是大的时候,直接将 stdout 或 stderr 重定向到文件,避免了撑爆内存的问题。不过这种情形不多见,不经常使用
Tips:
communicate 的实现中能够看到一些有趣的东西,写一下想法,欢迎讨论
每次读取管道中的数据为 32768bytes,即 32KB。选用这个数据的缘由猜测为
从管道中读取数据并缓存到内存中的操做,并不是循环用字符串拼接的方式,而是先将数据分段放进一个 list,最后再链接起来,缘由
ulimit -a
的输出中,能够看到 PIPE BUF 4KB
和 PIPE SIZE 64KB
两个值,查了一下了解到,前者是对管道单次写入大小的限制,然后者是管道总大小的限制。前者恰好对应了内核分页单页的大小。