FIFO(命名管道)与常规管道 (|) 有何不同?据我了解维基百科与常规管道不同,先进先出管道在进程结束后“继续存在”,并且可以在之后的某个时间删除。
但是,如果进程基于包含管道 ( cat x | grep y
) 的 shell 命令,如果我们将其存储在变量或文件中,我们就可以“在进程之后保持其活动状态”,这不是 FIFO 吗?
此外,普通管道还具有它得到的第一个标准输出,作为另一个命令的标准输入,那么它不也是一种先进先出管道吗?
答案1
“命名管道”实际上是一个非常准确的名称——它就像一个普通的管道,只是它有一个名称(在文件系统上)。
管道——使用的常规的、未命名的(“匿名”)管道some-command | grep pattern
是一种特殊的文件。我指的是文件,您可以像对其他文件一样读取和写入它。 Grep 并不真正关心 1 它是从管道而不是终端 3 或普通文件中读取数据。
从技术上讲,幕后发生的事情是 stdin、stdout 和 stderr 是传递给每个命令运行的三个打开文件(文件描述符)。文件描述符(在每个系统调用中用于读/写等文件)只是数字; stdin、stdout 和 stderr 是文件描述符 0、1 和 2。因此,当您的 shell 设置some-command | grep
它时,它会执行以下操作:
向内核请求匿名管道。没有名称,因此不能像普通文件那样使用它来完成- 而是使用or
open
完成,它返回两个文件描述符。⁴pipe
pipe2
分叉一个子进程(
fork()
创建父进程的副本;管道的两侧都在此处打开),将管道的写入端复制到 fd 1 (stdout)。内核有一个系统调用来复制文件描述符编号;是dup2()
或dup3()
。然后它关闭读取端和写入端的其他副本。最后,它用来execve
执行some-command
.由于管道是 fd 1,stdoutsome-command
就是管道。另一个子进程的分叉。这次,它将管道的读取端复制到 fd 0 (stdin),并执行
grep
.因此 grep 将从管道中读取为标准输入。然后等待这两个孩子退出。
此时,内核注意到管道不再打开,并且垃圾收集它。这才是真正破坏管道的原因。
命名管道只是通过将其放入文件系统中来为匿名管道命名。所以现在任何进程在将来的任何时候都可以通过使用普通的系统调用来获取管道的文件描述符open
。从概念上讲,直到所有读取器/写入器都关闭管道并将其unlink
从文件系统中删除后,管道才会被销毁。²
顺便说一句,这就是文件在 Unix 上的一般工作方式。unlink
(后面的系统调用rm
)只是删除文件的名称之一;只有当所有名称都被删除并且没有任何文件打开时,它才会真正被删除。这里有几个答案对此进行了探讨:
脚注
- 从技术上讲,这可能不是真的——通过了解可能可以进行一些优化,并且实际的 grep 实现通常已经过大量优化。但从概念上讲,它并不关心(实际上 grep 的直接实现也不会关心)。
- 当然,内核实际上并不会永远将所有数据结构保留在内存中,而是每当第一个程序打开命名管道时(然后在其打开期间将它们保留下来),它就会透明地重新创建它们。因此,就好像它们的存在时间一样长。
- 终端不是 grep 读取的常用位置,但当您不指定另一个时,它是默认的标准输入。因此,如果您只是
grep pattern
在 shell 中输入,grep
将从终端读取。我想到的唯一用途是如果您要将某些内容粘贴到终端。 - 在 Linux 上,匿名管道实际上是在特殊的文件系统(pipefs)上创建的。看Linux 中管道的工作原理了解详情。请注意,这是 Linux 的内部实现细节。
答案2
我认为您混淆了管道的 shell 语法与底层 Unix 系统编程。管道 / FIFO 是一种不存储在磁盘上的文件类型,而是通过内核中的缓冲区将数据从写入器传递到读取器。
无论写入器和读取器是通过进行系统调用(如 、 )open("/path/to/named_pipe", O_WRONLY);
或使用pipe(2)
创建一个新的匿名管道并将打开的文件描述符返回到读写端。
fstat(2)
管道文件描述符上会给你sb.st_mode & S_IFMT == S_IFIFO
任何一种方式。
当你跑步时foo | bar
:
- 对于任何非内置命令,shell 像平常一样分叉
- 然后进行
pipe(2)
系统调用以获取两个文件描述符:匿名管道的输入和输出。 - 然后它分叉再次。
- 孩子(其中
fork()
返回0)- 关闭管道的读取端(保持写入 fd 打开)
- 并重定向
stdout
到 write fddup2(pipefd[1], 1)
- 那么
execve("/usr/bin/foo", ...)
- 家长(其中
fork()
返回非0子PID)- 关闭管道的写入端(使读取 fd 保持打开状态)
stdin
并从读取的 fd重定向dup2(pipefd[0], 0)
- 那么
execve("/usr/bin/bar", ...)
如果你跑步,你会遇到非常相似的情况foo > named_pipe & bar < named_pipe
。
命名管道是进程之间建立管道的集合点。
这种情况类似于匿名 tmp 文件与有名称的文件。您可以open("/path/to/dir", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR);
创建一个临时文件没有名字( O_TMPFILE
),就好像您打开了"/path/to/dir/tmpfile"
它O_CREAT
,然后取消了它的链接,留下了一个已删除文件的文件描述符。
使用linkat
,您甚至可以将该匿名文件链接到文件系统,并为其命名(如果它是使用O_TMPFILE
. (不过,您无法在 Linux 上对使用某个名称创建然后删除的文件执行此操作。)