一個小小的 Shell 管道符,內部實現可真不簡單!

老錢發表於2018-10-19

管道命令我們經常使用,將一個指令的輸出匯入另一個指令的輸入,也就是屁股對上嘴,這個原理連程式設計小學生都知道。但是如果要深入問進去,一個指令的輸出是如何匯入到另一個指令的輸入,管道又起到什麼角色,估計能回答這個問題的人不足 1%。下面我們來深入分析一下管道指令的實現原理,對於下面的這條指令,shell 到底幹了些什麼

$ cmd1 | cmd2
複製程式碼

首先我用下面這張圖來描述最終形態,然後再一步一步來分解最終形態的形成過程

圖片

上圖我們看到了程式描述符表、管道、程式的父子關係。

fork 和 exec

shell 每次執行指令, 需要 fork 出一個子程式來執行,然後將子程式的映象替換成目標指令,這又會用到 exec 函式。比如下面這條簡單的指令

$ cmd
複製程式碼

一個小小的 Shell 管道符,內部實現可真不簡單!

exec 函式不會改變當前程式的程式號,不會改變程式之間的父子關係。可以將程式看成一個帶殼的球體,exec 之後,外面的殼不會變,球裡面的東西被完全替換了。而輸入輸出檔案描述符預設在殼上面,這意味著指令 cmd 的輸入輸出繼承了 shell 程式的輸入輸出。

$ cmd1 | cmd2
複製程式碼

當指令裡面包含一個管道符,意味著需要並行執行兩個指令,這時候 shell 需要 fork 兩次生成兩個子程式,然後分別 exec 換成目標指令。

一個小小的 Shell 管道符,內部實現可真不簡單!

我們注意到圖裡面還有一個 pipe,它就是負責父子程式通訊的管道。

pipe

管道用於父子程式的通訊,在 fork 之前建立 pipe,pipe將成為 fork 之後父子程式之間的紐帶。pipe 函式會返回兩個描述符(pipe_in, pipe_out),一個用於讀,一個用於寫。

一個小小的 Shell 管道符,內部實現可真不簡單!

dup2

下面我們就需要調整圖中描述符的尖頭,將 cmd1 程式的 stdout 描述符指向管道寫,將 cmd2 程式的 stdin 描述符指向管道讀,這就需要神奇的 dup2(fd1, fd2) 函式,它的作用是將 fd1 描述符關聯 fd2 指向的核心物件,之前 fd1 指向的核心物件引用計數減一,如果減到零就銷燬。注意平時我們呼叫 close 方法本質上只是遞減引用計數,同一個核心物件是可以被多個程式共享的。當引用計數減到零時就會正式關閉。

一個小小的 Shell 管道符,內部實現可真不簡單!

下面我們將 dup2 函式的規則應用一下,對兩個程式分別呼叫 dup2 方法得到

一個小小的 Shell 管道符,內部實現可真不簡單!

然後再將不需要的描述符關閉掉,就得到了下面的終極圖,完美!

一個小小的 Shell 管道符,內部實現可真不簡單!

如果是兩個管道符三個命令如下,就會生成兩個管道

$ cmd1 | cmd2 | cmd3
複製程式碼

一個小小的 Shell 管道符,內部實現可真不簡單!

如果任意一端的程式突然掛掉了會發生什麼?

假設 cmd1 先掛掉,管道寫被動關閉,cmd2 在讀取管道內容時會遭遇 EOF,然後正常結束。 假設 cmd2 先掛掉,管道讀被動關閉,cmd1 繼續寫管道,這時候程式會收到一個 SIGPIPE 訊號,預設動作是程式直接退出。

下一篇我們將使用酷炫的程式碼來實現上面的整個過程,我們不僅要知道其中的原理,而且還需要通過親手實驗來了解其中更多的細枝末節。

一個小小的 Shell 管道符,內部實現可真不簡單!

閱讀更多深度技術文章,掃一掃上面的二維碼關注微信公眾號「碼洞」

相關文章