PEP 324 subprocess 新的進程模組 — Python官方文檔譯文 [原創]

PEP 324 — subprocess 新的進程模組(subprocess – New process module)

英文原文://www.python.org/dev/peps/pep-0324/
採集日期:2021-05-13

PEP: 324
Title: subprocess – New process module
Version: $Revision$
Author: Peter Astrand [email protected]
Status: Final
Type: Standards Track
Created: 19-Nov-2003
Python-Version: 2.4
Post-History:

目錄

摘要(Abstract)


本文描述了一個新模組,用於啟動進程並與之通訊。

動機(Motivation)


不管用什麼程式語言,啟動新的進程都是一項常見的任務,特別是 Python 這種高級語言更是十分常見。為此提供支援是很有必要的,原因如下:

  • 用不合適的函數啟動進程,可能會存在安全風險:如果程式是通過 shell 啟動的,並且參數中包含了shell 元(meta)字元,結果可能會比較慘。[^注1]

  • 這讓 Python 成為了更好的替代語言,替換過於複雜的shell腳本。

當前 Python 有很多不同的函數用於創建進程,讓開發人員難以選擇。

subprocess模組比以前函數的改進之處:

  • 用一個「統一」的模組,提供了以前函數的所有功能。
  • 支援跨進程異常:在開始執行新的進程之前,發生在子進程中的異常會在父進程中再次觸發。這就意味著對exec()執行失敗就很容易處理。而比如用popen2就無法檢測執行是否失敗。
  • 在 fork 和 exec 之間提供了鉤子(hook),以便執行自定義程式碼。可被用於改變 uid 之類的操作。
  • 不會隱式調用 /bin/sh。這就意味著不必對危險的 shell 元字元進行轉義了。
  • 允許對文件描述符進行所有的重定向組合。比如,「python-dialog」[^注2]需要生成一個進程,並對 stderr 做重定向,但不對 stdout 做重定向。如果不使用臨時文件,用目前的函數是不可能做到的。
  • 利用 subprocess 模組,能夠在啟動新程式前控制是否關閉所有打開的文件描述符。
  • 支援將多個子進程連接起來(shell 的管道「pipe」)
  • 為換行符提供統一的支援。
  • 提供了communicate()方法,使得發送 stdin 數據及讀取 stdout、stderr 數據變得容易,且沒有死鎖的風險。大多數人都知道要注意子進程通訊時的流控問題,但不是所有人都有耐心和技巧編寫出完全正確且無死鎖的循環選擇過程(select loop)。 這意味著會有很多 Python 應用程式帶有競態條件。標準庫中有個communicate()方法可解決這個難題。

原由(Rationale)


設計思路匯總如下:

  • subprocess 基於 popen2 實現,因其已久經考驗。

  • popen2 的工廠方法已被去除,因為類構造函數用起來同樣簡單。

  • popen2包含很多工廠方法和類,用於各種重定向組合。而 subprocess 中只有1個類。因為 subprocess 模組支援12種不同的重定向組合,再為每種組合提供1個類或函數就有點累贅,也不太直觀。即便是針對 popen2,清晰度也存在問題。比如離開了文檔,很多人說不出 popen2.popen2 和popen2.popen4 的區別。

  • 提供一個工具小函數:subprocess.call()。目標是os.system()的增強版,易用性仍舊很好。

    • 不採用標準的C函數system(),因其有缺陷。
    • 不會隱式調用 shell。
    • 無需用引號,而是採用參數列表。
    • 返回值更易於處理。

    正如Popen類構造函數那樣,工具函數call()可接受1個 ‘args’ 參數。等待命令完成,並返回returncode。實現程式碼非常簡單:

      def call(*args, **kwargs):
          return Popen(*args, **kwargs).wait()
    

    call() 函數的設計初衷很簡單:啟動進程並等待其·完成,這是一種很常見的任務。

    Popen 則支援很多可選參數,很多用戶需要簡潔的形式。目前還有很多人在用 os.system(),主要原因就是它的介面比較簡潔。比如:

      os.system("stty sane -F " + device)
    

    採用subprocess.call()可能就會是以下方式:

      subprocess.call(["stty", "sane", "-F", device])
    

    或者,如果要通過 shell 執行,則如下:

      subprocess.call("stty sane -F " + device, shell=True)
    
  • 提供「預執行(preexec)」能力,以便在 fork 和 exec 之間執行任何程式碼。也許有人會問,為什麼特地有參數用於設置環境變數和目錄,卻沒有設置 uid 之類的參數,答案就是:

    • 修改環境變數和工作目錄相當常用。
    • 類似spawn()之類的傳統函數已經支援了「env」參數。
    • env和cwd很大程度上被視為跨平台的,在Windows平台中也能生效。
  • 在POSIX平台中,不需要用到擴展模組:只用到了os.fork()os.execvp()這類函數。

  • 在 Windows 平台,需要用到 Mark Hammond 的 Windows 擴展模組[^注5]或 _subprocess 擴展模組。

特性(Specification)


本模組定義了一個名為Popen的類:

```
class 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):
```

參數如下:

  • args 應為一個表示程式參數的字元串或序列。要執行的程式通常是args序列或字元串的第一項,但也可以通過executable參數進行顯式設置。

    在UNIX平台用shell=False(默認)時:Popen類採用os.execvp()執行子程式。args通常應為一個序列。字元串將被視作序列類型,該字元串是序列的唯一數據項(即要執行的程式名)。

    在UNIX平台用shell=True時:如果args是個字元串,那就是指定了要通過 shell 執行的命令行。如果args是個序列,則第1個數據項就是命令,其他數據項則都被視為shell參數。

    在Windows平台:Popen類採用CreateProcess()執行子程式,其只認字元串。如果args是個序列,則會用list2cmdline方法轉換為字元串。請注意,並不是所有Windows應用都用同樣的方式解析命令行:list2cmdline的設計初衷,是為採用MS C運行庫規則的應用程式服務的。

  • 如果給出了bufsize,則意義與內置open()函數的參數相同:0表示無緩衝,1表示行緩衝,其他正值表示採用該大小(近似)的緩衝區,bufsize為負值表示採用系統默認值,通常意味著全緩衝。bufsize默認值為0(無緩衝)。

  • stdinstdoutstderr分別指定了被執行程式的標準輸入、標準輸出和標準錯誤文件句柄。合法值可以是PIPE、已存在的文件描述符(正整數)、已存在的文件對象或者NonePIPE表示應該為子進程新建一個管道。 None表示不會發生重定向,子進程的文件句柄將繼承自父進程。stderr也可以是 STDOUT,表示應用程式的 stderr 數據將捕獲並放入與 stdout 相同的文件句柄中。

  • 如果preexec_fn設為可調用對象,則在子進程執行之前會調用該對象。

  • 如果close_fds為 True,則在執行子進程之前,除 0、1、2 之外的所有文件描述符都會關閉。

  • 如果shell為 True,命令將會通過shell執行。

  • 如果cwd不為None,則在子進程執行之前,當前目錄將會改為cwd。

  • 如果env不為None,則將其定義為新進程的環境變數。

  • 如果universal_newlines為 True,文件對象stdout和stderr將打開為文本文件,但每行將由以下任一符號結束: Unix 行結束符\n、Macintosh 行結束符\r或 Windows 行結束符\r\n。這些符號都將被 Python 視為\n。請注意,僅當Python編譯時帶上通用換行支援(默認)時,該特性才會生效。此外,文件對象 stdout、stdin 和 stderr 的換行符屬性不會被communication()方法更新。

  • 如果給出了startupinfocreationflags,則會傳給底層的CreateProcess()函數。可用於指定主窗口的外觀和新進程的優先順序等。(僅限 Windows)

本模組還定義了兩個便捷函數:

  • call(*args, **kwargs)帶上實參運行某命令。等待命令運行結束,然後返回returncode屬性。

    參數與Popen的構造函數相同。例如:

      retcode = call(["ls", "-l"])
    

異常(Exceptions)


在開始執行新程式之前,子進程中觸發的異常將會在父進程中重新觸發。此外,異常對象將帶有一個名為「child_traceback」的額外屬性,這是一個字元串,包含了從子進程角度看到的回調資訊。

最常見的異常就是OSErrors。例如,在嘗試執行的文件不存在時就會發生這種情況。應用程式應對OSErrors有所準備。

如果 popen 的參數非法,則會觸發ValueError

安全性(Security)

與其他一些 popen 函數不同,此處程式碼永遠不會隱式調用 /bin/sh。這意味著所有字元,包括 shell 元字元,都可以安全地傳給子進程。

Popen 對象(Popen objects)


Popen 類的實例擁有以下方法:

poll() 檢測子進程是否運行結束。返回returncode屬性。

wait() 等待子進程運行結束。返回returncode屬性。

communicate(input=None) 與進程交互:將數據發送到 stdin。從 stdout 和 stderr 讀取數據,直至文件末尾。等待進程終止。可選的 stdin 參數應為要發送給子進程的字元串,若沒有數據要發給子進程,則應為 None

communicate() 返回元組 (stdout, stderr)

注意:由於讀到的數據是快取在記憶體中的,所以如果數據量很大或者沒有限制就不要使用這種方式。

還有以下屬性可用:

stdin 如果stdinPIPE,則本屬性將是一個文件對象,用於向子進程提供輸入。否則為None

stdout 如果stdoutPIPE,則本屬性將是一個文件對象,用於為子進程提供輸出。否則為None

stderr 如果stderrPIPE,則本屬性將是一個文件對象,用於為子進程提供錯誤輸出。否則為None

pid 子進程的進程 ID。

returncode 子進程的返回碼。None 值表示進程尚未結束。負值 -N 表示子進程被訊號 N 終止(僅限 UNIX)。

用 subprocess 模組替換舊函數(Replacing older functions with the subprocess module)


本節中的「a ==> b」表示可以將 b 換用 a。

注意:如果找不到被執行的程式,本節中的所有函數(或多或少)都會靜默地失敗;本模組將觸發 OSError 異常。

以下示例假設 subprocess 模組是用 from subprocess import * 語句導入的。

替換反引號包裹的 /bin/sh shell 命令


    output=`mycmd myarg`
    ==>
    output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0]

替換 shell 管道


    output=`dmesg | grep hda`
    ==>
    p1 = Popen(["dmesg"], stdout=PIPE)
    p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
    output = p2.communicate()[0]

替換 os.system()


    sts = os.system("mycmd" + " myarg")
    ==>
    p = Popen("mycmd" + " myarg", shell=True)
    sts = os.waitpid(p.pid, 0)

注意:

  • 通常沒有必要通過 shell 調用程式。
  • 查看 returncode 屬性要比查看退出狀態更為容易。

現實中的程式碼實例可能會如下所示:

    try:
        retcode = call("mycmd" + " myarg", shell=True)
        if retcode < 0:
            print >>sys.stderr, "Child was terminated by signal", -retcode
        else:
            print >>sys.stderr, "Child returned", retcode
    except OSError, e:
        print >>sys.stderr, "Execution failed:", e

替換os.spawn*


P_NOWAIT 示例:

    pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
    ==>
    pid = Popen(["/bin/mycmd", "myarg"]).pid

P_WAIT 示例:

    retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
    ==>
    retcode = call(["/bin/mycmd", "myarg"])

Vector 示例:

    os.spawnvp(os.P_NOWAIT, path, args)
    ==>
    Popen([path] + args[1:])

環境變數示例:

    os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
    ==>
    Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})

替換 os.popen*


    pipe = os.popen(cmd, mode='r', bufsize)
    ==>
    pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout

    pipe = os.popen(cmd, mode='w', bufsize)
    ==>
    pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin


    (child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize)
    ==>
    p = Popen(cmd, shell=True, bufsize=bufsize,
              stdin=PIPE, stdout=PIPE, close_fds=True)
    (child_stdin, child_stdout) = (p.stdin, p.stdout)


    (child_stdin,
     child_stdout,
     child_stderr) = os.popen3(cmd, mode, bufsize)
    ==>
    p = Popen(cmd, shell=True, bufsize=bufsize,
              stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
    (child_stdin,
     child_stdout,
     child_stderr) = (p.stdin, p.stdout, p.stderr)


    (child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize)
    ==>
    p = Popen(cmd, shell=True, bufsize=bufsize,
              stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
    (child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)

替換popen2.*


注意:如果 popen2 函數的 cmd 參數是個字元串,則命令將通過 /bin/sh 執行。如果是個列表,則直接執行命令。

    (child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode)
    ==>
    p = Popen(["somestring"], shell=True, bufsize=bufsize
              stdin=PIPE, stdout=PIPE, close_fds=True)
    (child_stdout, child_stdin) = (p.stdout, p.stdin)


    (child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode)
    ==>
    p = Popen(["mycmd", "myarg"], bufsize=bufsize,
              stdin=PIPE, stdout=PIPE, close_fds=True)
    (child_stdout, child_stdin) = (p.stdout, p.stdin)

popen2.Popen3popen3.Popen4的工作方式基本和subprocess.Popen一樣,除了:

  • 如果執行失敗,subprocess.Popen將觸發異常。
  • capturestderr參數將用 stderr 替換。
  • stdin=PIPEstdout=PIPE必須指定。
  • popen2默認會關閉所有文件描述符,而 subprocess.Popen 則必須指定close_fds=True才行。

未決議題(Open Issues)


有些特性已有人提出要求,但尚未實現。包括:

  • 對子進程族的管理功能。
  • 「守護」進程的管理功能。
  • 提供殺死子進程的內置方法。

當然這些特性是很有用,預計後續添加也毫無問題。

  • 貌似需要的功能,包括 pty 的支援。

pty 功能高度依賴於平台,這是一個難題。並且已有其他模組提供了該類功能 [^注6]。

向下兼容性(Backwards Compatibility)


這是一個新模組,估計不會出現重大的向下兼容問題。模組名稱「subprocess」可能會與以前的同名模組[^注3]發生衝突,但名稱「subprocess」似乎是迄今為止最好的名稱。本模組的第一個名字是「popen5」,但覺得太不直觀了。有一段時間,本模組被稱為「process」,但已被 Trent Mick 的模組[^注4]用掉了。

為了保持向下兼容,預計在未來很長一段時間內,本模組試圖替換的函數和模組(os.systemos.spawn*os.popen*popen2.*commands.*)在後續 Python 版本中依然可用。

參考實現程式碼(Reference Implementation)


//www.lysator.liu.se/~astrand/popen5/

參考文獻


[注1] Linux 和 Unix 安全編程指南,第 8.3 節。//www.dwheeler.com/secure-programs/

[注2] Python Dialog //pythondialog.sourceforge.net/

[注3] //www.iol.ie/~padraiga/libs/subProcess.py

[注4] //starship.python.net/crew/tmick/

[注5] //starship.python.net/crew/mhammond/win32/

[注6] //www.lysator.liu.se/~ceder/pcl-expect/