一个小小的 Shell 管道符,内部实现可真不简单!

一个小小的 Shell 管道符,内部实现可真不简单!

管道命令我们经常使用,将一个指令的输出导入另一个指令的输入,也就是屁股对上嘴,这个原理连编程小学生都知道。但是如果要深入问进去,一个指令的输出是如何导入到另一个指令的输入,管道又起到什么角色,估计能回答这个问题的人不足 1%。下面我们来深入分析一下管道指令的实现原理,对于下面的这条指令,shell 到底干了些什么

$ cmd1 | cmd2

首先我用下面这张图来描述最终形态,然后再一步一步来分解最终形态的形成过程

一个小小的 Shell 管道符,内部实现可真不简单!

上图我们看到了进程描述符表、管道、进程的父子关系。

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 信号,默认动作是进程直接退出。

相关推荐