分解shell脚本; 引擎盖下会发生什么?

kst*_*tis 1 c unix bash shell exec

所以,我得到了这一行脚本:

echo test | cat | grep test
Run Code Online (Sandbox Code Playgroud)

您可以向我解释一下,如果给出以下系统调用,它究竟是如何工作的:pipe(),fork(),exec()和dup2()?

我在这里寻找一般概述,主要是操作顺序.到目前为止我所知道的是shell将使用fork()进行fork,并且脚本的代码将使用exec()替换shell的代码.但是管道和dup2怎么样?它们如何落实到位?

提前致谢.

use*_*342 6

首先考虑一个更简单的例子,例如:

echo test | cat
Run Code Online (Sandbox Code Playgroud)

我们想要的是echo在一个单独的过程中执行,安排其标准输出转移到执行过程的标准输入cat.理想情况下,这种转移,一旦设置,将不需要shell的进一步干预 - shell将平静地等待两个进程退出.

实现这一目标的机制称为"管道".它是在内核中实现并导出到用户空间的进程间通信设备.一旦由Unix程序创建,管道就会出现一对具有特殊属性的文件描述符,如果您写入其中一个,则可以从另一个读取相同的数据.这在同一个过程中并不是很有用,但请记住,文件描述符(包括但不限于管道)是跨越fork()甚至是跨越的exec().这使得管道易于设置并且相当有效的IPC机制.

shell创建管道,现在拥有一组属于管道的文件描述符,一个用于读取,一个用于写入.这两个文件描述符都由分叉子进程继承.现在只有echo在写入管道的写端描述符而不是写入其实际标准输出时,如果cat从管道的读取端描述符而不是从其标准输入读取,一切都会起作用.但他们没有,这就是dup2发挥作用的地方.

dup2将文件描述符复制为另一个文件描述符,预先自动关闭新描述符.例如,dup2(1, 15)将关闭文件描述符1(按照惯例用于标准输出),并将其重新打开为文件描述符15的副本 - 这意味着写入标准输出实际上等同于写入文件描述符15.适用于读取:dup2(0, 8)将从文件描述符0(标准输入)读取相当于从文件描述符8读取.如果我们继续关闭原始文件描述符,打开的文件(或管道)将从原始文件有效地移动新的描述符,就像科幻远程传送一样,首先在远程位置复制一块物质,然后分解原件.

如果您仍然遵循该理论,那么shell执行的操作顺序现在应该是明确的:

  1. shell创建一个管道,然后创建fork两个进程,这两个进程都将继承管道文件描述符,r以及w.

  2. 在要执行的子进程中echo,shell dup2(1, w); close(w)之前调用exec以便将标准输出重定向到管道的写入端.

  3. 在要执行的子进程中cat,shell调用dup2(0, r); close(r)以将标准输入重定向到管道的读取端.

  4. 分叉后,主壳过程必须自己关闭管道的两端.一个原因是一旦子进程退出就释放与管道相关的资源.另一种是允许cat实际终止 - 只有在管道写入端的所有副本都关闭后,管道读取器才会收到EOF .在上面的步骤中,我们确实关闭了孩子的写入结束的冗余副本,文件描述符15,在其复制到1之后.但是文件描述符15也必须存在于父级中,因为它是在该号码下继承的,并且可以只有父母关闭.如果不做到这一点,那么cat标准输入永远不会报告EOF,因此其cat过程悬而未决.

这种机制很容易推广到三个或更多通过管道连接的过程.在三个进程的情况下,管道需要安排echo输出写入cat输入,cat输出写入grep输入.这需要两次调用pipe(),三个调用fork(),四调用dup2()close(一个用于echogrep,两个用于cat),三次调用exec(),和到四个额外呼叫close()(两个用于每个管道).