是什么阻止了 stdout/stderr 交错?

是什么阻止了 stdout/stderr 交错?

假设我运行一些进程:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

我像这样运行上面的脚本:

foobarbaz | cat

据我所知,当任何进程写入 stdout/stderr 时,它们的输出永远不会交错 - stdio 的每一行似乎都是原子的。这是如何运作的?什么实用程序控制每行的原子性?

答案1

他们确实交错!您只尝试了短的输出突发,它们保持不分裂,但实际上很难保证任何特定的输出保持不分裂。

输出缓冲

这取决于程序如何缓冲他们的输出。这标准输入输出库大多数程序在写入时使用缓冲区来提高输出效率。该函数不会在程序调用库函数写入文件时立即输出数据,而是将这些数据存储在缓冲区中,只有在缓冲区填满后才实际输出数据。这意味着输出是批量完成的。更准确地说,有三种输出模式:

  • 无缓冲:数据立即写入,不使用缓冲区。如果程序将其输出分成小块(例如逐个字符)写入,则速度可能会很慢。这是标准错误的默认模式。
  • 全缓冲:仅当缓冲区满时才写入数据。这是写入管道或常规文件时的默认模式(stderr 除外)。
  • 行缓冲:​​在每个换行符之后或缓冲区已满时写入数据。这是写入终端时的默认模式(stderr 除外)。

程序可以重新编程每个文件以使其表现不同,并且可以显式刷新缓冲区。当程序关闭文件或正常退出时,缓冲区会自动刷新。

如果写入同一管道的所有程序都使用行缓冲模式,或者使用无缓冲模式并通过对输出函数的一次调用来写入每一行,并且如果行足够短以写入单个块,则输出将是整行的交错。但如果其中一个程序使用全缓冲模式,或者行太长,那么您将看到混合行。

这是一个示例,其中我交错了两个程序的输出。我在 Linux 上使用 GNU coreutils;这些实用程序的不同版本可能表现不同。

  • yes aaaaaaaa以本质上等同于行缓冲模式的方式永远写入。该yes实用程序实际上一次写入多行,但每次发出输出时,输出都是整数行。
  • while true; do echo bbbb; done | grep bbbbb在全缓冲模式下永远写入。它使用的缓冲区大小为 8192,每行长度为 5 个字节。由于5不能整除8192,因此写入之间的边界通常不在行边界处。

让我们把它们放在一起。

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

正如您所看到的,是的,有时会中断 grep,反之亦然。只有大约 0.001% 的线路被中断,但它确实发生了。输出是随机的,因此中断的数量会有所不同,但我每次都至少看到一些中断。如果行较长,则中断行的比例会更高,因为随着每个缓冲区的行数减少,中断的可能性也会增加。

有几种方法可以调整输出缓冲。主要有:

  • 关闭使用 stdio 库的程序中的缓冲,而不更改程序的默认设置stdbuf -o0在 GNU coreutils 和其他一些系统(例如 FreeBSD)中找到。您也可以使用 切换到行缓冲stdbuf -oL
  • 通过为此目的创建的终端引导程序的输出来切换到行缓冲unbuffer。某些程序可能在其他方面表现不同,例如,grep如果其输出是终端,则默认使用颜色。
  • 配置程序,例如传递--line-buffered给 GNU grep。

让我们再次看一下上面的代码片段,这次两边都有行缓冲。

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

所以这次 yes 从未打断 grep,但 grep 有时会打断 yes。稍后我会解释为什么。

管道交错

只要每个程序一次输出一行,并且行足够短,输出行就会整齐地分开。但要实现这一点,排队的时间是有限的。管道本身有一个传输缓冲区。当程序输出到管道时,数据从写入程序复制到管道的传输缓冲区,然后从管道的传输缓冲区复制到读取程序。 (至少从概念上来说——内核有时可能会将其优化为单个副本。)

如果要复制的数据多于管道传输缓冲区的容量,则内核一次复制一个缓冲区。如果多个程序正在写入同一个管道,并且内核选择的第一个程序想要写入多个缓冲区,则不能保证内核第二次会再次选择相同的程序。例如,如果是缓冲区大小,foo想写2*字节并且bar想要写入 3 个字节,那么一种可能的交错是来自 的字节foo,然后来自 的 3 个字节bar,以及来自 的字节foo

回到上面的 yes+grep 示例,在我的系统上,yes aaaa碰巧一次性写入了 8192 字节缓冲区中可以容纳的行数。由于要写入 5 个字节(4 个可打印字符和换行符),这意味着每次写入 8190 个字节。管道缓冲区大小为 4096 字节。因此,可以从 yes 获取 4096 字节,然后从 grep 获取一些输出,然后从 yes 获取其余写入内容(8190 - 4096 = 4094 字节)。 4096 字节为 819 行和aaaa一个单独的a.因此,一行带有这个 lone ,a后面跟着一个来自 grep 的写入,给出了一行带有abbbb.

如果您想查看正在发生的详细信息,getconf PIPE_BUF .则会告诉您系统上的管道缓冲区大小,并且您可以使用以下命令查看每个程序进行的系统调用的完整列表

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

如何保证干净的线路交错

如果行长度小于管道缓冲区大小,则行缓冲可保证输出中不会出现任何混合行。

如果行长度可以更大,则当多个程序写入同一管道时,无法避免任意混合。为了确保分离,您需要让每个程序写入不同的管道,并使用一个程序来组合这些行。例如GNU 并行默认这样做。

答案2

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P对此进行了研究:

GNU xargs 支持并行运行多个作业。 -P n 其中 n 是并行运行的作业数。

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

这在许多情况下都可以正常工作,但有一个欺骗性的缺陷:如果 $a 包含超过 1000 个字符,则回显可能不是原子的(它可能会分成多个 write() 调用),并且存在两行会混合的。

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

显然,如果多次调用 echo 或 printf 也会出现同样的问题:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

并行作业的输出混合在一起,因为每个作业由两个(或多个)单独的 write() 调用组成。

如果您需要未混合的输出,因此建议使用保证输出被序列化的工具(例如 GNU Parallel)。

相关内容