自己動手實現 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 4503grep def 49783 49782wc -l 49784 49783 
 
 
 
3複製程式碼

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

$ python pipe.py "cat pipe.py | grep def | wc -l"cat pipe.py 49949 49948grep def 49950 49948wc -l 49951 49948 
 
 
 
3複製程式碼
自己動手實現 Shell 多程式管道符

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

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

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

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

來源:https://juejin.im/post/5bcd33456fb9a05d2f36e799

相關文章