所以,我给了这一行脚本:
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的操作顺序应该是明确的:
shell创建一个管道,然后fork
两个进程,这两个进程都将继承管道文件描述符r
和w
。
在要执行echo
的子dup2(1, w); close(w)
,shell调用dup2(1, w); close(w)
在exec
之前dup2(1, w); close(w)
以将标准输出重定向到管道的写入端。
在要执行cat
的子dup2(0, r); close(r)
,shell调用dup2(0, r); close(r)
dup2(0, r); close(r)
以将标准输入重定向到管道的读取端。
分叉后,主壳程必须自己关闭管子的两端。 一个原因是一旦子流程退出,释放与管道相关的资源。 另一个是允许cat
实际终止 – 只有在管道写入结束的所有副本都关闭后,管道读取器才会收到EOF。 在上面的步骤中,我们将写入结束的文件描述符15的副本复制到1上,但是文件描述符15也必须存在于父文件中,因为它是在该编号下继承的,并且可以只能由父母关闭。 如果没有这样做,叶cat
的标准输入就不会报告EOF,其cat
过程也会挂起。
这个机制很容易推广到三个或更多个由管道连接的过程。 在三个进程的情况下,管道需要安排echo
的输出写入cat
的输入, cat
的输出写入grep
的输入。 这需要两次调用pipe()
,三次调用fork()
,四次调用dup2()
和close
(一次echo
和grep
,两次for cat
),三次调用exec()
,另外四次调用close()
(每个管道两个)。