一篇技術文章如果僅僅是理論上講得天花亂墜,卻不能自己擼出東西來,那麼它寫的再好,也只能算紙上談兵。繼上一篇 《深入 Shell 管道符的內部原理》 收到大量讀者粉絲的點贊之後,本篇我們自己來實現一下管道符的功能。比如我們將支援下面的複雜指令,有很多個管套符串起來的一系列指令。
$ cmd1 | cmd2 | cmd3 | cmd4 | cmd5複製程式碼
我們要使用 Python 語言,因為 Go 和 Java 語言都不支援 fork 函式。我們最終需要的是下面這張圖,這張圖很簡單,但是為了構造出這張圖,是需要費一番功夫的。
程式的程式碼檔名是 pipe.py,程式的執行形式如下
python pipe.py "cat pipe.py | grep def | wc -l"複製程式碼
統計 pipe.py 檔案程式碼中包含 def 單詞的個數,輸出
3複製程式碼
指令執行
每一條指令的執行都需要至少攜帶一個管道,左邊的管道或者右邊的管道。第一個指令和最後一個指令只有一個管道,中間的指令有兩個管道。管道的標識是它的一對讀寫描述符(r, w)。
左邊管道的讀描述符 left_pipe[0] 對接程式的標準輸入。右邊管道的寫描述符 right_pipe[1] 對接程式的標準輸出。調整完描述符後,就可以使用 exec 函式來執行指令。
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 函式來建立子程式,然後使用子程式來執行指令。
子又生孫,孫又生子,子子孫孫無窮盡也。理論上使用管道可以串接非常多的程式輸入輸出流。
# 指令的列表以及下一條指令左邊的管道作為引數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複製程式碼
你會發現這三個指令程式都共享同一個父程式,這個父程式就是 Python 程式。如上圖所示,我們平時使用的 shell 在執行指令的時候形成的程式關係都是這種形式的,這種形式在邏輯結構上看起來更加清晰。
需要上面的完整原始碼,請關注下面的公眾號,在裡面回覆「管道」即可得到原始碼。
閱讀更多深度技術文章,掃一掃上面的二維碼關注微信公眾號「碼洞」