當前位置:
首頁 > 知識 > 深入理解子進程:Python 相關源碼解析

深入理解子進程:Python 相關源碼解析

很多時候,我需要寫腳本去做一些自動化操作,簡單的可以直接寫 Shell 腳本,但一些稍複雜的情況, 比如要用到分支語句,循環語句,或者調用一些高級函數,用 Shell 就太費勁了。 我更喜歡用一種完整的語言(比如 Python),調用 Shell 程序並獲取它的輸出,執行複雜操作。

本文介紹 UNIX 進程的創建過程(fork, exec),如何與子進程通信(pipe, pty), 並深入分析標準庫中的 subprocess 模塊和著名的第三方庫 sh 的源碼,闡述其實現原理。

這些庫提供了非常易用的介面,大部分時候只要看看文檔就能解決問題。 但它們也無法徹底掩蓋實現細節,The Law of Leaky Abstractions - 抽象漏洞法則 解釋了許多不完美的抽象,希望這篇文章也能幫你更好地理解子進程。

UNIX 進程的創建過程fork

fork 函數以複製調用進程的方式創建新進程,它調用一次,返回兩次,在子進程中它返回 , 在父進程中它返回子進程的 ID。

fork 之後父子進程各自繼續往下執行,所以示例中的兩個 都會執行, 子進程通過 退出,否則兩個進程共用標準輸入,shell 就不能正常工作了。

是個系統調用,可通過 查看其手冊。

也是個系統調用,它與 的區別在於: 直接退出, 而 先執行一些清理工作再退出。可通過 和 查看其手冊。

exec

exec 是一系列函數,總共有七個,它把當前進程執行的程序替換成新程序,正常情況下它們調用一次,永不退出

執行示例中的代碼之後,進程就變成了 sh,無法再回到 python。

exec 函數中,通常 execve 是系統調用,其他幾個都能通過它來實現, 可通過 和 查看其手冊。

waitpid

通常 fork 之後,子進程調用 exec 執行新程序,父進程要等待子進程的結果, 這就可以通過 waitpid 實現。如果父進程沒有調用 waitpid 獲取子進程的退出狀態, 那麼子進程退出後,狀態信息會一直保留在內存中,這樣的子進程也被稱為殭屍進程。

這就是 的實現原理,子進程共用父進程的標準輸入輸出,父進程阻塞,直到子進程結束。 因為子進程的輸出是直接送到標準輸出,所以無法被父進程獲取。

可通過 和 查看其手冊。

dup2

dup2 可以複製文件描述符, 它會先把 newfd 關閉,再把 oldfd 複製到 newfd, 經常用它來修改進程的標準 I/O。

可通過 查看手冊。

進程間通信管道

進程之間有很多種通信方式,這裡只討論管道這種方式,這也是最常用的一種方式。 管道通常是半雙工的,只能一端讀,另一端寫,為了可移植性,不能預先假定系統支持全雙工管道。

可通過 查看其手冊。

緩衝 I/O

I/O 可分為:無緩衝行緩衝全緩衝三種。

通過 read 和 write 系統調用直接讀寫文件,就是無緩衝模式,性能也最差。 而通過標準 I/O 庫讀寫文件,就是緩衝模式,標準 I/O 庫提供緩衝的目的是儘可能減少 read 和 write 調用的次數,提高性能。

行緩衝模式,當在輸入輸出中遇到換行符時,才進行實際 I/O 操作。

全緩衝模式,當填滿緩衝區時,才進行實際 I/O 操作。

管道和普通文件默認是全緩衝的,標準輸入和標準輸出默認是行緩衝的,標準錯誤默認是無緩衝的。

這個例子中,讀管道這步會一直阻塞。有兩個原因:

寫管道有緩衝,沒有進行實際 I/O,所以讀端讀不到數據

讀管道也有緩衝,要讀滿緩衝區才會返回

只要滿足其中任何一個條件都會阻塞。通常寫管道是在子進程中進行,我們無法控制其緩衝, 這也是管道的一個局限性。

偽終端

偽終端看起來就像一個雙向管道,一端稱為 master(主),另一端稱為 slave(從)。 從端看上去和真實的終端一樣,能夠處理所有的終端 I/O 函數。

偽終端 echo 默認是開啟的,所以最後 master 讀的時候,會先讀出之前寫入的數據。

如果寫入的數據沒有換行符,就可能不會被傳送到另一端, 造成讀端一直阻塞(猜測是偽終端的底層實現使用了行緩衝)。

可通過 和 查看其手冊。

subprocess 的實現

subprocess 提供了很多介面, 其核心是 Popen(Process Open),其他部分都通過它來實現。subprocess 也支持 Windows 平台, 這裡只分析它在 Unix 平台的實現。

subprocess 源碼: https://github.com/python/cpython/blob/3.6/Lib/subprocess.py#L540

首先看一下介面原型(L586),參數非常多:

然後看到中間(L648) 一大段注釋:

父子進程通過這三對文件描述符進行通信。

再跳到 的實現(L1144):

它創建了三對文件描述符, , , 這三個參數都是類似的, 分別對應一對文件描述符。當參數等於 時,它就會創建一條管道,這也是最常用的參數。

回到 Popen,接著往下看(L684):

它把其中三個文件描述符變成了文件對象,用於在父進程中和子進程通信。 注意 參數只作用於父進程中的文件對象,子進程中的緩衝是沒法控制的。

和 參數就不贅述了,可以查看 io 模塊的文檔。

再看 的實現(L1198):

看到這裡, , , 這幾個參數就很好理解了。

可以是字元串或列表, 表示通過 shell 程序執行命令, 是 shell 程序的路徑,默認是 。

當 是字元串時,通常配合 使用,用來執行任意 shell 命令。

接著往下看(L1221):

這是第四對文件描述符。子進程在 fork 之後到 exec 之前會執行許多步驟, 萬一這些步驟失敗了,就通過這對文件描述向父進程傳遞異常信息,父進程收到後拋出相應的異常。

創建子進程並執行命令(L1252):

繼續追蹤 源碼: https://github.com/python/cpython/blob/3.6/Modules/_posixsubprocess.c#L545

在子進程中執行 child_exec,父進程返回 pid。

繼續看 child_exec 的實現(L390):

把 Popen 中 參數指定的文件描述符設為可繼承, 它通過對每個文件描述符調用 ,清除 標誌。

需要注意,父進程的標準 I/O 默認是可繼承的。

參考 PEP 446 – Make newly created file descriptors non-inheritable

( ) 標誌表示這個文件會在進程執行 exec 系統調用時自動關閉。

接著往下看(L430):

通過 dup2 系統調用,把 p2cread 設為子進程的標準輸入。標準輸出和標準錯誤也是類似的。

接著往下看(L463):

參數,設置當前工作目錄。

參數,把信號處理恢復為默認值,涉及信號處理的內容,這裡略過。

,即 Popen 的 參數, 創建會話並設置進程組 ID,內容太多也略過。

接著往下看(L474):

執行 ,隨後根據 參數判斷是否關閉打開的文件描述符。

最後,執行命令(L497):

嘗試執行命令,只要有一個成功,後面的就不會執行了,因為 exec 執行成功後永不返回。

Popen 的創建過程到這就結束了,子進程已經運行起來了,接下來分析如何與子進程通信。

跳到 Popen 中的 communicate 方法(L796):

這個 是為什麼呢?稍後解答。

繼續往下看(L813):

部分直接和子進程通信, 部分調用了 , 用線程和 I/O 多路復用的方式與子進程通信,去掉影響也不大,這部分細節就不分析了。 注意 很關鍵,在 中也是調用它向子進程寫數據。

看 的實現(L773):

省略了異常處理的代碼,主要就這兩句。可以看到寫完輸入之後,立即把標準輸入關閉了。 結合上面的 ,可以看出只能向子進程寫入一次。

這涉及到緩衝的問題,管道是默認全緩衝的,如果不立即關閉寫端,子進程讀的時候就可能一直阻塞, 我們沒法控制子進程使用什麼類型的緩衝。同樣的,如果子進程一直寫輸出,父進程也會讀不到數據, 只有子進程退出之後,系統自動關閉它的標準輸出,父進程才能讀到數據。

下面是個例子:

運行 ,儘管子進程在不停地輸出,但是父進程一直讀取不到數據。 此時如果我們把子進程殺死,父進程便會立即讀到數據。

subprocess 的核心代碼到這就分析完了。

再放個自己實現的 popen(未考慮各種異常情況,僅用於說明實現原理):

執行命令試一試:

題圖:pexels,CC0 授權。

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 編程派 的精彩文章:

深入理解子進程
改善程序員生活質量的 3 10 習慣
一些學習 Python 編程的電子書資源
Python 在騰訊雲的實踐
可視化爬蟲 Portia 試用體驗

TAG:編程派 |

您可能感興趣

深入解析Struts攔截器的工作原理
為你詳細解析Python中的線程與進程的區別
Photoshop詳細解析人物插畫肌理繪製過程
AtomicInteger 源碼解析
Word2v的ec 原理解析!
Google研究員深入解析Anthos和開源戰略
使用 Python 解析參數
c井Queue源碼解析
Ansible 深度解析
hbase查詢解析
FutureTask 在線程池中應用和源碼解析
Photoshop解析合成教程中紋理貼圖運用
Photoshop詳細解析ICON圖標繪製教程
Android圖片載入框架最全解析二,從源碼的角度理解Glide的執行流程
Android項目解耦-路由框架ARouter源碼解析
Photoshop詳細解析商業人像後期修圖過程
InterValue:新型抗量子攻擊密碼演算法解析
Spring源碼解析——Spring思想、設計模式總結
Google的開源Consent解決方案解析APUS研究院
教你用Python解析HTML