好的,这是我无法解决的问题。在编写一个相当复杂的脚本时,我碰到了这个问题。设法将其简化到最低限度,但这仍然没有意义。
假设我有一个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的答案。要获得更深入和完整的见解,您需要了解rici的说明!
确实,如果我们消除了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...
),则将显示很多。
最后,让我们回到第一种情况,考虑一下两个进程循环时的可能性。在上面的描述中,我假设两个CLOSE操作都将在尝试执行任何OPEN操作之前成功执行。那是常见的情况,但没有任何保证。假设写者在读者设法完成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中,“双向重定向”运算符<>
仍然仅重定向stdin-或fd n形式n<>
-因此,上述含义并不意味着“将stdin和stdout重定向到fifo”。)
比赛条件极为罕见的事实并不是忽略它的理由。墨菲定律指出,它将在最关键的时刻发生。例如,当关键文件损坏前需要正确的功能以创建备份时。但是,为了触发竞争条件,编写者进程需要安排其动作在某些非常紧迫的时间段内发生:
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并通过关闭fifo做出响应之间的短暂间隔内执行OPEN。(这是写程序的OPEN不会被阻塞的唯一方法。)然后,它需要在读者关闭fifo的那一刻与随后的重新打开之间的(不同的)短暂间隔内进行写操作。(重新打开不会被阻止,因为现在作家已经打开了fifo。)
就像我说的那样,这就是亿万种竞争条件中的一种,仅在最不合时宜的时刻出现,可能是在编写代码后的数年。但这并不意味着您可以忽略它。确保编写器已准备好处理SIGPIPE,然后重试因EPIPE失败的写操作。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句