Shell 中的 Pipe 破裂机制浅谈

来源:互联网 发布:php开源采集类 编辑:程序博客网 时间:2024/04/28 06:31

内容转自CU lightspeed 的解说 很精彩!


Pipe 是 shell 中进程间通信的一种机制, 虽然大家经常使用, 但对于不搞开发


人来说, 其实现的一些细节未必很清楚。 本文就想以 shell 用户,而不是


C programmer 的角度探讨一下 shell 中 pipe 的实现细节。因此论述不一定


特别精确。这里讨论的是普通的单向 pipe, 不论及 co-process 和 FIFO.


另外,文中的大部分例子是为说明问题而设计的, 不具有实际价值, 但原理与


实际应用是相同的。




如果想深入了解 pipe 底层的实现原理,请参考 Advanced Programming in the Unix. 


如需要各个 shell 中 pipe 的准确实现, 请去下面 download 各 shell 


的 source code.




Bash:  http://cnswww.cns.cwru.edu/~chet/bash/bashtop.html


Ksh:   http://www.kornshell.com/


Pdksh: http://web.cs.mun.ca/~michael/pdksh/


Tcsh:  http://www.tcsh.org/


Zsh:   http://www.zsh.org/






1. Pipe 的开始




考虑到下面  shell 中的 pipe:




        cmd1 | cmd2 |.....|cmdN




Shell 将 cmd1 .. cmdN-1 的 stdout 定向到其后面相应的 pipe,


将 cmd2 .. cmdN 的 stdin 定向到其前面相应的 pipe.


并且将 cmd1 到 cmdN 的每一个命令放入一个 sub-shell 中同时执行。


(注意, 没有绝对的同时, 后面就有一个两个进程临界状态竞争的例子。)




对于最后一个命令 cmdN 则有一个例外, 就是如果 cmdN 是 shell 的 builtin 命令


的话, ksh 及 zsh 会在当前的 shell 中执行 cmdN, 而不是启动 sub-shell.


这是一个非常好的特性。否则会产生令人困惑的代码。关于这个问题,请参考 




Shell 经典问题之 [ I/O 重定向] [2] 


http://bbs.chinaunix.net/forum/24/20041119/447969.html




BASH FAQ


http://www.unixguide.net/unix/bash/E4.shtml






2. Pipe 的结束




如果 cmd1 .. cmdN 每一个命令运行结束, 则整个pipe 结束。 那是不是说整个pipe 


的运行时间一定由由运行时间最长的 cmd 决定呢? 答案是否定的。 因为 shell 的理念是:


我们最关心的是 pipe 最后的命令 cmdN, 如果 cmdN 运行结束了, 有什么理由让前面


的 cmdX 继续运行呢。 shell 用信号 SIGPIPE (13) 来实现这种结束机制。


考虑下面 pipe 两端的 cmd1 及 cmd2 的两种情况:




        cmd1 | cmd2




a. cmd2 的运行时间大于 cmd1 的运行时间




此时不需要 SIGPIPE.  当 cmd1 运行结束时, pipe 左端关闭, 此时 pipe 破裂.


当 cmd2 继续从 pipe 读取数据时, 会产生读错误 ( 系统调用 read (2) 返回 0, 


shell 中的 read  返回 1). 大部分命令(如下例中的 grep)会处理这个错误, 一般的动作是退出. 


此时整个pipe 结束.




         find / | grep aaa




如果忘记处理这个错误, cmd2 也许永不结束, 如下例:




        cat /etc/hosts | while true; do read a; echo $a; done




b. cmd1 的运行时间大于 cmd2 的运行时间




当 cmd2 运行结束时, pipe 右端关闭, 此时 pipe 破裂.


当 cmd1 继续从 向 pipe 写入数据时, 系统会产生 SIGPIPE 信号, 接到 SIGPIPE 的 cmd1的


default 行为就是 exit, 此时整个pipe 结束. 




例如, find / 可能要运行半个小时, 由于有了 SIGPIPE 信号, find / |head -1 在输出一行后


几乎马上返回.




如果想验证某个命令(例如 find)是否正常响应 SIGPIPE 信号, 可用如下方法: 






        # find / >/dev/null 2>&1 &


        # ps -ef|grep find 


        # kill -PIPE pid_of_find




可以用 trap 来验证是否 cmd1 接到了 SIGPIPE 信号:




        (trap "echo TRAP SIGPIPE >&2" PIPE; sleep 1; echo a)|:




注意, 产生 SIGPIPE 信号的条件是 pipe 右端关闭后左端继续从 向 pipe 写入. 如果左端没有


输出, 如




        sleep 100 | sleep 1




或左端进行了重定向, 如




        cmd1 > a | cmd2




则pipe 破裂后也不会产生 SIGPIPE, 很显然,  cmd1 会正常运行结束而不会中断。




一般 cmd1 无输出的例子在实际应用中较常见, 如下例




        find / -name aaa | head -10




查找系统不定期产生的文件 aaa, 并用 cmd2 来处理. 如果 aaa 不存在就是这种情况.


注意,此例中的右端有 read  存在, pipe 不会破裂. 




而 cmd1 > a | cmd2 的形式就很少见, 因为如果二者之间没有 I/O, 要 pipe 干什么呢.


这涉及到 pipe 的一种特殊的使用, 就是将两个命令在前台并行执行而不是 & 的后台方式.




如并行执行 cmd1 及 cmd2 并将结果输出到屏幕上:




        cmd1 >&2 | cmd2








3. Pipe 中的 shell script




当 pipe 中的 cmd 是 shell script 时, 经常由于输出的不连续造成运行效果的不同.


我们来分析下面语句.




         (echo aaa; sleep 200) | (sleep 1;echo bbb)




0 秒: Pipe 右端运行 sleep 1


0 秒: Pipe 左端运行 echo aaa,几乎立刻完成, 然后运行 sleep 200 


1 秒: Pipe 右端运行 echo bbb,几乎立刻完成, 右端 pipe 关闭, pipe 破裂, 右端运行结束.


1 秒: Pipe 左端继续运行 sleep 200 直至完成.




Pipe 总运行时间大约: 200s




再看另外一个:




        (sleep 1; echo aaa; sleep 200)|:


        


0 秒: Pipe 右端运行 空语句 : ,几乎立刻完成, 右端 pipe 关闭, pipe 破裂, 右端运行结束.


0 秒: Pipe 左端运行 sleep 1


1 秒: Pipe 左端运行 echo aaa, 因右端 pipe 关闭而收到 SIGPIPE 信号, 左端退出, 结束.




Pipe 总运行时间大约: 1s




对有任何命令 cmd, 可计算其第一个输出时所占用的时间并退出:




        time cmd|:




最后是一个有点极端而又有趣的例子:




        (echo aaa; sleep 200) | echo aaa




0 秒: Pipe 右端运行 echo aaa


0 秒: Pipe 左端运行 echo aaa




如果右端 echo 先运行结束, 按上面的分析, Pipe 总运行时间大约: 0s


如果左端 echo 先运行结束, 按上面的分析, Pipe 总运行时间大约: 200s




实际上没有绝对的开始时间,而运行时间也非绝对相等。 左右端的 echo aaa 在大约 0 秒时


展开竞争, 无法预测谁先结束.因此, pipe 的运行时间有两种可能: 


大约 0s 或 200s.  当然, 根据不同的 shell 的算法及运行时系统的状态的关系, 


有时某一端先完成的几率可能大大大于另外一端, 但只要运行足够多的次数(一般几十次足矣), 


两种结果一定都会出现。






4. Builtin 命令及外部命令




还拿上面的运行时间为 1s 的一个例子来看:




        (sleep 1; echo aaa; sleep 200)|:   




产生输出的是 builtin 命令 echo. 但 shell 中往往产生大量输出的是外部命令,


这两者有区别吗? 




        (sleep 1; /usr/bin/echo aaa; sleep 200)|:   




第一印象这两者相同, 都运行 1s 接受到 SIGPIPE 后而结束. 然而, 后者需要 200s.


怎么回事? 原来导致pipe 破裂, 以及 SIGPIPE 信号的接受者都是外部命令 /usr/bin/echo,


而不是左端的 shell, 这样一来, sleep 200 还要执行的。




因此, 假如 ls -lR / 需 30min 完成, 则下面各例运行时间是多长呢?




        (sleep 1; ls -lR /)|:                          


        (sleep 1; ls -lR /; sleep 200)|:    


        (sleep 1; ls -lR /; echo ; sleep 200)|:    




运用这种技术, 可以在复杂 shell 中精确设置 pipe 左端在遇到 pipe 破裂时的退出位置。






5. Pipe 的异常中断




如果某个pipe 在运行时有了 Ctrl-C 的异常中断怎么办? 一般 shell 都将 SIGINT 送到 pipe


中的每一个 cmd 中, 而 default 下, 接到 SIGINT 的 cmd 会退出。从而整个 pipe 结束。

 

原创粉丝点击