好的,这是我无法解决的问题。我在处理一个相当复杂的脚本时遇到了这个问题。管理将这个简化到最低限度,但它仍然没有意义。
让我们说,我有一个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!
答案 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;。)
竞争条件极为罕见的事实不是忽视它的理由。墨菲定律指出它将在最关键的时刻发生;例如,当需要正确运行以便在关键文件损坏之前创建备份时。但是为了触发竞争条件,作者流程需要安排其行动在一些非常紧张的时间段内发生:
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只会在它开始循环之前阻塞。