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

时间:2013-11-22 23:06:46

标签: c bash shell unix exec

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

echo test | cat | grep test

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

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

提前致谢。

1 个答案:

答案 0 :(得分:6)

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

echo test | cat

我们想要的是在一个单独的进程中执行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两个进程,这两个进程都将继承管道文件描述符rw

  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进程会因此而停止。

  5. 这种机制很容易推广到三个或更多通过管道连接的进程。如果有三个进程,管道需要安排echo的输出写入cat的输入,cat的输出写入grep的输入。这需要两次拨打pipe(),三次拨打fork(),四次拨打dup2()close(一次拨打echogrep和两次对于cat),三次调用exec(),另外四次调用close()(每个管道两次)。