命名管道简介

作者:Andy Vaught

管道是使 Linux 和其他 Unix 系统有用的基本功能之一。“管道”允许独立的进程进行通信,而无需显式地设计为协同工作。这使得功能相当狭窄的工具能够以复杂的方式组合在一起。

使用管道的一个简单例子是命令

ls | grep x

当 bash 检查命令行时,它会找到分隔两个命令的竖线字符 |。Bash 和其他 shell 运行这两个命令,并将第一个命令的输出连接到第二个命令的输入。ls 程序生成当前目录中的文件列表,而 grep 程序读取 ls 的输出,并且仅打印包含字母 x 的行。

以上示例对于大多数 Unix 用户来说都很熟悉,它是“未命名管道”的一个例子。管道仅存在于内核内部,并且无法被创建它的进程访问,在本例中是 bash shell。对于那些尚不了解的人来说,父进程是由程序启动的第一个进程,该程序反过来创建执行该程序的单独的子进程。

另一种管道是“命名”管道,有时也称为 FIFO。FIFO 代表“先进先出”,指的是字节进入的顺序与出来的顺序相同的属性。命名管道的“名称”实际上是文件系统中的文件名。管道在 ls 中显示为任何其他文件,但有一些不同之处

% ls -l fifo1
prw-r--r--   1 andy  users    0 Jan 22 23:11 fifo1|

最左列的 p 表示 fifo1 是一个管道。其余的权限位控制谁可以像普通文件一样读取或写入管道。在具有现代 ls 的系统上,文件名末尾的 | 字符是另一个线索,并且在启用颜色选项的 Linux 系统上,默认情况下 fifo| 以红色打印。

在较旧的 Linux 系统上,命名管道由 mknod 程序创建,通常位于 /etc 目录中。在更现代的系统上,mkfifo 是一个标准实用程序。mkfifo 程序将一个或多个文件名作为此任务的参数,并创建具有这些名称的管道。例如,要创建名为 pipe1 的命名管道,请给出命令

mkfifo pipe

展示命名管道如何工作的最简单方法是使用一个示例。假设我们如上所示创建了 pipe。在一个虚拟控制台 1 中,键入

ls -l > pipe1
在另一个虚拟控制台中键入
cat < pipe
瞧!第一个控制台上运行的命令的输出出现在第二个控制台上。请注意,您运行命令的顺序无关紧要。

如果您以前没有使用过虚拟控制台,请参阅 John M. Fisk 在 1996 年 11 月的 Linux Journal 中发表的文章“键盘、控制台和 VT 巡航”。

如果您仔细观察,您会注意到您运行的第一个命令似乎挂起了。发生这种情况是因为管道的另一端尚未连接,因此内核会暂停第一个进程,直到第二个进程打开管道。在 Unix 术语中,该进程被称为“阻塞”,因为它正在等待某些事情发生。

命名管道的一个非常有用的应用是允许完全不相关的程序相互通信。例如,一个服务某种请求(打印文件、访问数据库)的程序可以打开管道进行读取。然后,另一个进程可以通过打开管道并写入命令来发出请求。也就是说,“服务器”可以代表“客户端”执行任务。如果客户端没有写入,或者服务器没有读取,也可能发生阻塞。

管道狂热

创建两个命名管道,pipe1 和 pipe2。运行命令

echo -n x | cat - pipe1 > pipe2 &
cat <pipe2 > pipe1

在屏幕上,看起来好像什么都没发生,但是如果您运行 top(一个类似于 ps 的命令,用于显示进程状态),您会看到两个 cat 程序都在疯狂地运行,在一个无限循环中来回复制字母 x

在您按下 ctrl-C 以退出循环后,您可能会收到消息“broken pipe”(管道破裂)。当进程写入管道时,读取管道的进程关闭其末端时,会发生此错误。由于读取器已消失,因此数据无处可去。通常,写入器将完成写入其数据并关闭管道。此时,读取器会看到 EOF(文件结束符)并执行请求。

是否发出“broken pipe”消息取决于按下 ctrl-C 的瞬间发生的事件。如果第二个 cat 刚刚读取了 x,则按下 ctrl-C 会停止第二个 catpipe1 关闭,并且第一个 cat 静默停止,即,没有消息。另一方面,如果第二个 cat 正在等待第一个 cat 写入 x,则 ctrl-C 会导致 pipe2 在第一个 cat 可以写入之前关闭,并且会发出错误消息。这种随机行为被称为“竞争条件”。

命令替换

Bash 以一种非常巧妙的方式使用命名管道。回想一下,当您将命令括在括号中时,该命令实际上是在“子 shell”中运行的;也就是说,shell 克隆自身,并且克隆解释括号内的命令。由于外部 shell 仅运行单个“命令”,因此可以将一整套命令的输出作为一个单元重定向。例如,命令

(ls -l; ls -l) >ls.out

将当前目录列表的两个副本写入文件 ls.out。

当您在左括号前面放置 <> 时,会发生命令替换。例如,键入命令

cat <(ls -l)

导致命令 ls -l 像往常一样在子 shell 中执行,但是将输出重定向到一个临时的命名管道,该管道由 bash 创建、命名并在稍后删除。因此,cat 有一个有效的文件名可以从中读取,并且我们看到了 ls -l 的输出,比通常的做法多了一步。类似地,给出 >(commands) 会导致 Bash 命名一个临时管道,括号内的命令从中读取输入。

如果您想查看两个目录是否包含相同的文件名,请运行单个命令

cmp <(ls /dir1) <(ls /dir2)

比较程序 cmp 将看到两个文件的名称,它将读取并比较它们。

命令替换还使 tee 命令(用于查看和保存命令的输出)更加有用,因为您可以使单个输入流被多个读取器读取,而无需借助临时文件——bash 为您完成所有工作。命令

ls | tee >(grep foo | wc >foo.count) \
         >(grep bar | wc >bar.count) \
         | grep baz | wc >baz.count

计算 ls 输出中 foobarbaz 的出现次数,并将此信息写入三个单独的文件。命令替换甚至可以嵌套

cat <(cat <(cat <(ls -l))))
作为一种非常迂回的方式来列出当前目录。

如您所见,虽然未命名管道允许将简单命令串在一起,但命名管道,在 bash 的帮助下,允许创建整个管道树。可能性仅受您的想象力限制。

Introduction to Named Pipes
Andy Vaught 目前是亚利桑那州立大学计算物理学博士候选人,并且自 1.1 版本以来一直在运行 Linux。他喜欢与民用航空巡逻队一起飞行以及滑雪。可以通过 andy@maxwell.la.asu.edu 联系到他。
加载 Disqus 评论