是什么阻止stdout / stderr交错?


13

假设我运行一些流程:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

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

foobarbaz | cat

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


3
您的命令输出多少数据?尝试使它们输出几千字节。
库萨兰达

您的意思是其中一个命令在换行符之前输出几kb的位置?
亚历山大·米尔斯

Answers:


22

他们交织!您仅尝试了短的输出突发,这些突发仍未拆分,但是实际上很难保证任何特定的输出都保持拆分。

输出缓冲

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

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

程序可以将每个文件重新编程为不同的行为,并且可以显式刷新缓冲区。当程序关闭文件或正常退出时,缓冲区将自动刷新。

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

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

  • yes aaaaaaaa以本质上等同于行缓冲模式的方式永久写入。该yes实用程序实际上一次写入多行,但是每次发出输出时,输出都是整数行。
  • 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 -o0GNU 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。我稍后再说。

管道交织

只要每个程序一次输出一行,并且这些行足够短,输出行就会整齐地分开。但是线路可以工作多长时间是有限制的。管道本身具有传输缓冲区。当程序输出到管道时,数据会从写入器程序复制到管道的传输缓冲区,然后再从管道的传输缓冲区复制到读取器程序。(至少从概念上讲,内核有时可能会将其优化为单个副本。)

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

回到上面的yes + grep示例,在我的系统上,yes aaaa碰巧一次写入了可容纳8192字节缓冲区的尽可能多的行。由于有5个字节要写入(4个可打印字符和换行符),这意味着它每次写入8190个字节。管道缓冲区的大小为4096字节。因此,可以从yes中获取4096字节,然后从grep中获取一些输出,然后从yes中获取其余的写操作(8190-4096 = 4094字节)。4096个字节为819行留出了空间,aaaa并带有一个孤行a。因此,一行带有此孤行,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 Parallel执行此操作。


有趣的是,这可能是确保所有行都以cat原子方式写入的好方法,从而使cat进程从foo / bar / baz接收整行,但从一个接收不到一行,从另一个接收到一半行,等等。 bash脚本可以做些什么吗?
亚历山大·米尔斯

1
听起来这也适用于我的情况,我有数百个文件,并且awk对于相同的ID产生了两行(或更多行)的输出,find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'正确地为每个ID产生了一行。
αғsнιη

为了防止任何交织,我可以在像Node.js这样的编程环境中进行此操作,但是对于bash / shell,则不确定如何执行此操作。
亚历山大·米尔斯

1
@JoL这是由于管道缓冲区已满。我知道我必须写故事的第二部分……完成。
吉尔(Gilles)'所以

1
@OlegzandrDenman TLDR添加:它们确实交错。原因很复杂。
吉尔(Gilles)'所以

1

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)。


该部分是错误的。xargs echo不会调用内置的echo bash,而是echo来自的实用程序$PATH。而且无论如何,我无法使用bash 4.4重现该bash回显行为。在Linux上,写入到大于4K的管道(不是/ dev / null)并不能保证是原子的。
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.