管道命令我們經常使用,將一個指令的輸出匯入另一個指令的輸入,也就是屁股對上嘴,這個原理連程式設計小學生都知道。但是如果要深入問進去,一個指令的輸出是如何匯入到另一個指令的輸入,管道又起到什麼角色,估計能回答這個問題的人不足 1%。下面我們來深入分析一下管道指令的實現原理,對於下面的這條指令,shell 到底幹了些什麼
$ cmd1 | cmd2
複製程式碼
首先我用下面這張圖來描述最終形態,然後再一步一步來分解最終形態的形成過程
上圖我們看到了程式描述符表、管道、程式的父子關係。
fork 和 exec
shell 每次執行指令, 需要 fork 出一個子程式來執行,然後將子程式的映象替換成目標指令,這又會用到 exec 函式。比如下面這條簡單的指令
$ cmd
複製程式碼
exec 函式不會改變當前程式的程式號,不會改變程式之間的父子關係。可以將程式看成一個帶殼的球體,exec 之後,外面的殼不會變,球裡面的東西被完全替換了。而輸入輸出檔案描述符預設在殼上面,這意味著指令 cmd 的輸入輸出繼承了 shell 程式的輸入輸出。
$ cmd1 | cmd2
複製程式碼
當指令裡面包含一個管道符,意味著需要並行執行兩個指令,這時候 shell 需要 fork 兩次生成兩個子程式,然後分別 exec 換成目標指令。
我們注意到圖裡面還有一個 pipe,它就是負責父子程式通訊的管道。
pipe
管道用於父子程式的通訊,在 fork 之前建立 pipe,pipe將成為 fork 之後父子程式之間的紐帶。pipe 函式會返回兩個描述符(pipe_in, pipe_out),一個用於讀,一個用於寫。
dup2
下面我們就需要調整圖中描述符的尖頭,將 cmd1 程式的 stdout 描述符指向管道寫,將 cmd2 程式的 stdin 描述符指向管道讀,這就需要神奇的 dup2(fd1, fd2) 函式,它的作用是將 fd1 描述符關聯 fd2 指向的核心物件,之前 fd1 指向的核心物件引用計數減一,如果減到零就銷燬。注意平時我們呼叫 close 方法本質上只是遞減引用計數,同一個核心物件是可以被多個程式共享的。當引用計數減到零時就會正式關閉。
下面我們將 dup2 函式的規則應用一下,對兩個程式分別呼叫 dup2 方法得到
然後再將不需要的描述符關閉掉,就得到了下面的終極圖,完美!
如果是兩個管道符三個命令如下,就會生成兩個管道
$ cmd1 | cmd2 | cmd3
複製程式碼
如果任意一端的程式突然掛掉了會發生什麼?
假設 cmd1 先掛掉,管道寫被動關閉,cmd2 在讀取管道內容時會遭遇 EOF,然後正常結束。 假設 cmd2 先掛掉,管道讀被動關閉,cmd1 繼續寫管道,這時候程式會收到一個 SIGPIPE 訊號,預設動作是程式直接退出。
下一篇我們將使用酷炫的程式碼來實現上面的整個過程,我們不僅要知道其中的原理,而且還需要通過親手實驗來了解其中更多的細枝末節。
閱讀更多深度技術文章,掃一掃上面的二維碼關注微信公眾號「碼洞」