管道命令我們經常使用,將一個指令的輸出導入另一個指令的輸入,也就是屁股對上嘴,這個原理連編程小學生都知道。但是如果要深入問進去,一個指令的輸出是如何導入到另一個指令的輸入,管道又起到什麼角色,估計能回答這個問題的人不足 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 信號,默認動作是進程直接退出。

閱讀更多有趣文章,用微信掃一掃上面的二維碼關注公眾號「碼洞」


推薦閱讀:
查看原文 >>
相關文章