自己動手實現 Shell 多程式管道符

老錢發表於2018-10-22

一篇技術文章如果僅僅是理論上講得天花亂墜,卻不能自己擼出東西來,那麼它寫的再好,也只能算紙上談兵。繼上一篇 《深入 Shell 管道符的內部原理》 收到大量讀者粉絲的點贊之後,本篇我們自己來實現一下管道符的功能。比如我們將支援下面的複雜指令,有很多個管套符串起來的一系列指令。

$ cmd1 | cmd2 | cmd3 | cmd4 | cmd5
複製程式碼

我們要使用 Python 語言,因為 Go 和 Java 語言都不支援 fork 函式。我們最終需要的是下面這張圖,這張圖很簡單,但是為了構造出這張圖,是需要費一番功夫的。

自己動手實現 Shell 多程式管道符

程式的程式碼檔名是 pipe.py,程式的執行形式如下

python pipe.py "cat pipe.py | grep def | wc -l"
複製程式碼

統計 pipe.py 檔案程式碼中包含 def 單詞的個數,輸出

3
複製程式碼

指令執行

每一條指令的執行都需要至少攜帶一個管道,左邊的管道或者右邊的管道。第一個指令和最後一個指令只有一個管道,中間的指令有兩個管道。管道的標識是它的一對讀寫描述符(r, w)。

自己動手實現 Shell 多程式管道符

左邊管道的讀描述符 left_pipe[0] 對接程式的標準輸入。右邊管道的寫描述符 right_pipe[1] 對接程式的標準輸出。調整完描述符後,就可以使用 exec 函式來執行指令。

自己動手實現 Shell 多程式管道符

def run_cmd(cmd, left_pipe, right_pipe):
    if left_pipe:
        os.dup2(left_pipe[0], sys.stdin.fileno())
        os.close(left_pipe[0])
        os.close(left_pipe[1])
    if right_pipe:
        os.dup2(right_pipe[1], sys.stdout.fileno())
        os.close(right_pipe[0])
        os.close(right_pipe[1])
    # 分割指令引數
    args = [arg.strip() for arg in cmd.split()]
    args = [arg for arg in args if arg]
    try:
        # 傳入指令名稱、指令引數陣列
        # 指令引數陣列的第一個引數就是指令名稱
        os.execvp(args[0], args)
    except OSError as ex:
        print "exec error:", ex
複製程式碼

程式關係

shell 需要執行多個程式,就必須用到 fork 函式來建立子程式,然後使用子程式來執行指令。

自己動手實現 Shell 多程式管道符
子又生孫,孫又生子,子子孫孫無窮盡也。理論上使用管道可以串接非常多的程式輸入輸出流。

# 指令的列表以及下一條指令左邊的管道作為引數
def run_cmds(cmds, left_pipe):
    # 取出指令串的第一個指令,即將執行這第一個指令
    cur_cmd = cmds[0]
    other_cmds = cmds[1:]
    # 建立管道
    pipe_fds = ()
    if other_cmds:
        pipe_fds = os.pipe()
    # 建立子程式
    pid = os.fork()
    if pid < 0:
        print "fork process failed"
        return
    if pid > 0:
        # 父程式來執行指令
        # 同時傳入左邊和右邊的管道(可能為空)
        run_cmd(cur_cmd, left_pipe, pipe_fds)
    elif other_cmds:
        # 莫忘記關閉不再使用的描述符
        if left_pipe:
            os.close(left_pipe[0])
            os.close(left_pipe[1])
        # 子程式遞迴繼續執行後續指令,攜帶新建立的管道
        run_cmds(other_cmds, pipe_fds)

複製程式碼

啟動指令碼

需要對命令列引數按豎線進行分割得出多條指令,開始進入遞迴執行

def main(cmdtext):
    cmds = [cmd.strip() for cmd in cmdtext.split("|")]
    # 第一條指令左邊沒有管道
    run_cmds(cmds, ())
    
if __name__ == '__main__':
    main(argv[1])
複製程式碼

觀察程式關係

因為例子中的幾條指令執行時間太短,無法通過 ps 命令來觀察程式關係。所以我們在程式碼里加了一句除錯用的輸出程式碼,輸出當前程式執行的指令名稱、程式號和父程式號。

def run_cmd(cmd, left_pipe, right_pipe):
   print cmd, os.getpid(), os.getppid()
   ...
複製程式碼

執行指令碼時觀察輸出

$ python pipe.py "cat pipe.py | grep def | wc -l"
cat pipe.py 49782 4503
grep def 49783 49782
wc -l 49784 49783
       3
複製程式碼

從輸出中可以明顯看出父子程式的關係,第 N 條指令程式是第 N+1 條指令程式的父程式。在 run_cmds 函式中,fork 出子程式後由父程式來負責執行當前指令,剩餘的指令交給子程式執行。所以才形成了上面的程式關係。讀者可以嘗試調整互動執行順序,讓子程式負責執行當前指令,然後再觀察輸出

$ python pipe.py "cat pipe.py | grep def | wc -l"
cat pipe.py 49949 49948
grep def 49950 49948
wc -l 49951 49948
       3
複製程式碼

自己動手實現 Shell 多程式管道符

你會發現這三個指令程式都共享同一個父程式,這個父程式就是 Python 程式。如上圖所示,我們平時使用的 shell 在執行指令的時候形成的程式關係都是這種形式的,這種形式在邏輯結構上看起來更加清晰。

需要上面的完整原始碼,請關注下面的公眾號,在裡面回覆「管道」即可得到原始碼。

自己動手實現 Shell 多程式管道符

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

相關文章