命名管道和未命名管道

时间:2016-01-22 17:06:46

标签: bash

好的,这是我无法解决的问题。我在处理一个相当复杂的脚本时遇到了这个问题。管理将这个简化到最低限度,但它仍然没有意义。

让我们说,我有一个fifo

mkfifo foo.fifo

在一个终端上运行以下命令,然后将内容写入另一个终端上的管道(echo "abc" > foo.fifo)似乎工作正常:

while true; do read LINE <foo.fifo; echo "LINE=$LINE"; done
LINE=abc

但是,稍微更改命令,并且read命令在读取第一行后无法等待下一行:

cat a.fifo | while true; do read LINE; echo "LINE=$LINE"; done
LINE=abc
LINE=
LINE=
LINE=
[...] # At this keeps repeating endlessly

真正令人不安的是,它会等待第一行,但它只是将一个空字符串读入$LINE,并且无法阻止。 (有趣的是,这是少数几次之一,我想要阻止I / O操作:))

我想,我真的明白I / O重定向和这样的事情是如何工作的,但现在我很困惑。

那么,解决方案是什么,我错过了什么?任何人都能解释这种现象吗?

更新:要获得简短的答案和快速解决方案,请参阅William's answer。要获得更深入,更完整的见解,您需要使用rici's explanation

2 个答案:

答案 0 :(得分:3)

真的,问题中的两个命令行非常相似,如果我们删除UUOC

while true; do read LINE <foo.fifo; echo "LINE=$LINE"; done

while true; do read LINE; echo "LINE=$LINE"; done <foo.fifo

他们的行为略有不同,但重要的是他们都不正确

第一个打开并从fifo读取,然后每次通过循环关闭fifo。第二个打开fifo,然后每次通过循环尝试从中读取它。

fifo是一个稍微复杂的状态机,了解各种转换非常重要。

打开一个用于读取或写入的fifo将阻止,直到某个进程在另一个方向打开。这使得独立启动读者和作者成为可能; open来电将同时返回。

如果fifo缓冲区中有数据,则从fifo读取成功。如果fifo缓冲区中没有数据但是至少有一个写入器保持fifo打开,它会阻塞。如果fifo缓冲区中没有数据且没有编写器,则返回EOF。

如果fifo缓冲区中有空格并且至少有一个读取器打开了fifo,则对fifo的写入成功。如果fifo缓冲区中没有空格,它会阻塞,但至少有一个读取器打开fifo。如果没有读卡器,它会触发SIGPIPE(如果忽略该信号,则会失败并显示EPIPE)。

一旦fifo的两端都关闭,fifo缓冲区中剩余的任何数据都将被丢弃。

现在,基于此,让我们考虑第一个场景,其中fifo被重定向到read。我们有两个过程:

   reader                 writer
   --------------              --------------
1. OPEN blocks
2. OPEN succeeds          OPEN succeeds immediately
3. READ blocks
4.                        WRITE
5. READ succeeds     
6. CLOSE    /////////     CLOSE 

(作者同样可以先启动,在这种情况下,它会阻塞第1行而不是读取器。但结果是相同的。第6行的CLOSE操作不同步。见下文。)

在第6行,fifo不再拥有读者或编写者,因此其缓冲区被刷新。因此,如果作者写了两行而不是一行,那么在循环继续之前,第二行将被扔进比特桶。

让我们与第二种情况形成鲜明对比,第二种情况是读者是while循环,而不仅仅是阅读:

   reader                 writer
   ---------              ---------
 1. OPEN blocks

 2. OPEN succeeds          OPEN succeeds immediately
 3. READ blocks
 4.                        WRITE
 5. READ succeeds     
 6.                        CLOSE 
    --loop--
 7. READ returns EOF
 8. READ returns EOF
... and again   
42. and again              OPEN succeeds immediately
43. and again              WRITE
44. READ succeeds

在这里,读者将继续读取行,直到它用完为止。如果到那时没有作家出现,读者将开始获得EOF。如果它忽略它们(例如while true; do read...),那么它将获得很多它们,如图所示。

最后,让我们回到第一个场景,并考虑两个进程循环时的可能性。在上面的描述中,我假设在尝试OPEN操作之前两个CLOSE操作都会成功。这将是常见的情况,但没有任何保证。相反,假设编写者在读者设法完成CLOSE之前成功完成了CLOSE和OPEN。现在我们有了序列:

   reader                 writer
   --------------         --------------
1. OPEN blocks
2. OPEN succeeds          OPEN succeeds immediately
3. READ blocks
4.                        WRITE
5.                        CLOSE
5. READ succeeds          OPEN 
6. CLOSE                  
7.                        WRITE   !! SIGPIPE !! 

简而言之,第一次调用将跳过行,并且具有竞争条件,其中编写器偶尔会收到虚假错误。第二次调用将读取所写的所有内容,并且编写器将是安全的,但读者将持续接收EOF指示而不是阻塞,直到数据可用。

那么正确的解决方案是什么?

除竞争条件外,读者的最佳策略是阅读直到EOF,然后关闭并重新打开fifo。如果没有作家,第二次打开会阻止。这可以通过嵌套循环来实现:

while :; do
  while read line; do
    echo "LINE=$line"
  done < fifo
done

不幸的是,生成SIGPIPE的竞争条件仍有可能,尽管它将极为罕见[见注1]。同样,作家必须为其写入失败做好准备。

Linux上提供了一种更简单,更强大的解决方案,因为Linux允许打开fifos进行读写。这种开放总是立即成功。并且由于始终存在一个将fifo保持打开以进行写入的过程,因此读取将按预期阻塞:

while read line; do
  echo "LINE=$line"
done <> fifo

(请注意,在bash中,&#34;重定向两种方式&#34;运算符<>仍然只重定向stdin - 或fd n 形式n<> - 所以上面的意味着&#34;将stdin和stdout重定向到fifo&#34;。)

注释

  1. 竞争条件极为罕见的事实不是忽视它的理由。墨菲定律指出它将在最关键的时刻发生;例如,当需要正确运行以便在关键文件损坏之前创建备份时。但是为了触发竞争条件,作者流程需要安排其行动在一些非常紧张的时间段内发生:

       reader                 writer
       --------------         --------------
       fifo is open           fifo is open
    1. READ blocks
    2.                        CLOSE
    3. READ returns EOF
    4.                        OPEN
    5. CLOSE
    6.                        WRITE   !! SIGPIPE !! 
    7. OPEN
    

    换句话说,作者需要在读者收到EOF之间的短暂时间间隔内执行OPEN,并通过关闭fifo来响应。 (这是作家的OPEN不会阻止的唯一方式。)然后它需要在读者关闭fifo的那一刻(不同的)短暂间隔内进行写入,随后重新开放。 (重新开放不会阻止,因为现在作者已经打开了fifo。)

    那个曾经在亿亿竞争条件中的人之一,正如我所说的那样,只是在代码编写完成后的最不合时宜的时刻弹出。 但这并不意味着你可以忽略它。确保编写者准备处理SIGPIPE并重试使用EPIPE失败的写入。

答案 1 :(得分:2)

当你这样做时

cat a.fifo | while true; do read LINE; echo "LINE=$LINE"; done
顺便说一下,应该写一下:

while true; do read LINE; echo "LINE=$LINE"; done < a.fifo

该脚本将阻止,直到有人打开fifo进行写入。一旦发生这种情况,while循环就会开始。如果作者(&#39; echo foo&gt; a.fifo&#39;你在另一个shell中运行)终止并且没有其他人打开管道进行写入,则读取返回,因为管道是空的并且那里没有另一端打开的进程。试试这个:

在一个shell中:

while true; do date; read LINE; echo "LINE=$LINE"; done < a.fifo

在第二个shell中:

cat > a.fifo

在第三个shell中

echo hello > a.fifo
echo world > a.fifo

通过让cat在第二个shell中运行,while循环中的read阻止而不是返回。

我想关键的见解是当你在循环中进行重定向时,shell才会启动读取,直到有人打开管道进行写入。当你重定向到while循环时,shell只会在它开始循环之前阻塞。