打破shell脚本; 引擎盖下发生了什么?

所以,我给了这一行脚本:

echo test | cat | grep test 

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

我在这里寻找一个总的概述,主要是操作的顺序。 到目前为止我所知道的是,shell将使用fork()来分叉,脚本的代码将使用exec()来replaceshell的。 但是pipe和dup2呢? 他们如何到位?

提前致谢。

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

 echo test | cat 

我们想要的是在一个单独的进程中执行echo ,将其标准输出安排到执行cat的进程的标准输入中。 理想情况下,这种转移一旦建立,就不需要壳体的进一步干预 – 壳体将冷静地等待两个过程退出。

实现这一目标的机制被称为“管道”。 它是在内核中实现的进程间通信设备,并被导出到用户空间。 一旦由一个Unix程序创建,一个管道有一对文件描述符的外观,这个特殊的属性,如果你写入其中一个,你可以从另一个读取相同的数据。 这在同一个进程中并不是很有用,但请记住,文件描述符(包括但不限于管道fork()通过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的子dup2(1, w); close(w) ,shell调用dup2(1, w); close(w)exec之前dup2(1, w); close(w)以将标准输出重定向到管道的写入端。

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

  4. 分叉后,主壳程必须自己关闭管子的两端。 一个原因是一旦子流程退出,释放与管道相关的资源。 另一个是允许cat实际终止 – 只有在管道写入结束的所有副本都关闭后,管道读取器才会收到EOF。 在上面的步骤中,我们将写入结束的文件描述符15的副本复制到1上,但是文件描述符15也必须存在于父文件中,因为它是在该编号下继承的,并且可以只能由父母关闭。 如果没有这样做,叶cat的标准输入就不会报告EOF,其cat过程也会挂起。

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