【问题标题】:Multiple readers from FIFO来自 FIFO 的多个读取器
【发布时间】:2015-10-21 22:14:22
【问题描述】:

是否可以在多个阅读器之间拆分 STDIN,从而有效地成为一个作业队列?我想将每一行传递给一个读者。命名管道几乎可以工作,但同时读取会干扰:

reader.sh

#!/usr/bin/env bash
while read line
do
  echo $line
done <  fifo

writer.sh

#!/usr/bin/env bash
while true
do
  echo "This is a test sentance"
  sleep 1
done

执行:

mkfifo fifo
./reader.sh &
./reader.sh &
./writer.sh > fifo

偶尔的输出(特别是如果读取器和写入器在不同的窗口中)

This is atetsnac
Ti sats etnesats etne etsnac
isats etnes etsnac
Tisi etsnac
hi etsnac
Ti sats etn
hsi etsnac

注意事项:

  • 我知道有更好的方法,只是好奇这是否可行
  • 我认为这不是错误,因为我已经测试了 Linux 和 OSX 机器
  • 我希望每行有一个消费者,不包括 tee
  • 我想使用 STDIN,它排除了 xargs
  • GNU coreutils split 可以分配循环,但不是首先可用
  • GNU parallel --pipe 一直等到 STDIN 关闭;我想尽快分配

【问题讨论】:

  • 我在这里复制了这个(OS X),但我不明白。
  • 我想我明白了。虽然对管道的写入是原子的(只要它们小于BUFSIZ),但消息边界不会保存在管道中。因此并发读者可以分别读取输入的不同部分。从流中读取的系统调用不提供任何方法来请求将整行作为一个单元。
  • 我认为管道只是错误的机制。消息队列或数据报套接字会更好,尽管它们在bash 中没有很好的接口。 Yuo 可能需要用 Perl、PHP 或 Python 编写脚本。
  • 您可以将 GNU Parallel 调用为 sem --id mymutex 作为 read 命令的前缀,以便它们一次执行一个。

标签: bash stdin fifo


【解决方案1】:

不,一般来说不可能稳健地。写入小于 PIPE_BUF(在所有 POSIX 系统上 >=512 字节)的命名管道是原子的。问题是读取不是原子的,并且没有标准(或非标准的 AFAIK)方法可以使它们成为原子的。如果有 1 个或更多字节可用,则在阻塞读取管道时,将立即读取它们,并将实际读取的数字作为返回值返回。

Rochkind, Advance UNIX Programming 状态:

因为不能保证原子性,所以你绝不能允许多个读取器,除非你有另一个并发控制机制......改用消息队列之类的东西。

说了这么多,为了好玩,实现令人惊讶的稳健行为是可能的。基于行的cat | while read line do; .. 方法似乎 起作用的原因是因为cat 会在行到达后立即从管道中抢走行,并且一旦开始写入,读者就准备好阅读,因为你提到的。因为它是直接读取的,所以它碰巧在它们被写入的行边界处抢占了行(复数)。一般来说,虽然基于行的方法不会非常健壮,因为消息边界是不可预测的。

如果您以恒定大小的块 &lt;=PIPE_BUF 编写和阅读,您会做得更好。您保证永远不会读取超出您要求的内容,并且只要您写入恒定大小的块,每次写入的大小小于PIPE_BUF,就没有理由应该小于字节块的倍数可用读取,然而,不能保证所有可用字节都会被实际读取;无论read 实际可用的字节数是多少,底层读取系统调用返回的字节数少于您请求的字节数并不是错误:

成功时,返回读取的字节数(零表示文件结束),文件位置提前该数字。如果此数字小于请求的字节数,则不是错误;这可能会发生,例如因为现在实际可用的字节较少(可能是因为我们接近文件结尾,或者因为我们正在从管道或终端读取),或者因为 read() 被中断信号。

并且可能还有其他特殊原因 - 如果标准没有明确说明它是有保证的以及保证它的条件,请不要假设它是。

--

reader.sh:

#!/bin/bash
while read -N 21 packet
do
  echo [$$] $packet
done<fifo

writer.sh

#!/bin/bash
for((i=0; i<100; i++))
do
  s=`printf "%020d" $i`
  echo $s
  echo "wrote $s" >&2
done

执行:

mkfifo fifo
./reader.sh &
./reader.sh &
./writer.sh > fifo

【讨论】:

  • 我建议改写答案。第一句话表明没有简单的解决方案,最后提供了可行的解决方案。我跳过了答案,自己找到了固定大小记录的解决方案。
  • 我改写了,但答案仍然是你不能完全健壮地做到这一点,所以......使用消息队列或类似的。该解决方案似乎在大多数情况下都有效,但在某些极端情况下可能无效。
  • 这取决于你 ;-) 但我会写成“是的,一般来说这是可能的,但没有可靠的解决方案。”
【解决方案2】:

您可以将阅读器更改为

#!/usr/bin/env bash
cat fifo | while read line
do
  echo $line
done

这样,它将读取整行或什么都不读取。

另一个版本的问题是从fifo读取的责任是内置read,它使用1个字符的缓冲区进行读取,所以同一行的不同字符可以被两个读取如果它们同时运行,则处理。你可以通过strace看到它:

strace bash -c 'while read line; do echo $line; done < fifo'`

cat 使用更大的缓冲区来读取,因此它最终会接收到整行。测试它:

strace cat fifo | while read line; do echo $line; done

但是,我不建议将其用作作业队列,因为它似乎无法在读取器之间均匀地分配读取。

【讨论】:

  • cat 也将读取 STDIN 中的所有内容,因此如果存在,将清空整个多行队列。所有单行命令(head、sed、awk)在读取后退出,关闭管道。是否有一个命令可以一次连续读取整行(如果只有 head 有一个 -f --follow 选项,比如 tail)?
  • 如果缓冲区中存在更多行,则不可能读取一行,因为原始read() 操作通过读取给定数量的字节而不是通过阅读直到找到某种字符。只读取一行的程序通过一次读取一个字符来完成(正如我在内置 int read 的答案中解释的那样),因此它们会与其他程序交错,从而导致您试图解决的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-01
  • 2011-01-29
  • 1970-01-01
  • 2015-12-01
  • 1970-01-01
  • 2020-10-21
相关资源
最近更新 更多