管道有时不会导致立即输出

Piping sometimes does not lead to immediate output

我观察了几次 A | B | C 可能不会立即产生输出,尽管 A 一直在产生输出。我不知道这怎么可能。根据我的理解,所有三个进程应该同时工作,将它们的输出放入下一个管道(或标准输出)并在完成一个步骤时从前一个管道中获取。

这是我目前遇到的一个例子:

tcpflow -ec -i any port 8340 | tee second.flow | grep -i "\(</Manufacturer>\)\|\(</SerialNumber>\)" | awk -F'[<>]' '{print }'

应该发生什么:

我查看了一个用于 tcp 包的端口。如果有东西出现,它应该是某种 XML 格式,我想从这些包中获取制造商和序列号。我还想在文本文件 "second.flow" 中获得完整的、未修改的输出,以供以后参考。

会发生什么:

一切如愿,但不是每 10 秒输出一次(我确定我每十秒输出一次!)我必须等待很长时间,然后一次打印很多。这就像其中一种工具吞噬了缓冲区中的所有内容,并且仅在缓冲区已满时才打印它。我不想要那个。我想尽快获取每一行。

如果我将 tcpflow ... 替换为 cat second.flow,它会立即生效。有人可以描述发生了什么事吗?如果很明显,是否还有另一种方法可以达到相同的结果?

减少缓冲

管道中的每个应用程序都可以进行自己的缓冲。您可能想看看是否可以减少 tcpflow 中的缓冲,因为您的其他命令是面向行的,不太可能成为缓冲问题的根源。我没有在 tcpflow 中看到任何缓冲区控制的特定选项,尽管 max_bytes-b 标志在您想要使用的文本的情况下可能会有所帮助靠近流量的前端。

您也可以尝试使用 stdbuf from GNU coreutils 修改 tcpflow 的缓冲。这可能有助于减少管道中的延迟,但手册页提供了以下警告:

NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'. Also some filters (like 'dd' and 'cat' etc.) don't use streams for I/O, and are thus unaffected by 'stdbuf' settings.

例如,以下可能减少tcpflow的输出缓冲:

  • stdbuf --output=0 tcpflow -ec -i any port 8340 # unbuffered output
  • stdbuf --output=L tcpflow -ec -i any port 8340 # line-buffered output

除非上述警告之一适用。您的里程可能会有所不同。

一系列管道中的每一层都可以涉及缓冲;默认情况下,没有为 stdout 指定缓冲行为的工具将在输出到终端时使用行缓冲,并在输出到其他任何地方(包括管道到另一个程序或文件)时使用块缓冲。在链式管道中,除了最后一个阶段之外的所有阶段都将看到它们的输出不会进入终端,并且会阻塞缓冲区。

因此,在您的情况下,tcpflow 可能会不断产生输出,如果这样做,tee 应该几乎以相同的速率产生数据。但是 grep 会将流量限制为涓流,并且在该涓流超过输出缓冲区的大小之前不会产生输出。它已经执行了过滤并调用了 fwriteputsprintf,但是数据正在等待足够的字节在它后面建立起来,然后再将它发送到 awk,到减少(昂贵的)系统调用的次数。

cat second.flow 立即生成输出,因为一旦 cat 完成生成输出,它就会退出,刷新并关闭进程中的 stdout,当每个步骤找到其级联时stdin 到达 EOF,它退出、刷新并关闭其 stdouttcpflow 没有退出,所以 EOF 和刷新的级联没有发生。

对于某些程序,在一般情况下,您可以使用 stdbuf (or unbuffer 来更改缓冲行为,尽管这不能通过行缓冲来平衡效率,并且管道输入存在问题)。如果程序使用内部缓冲,这仍然可能不起作用,但值得一试。

不过,在您的特定情况下,因为很可能 grep 导致了中断(通过仅产生一小滴输出并滞留在缓冲区中,其中 tcpflowtee 正在生成一个 torrent,并且 awk 连接到 stdout,因此默认情况下行缓冲),您可以将命令行调整为:

tcpflow -ec -i any port 8340 | tee second.flow | grep -i --line-buffered "\(</Manufacturer>\)\|\(</SerialNumber>\)" | awk -F'[<>]' '{print }'

至少对于 Linux's grep(不确定 switch 是否是标准的),这使得 grep 将其自己的输出缓冲显式更改为面向行的缓冲,这应该会消除延迟。如果 tcpflow 本身没有产生足够的输出来定期刷新(你暗示它做到了,但你可能错了),你会在它上面使用 stdbuf (但不是 tee,根据 stdbuf 手册页注释,手动更改其缓冲,因此 stdbuf 不执行任何操作)使它们行缓冲:

stdbuf -oL tcpflow -ec -i any port 8340 | tee second.flow | grep -i --line-buffered "\(</Manufacturer>\)\|\(</SerialNumber>\)" | awk -F'[<>]' '{print }'

评论更新:看起来 awk 块缓冲区的某些风格打印到 stdout,即使连接到终端也是如此。 For mawk (the default on many Debian based distros), you can non-portably disable it by passing the -Winteractive switch at invocation. Alternatively, to work portably, you can just call system("") after each print, which portably forces output flushing on all implementations of awk。可悲的是,显而易见的 fflush() 不能移植到 awk 的旧实现,但如果您只关心现代 awk,只需使用 fflush() 就可以了,因为它很明显并且主要是可移植的。