所以,我得到了这一行剧本:
echo test | cat | grep test
请您解释一下,如果给出以下系统调用,究竟是如何工作的:pipe(),fork(),exec()和dup2()?
我在这里寻找概述,主要是操作顺序。 到目前为止我所知道的是shell将使用fork()进行fork,并且脚本的代码将使用exec()替换shell的代码。但是管道和dup2怎么样?他们是如何落实到位的?
提前致谢。
答案 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执行的操作顺序:
shell创建一个管道然后fork
两个进程,这两个进程都将继承管道文件描述符r
和w
。
在要执行echo
的子流程中,shell在dup2(1, w); close(w)
之前调用exec
,以便将标准输出重定向到管道的写端。
在即将执行cat
的子流程中,shell调用dup2(0, r); close(r)
以将标准输入重定向到管道的读取端。
分叉后,主壳过程必须自己关闭管道的两端。一个原因是一旦子进程退出就释放与管道相关的资源。另一种是允许cat
实际终止 - 只有在管道写入端的所有副本关闭后,管道读取器才会收到EOF。在上面的步骤中,我们确实关闭了孩子的写入结束的冗余副本,文件描述符15,在其复制到1之后。但是文件描述符15也必须存在于父级中,因为它是在该号码下继承的,并且可以只有父母关闭。如果不这样做会使cat
的标准输入永远不会报告EOF,并且其cat
进程会因此而停止。
这种机制很容易推广到三个或更多通过管道连接的进程。如果有三个进程,管道需要安排echo
的输出写入cat
的输入,cat
的输出写入grep
的输入。这需要两次拨打pipe()
,三次拨打fork()
,四次拨打dup2()
和close
(一次拨打echo
和grep
和两次对于cat
),三次调用exec()
,另外四次调用close()
(每个管道两次)。