本文內容均基於 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
兩個值,查了一下了解到,前者是對管道單次寫入大小的限制,然後者是管道總大小的限制。前者恰好對應了內核分頁單頁的大小。