PEP 324 -- subprocess 新的行程模塊(subprocess - New process module)
英文原文:https://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)
- 原由(Rationale)
- 規范(Specification)
- 例外(Exceptions)
- 安全性(Security)
- Popen物件(Popen objects)
- 用subprocess模塊替換的舊函式(Replacing older functions with the subprocess module)
- 替換/bin/sh反引號shell命令(Replacing /bin/sh shell backquote)
- 替換shell管道命令(Replacing shell pipe line)
- 替換
os.system()(Replacing os.system()) - 替換
os.spawn系列函式(Replacing os.spawn*) - 替換
os.popen系列函式(Replacing os.popen*) - 替換
popen2系列函式(Replacing popen2.*)
- 開放議題(Open Issues)
- 兼容性(Backwards Compatibility)
- 參考實體(Reference Implementation)
- 參考文獻(References)
- 著作權(Copyright)
摘要(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) - 不采用標準的C函式
-
提供“預執行(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(無緩沖), -
stdin、stdout和stderr分別指定了被執行程式的標準輸入、標準輸出和標準錯誤檔案句柄,合法值可以是PIPE、已存在的檔案描述符(正整數)、已存在的檔案物件或者None,PIPE表示應該為子行程新建一個管道,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()方法更新, -
如果給出了
startupinfo和creationflags,則會傳給底層的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 如果stdin為PIPE,則本屬性將是一個檔案物件,用于向子行程提供輸入,否則為None,
stdout 如果stdout為PIPE,則本屬性將是一個檔案物件,用于為子行程提供輸出,否則為None,
stderr 如果stderr為PIPE,則本屬性將是一個檔案物件,用于為子行程提供錯誤輸出,否則為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.Popen3和popen3.Popen4的作業方式基本和subprocess.Popen一樣,除了:
- 如果執行失敗,
subprocess.Popen將觸發例外, capturestderr引數將用 stderr 替換,stdin=PIPE和stdout=PIPE必須指定,popen2默認會關閉所有檔案描述符,而subprocess.Popen則必須指定close_fds=True才行,
未決議題(Open Issues)
有些特性已有人提出要求,但尚未實作,包括:
- 對子行程族的管理功能,
- “守護”行程的管理功能,
- 提供殺死子行程的內置方法,
當然這些特性是很有用,預計后續添加也毫無問題,
- 貌似需要的功能,包括 pty 的支持,
pty 功能高度依賴于平臺,這是一個難題,并且已有其他模塊提供了該類功能 [^注6],
向下兼容性(Backwards Compatibility)
這是一個新模塊,估計不會出現重大的向下兼容問題,模塊名稱“subprocess”可能會與以前的同名模塊[^注3]發生沖突,但名稱“subprocess”似乎是迄今為止最好的名稱,本模塊的第一個名字是“popen5”,但覺得太不直觀了,有一段時間,本模塊被稱為“process”,但已被 Trent Mick 的模塊[^注4]用掉了,
為了保持向下兼容,預計在未來很長一段時間內,本模塊試圖替換的函式和模塊(os.system、os.spawn*、os.popen*、popen2.*、commands.*)在后續 Python 版本中依然可用,
參考實作代碼(Reference Implementation)
http://www.lysator.liu.se/~astrand/popen5/
參考文獻
[注1] Linux 和 Unix 安全編程指南,第 8.3 節,http://www.dwheeler.com/secure-programs/
[注2] Python Dialog http://pythondialog.sourceforge.net/
[注3] http://www.iol.ie/~padraiga/libs/subProcess.py
[注4] http://starship.python.net/crew/tmick/
[注5] http://starship.python.net/crew/mhammond/win32/
[注6] http://www.lysator.liu.se/~ceder/pcl-expect/
著作權(Copyright)
本文已于公共區域發布,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/287069.html
標籤:其他
上一篇:高級區塊鏈工程師評定
