假设我运行一些流程:
#!/usr/bin/env bash
foo &
bar &
baz &
wait;
我像这样运行上面的脚本:
foobarbaz | cat
据我所知,当任何进程写入stdout / stderr时,它们的输出都不会交错-stdio的每一行似乎都是原子的。这是如何运作的?哪个实用程序控制每行的原子性?
假设我运行一些流程:
#!/usr/bin/env bash
foo &
bar &
baz &
wait;
我像这样运行上面的脚本:
foobarbaz | cat
据我所知,当任何进程写入stdout / stderr时,它们的输出都不会交错-stdio的每一行似乎都是原子的。这是如何运作的?哪个实用程序控制每行的原子性?
Answers:
他们交织!您仅尝试了短的输出突发,这些突发仍未拆分,但是实际上很难保证任何特定的输出都保持拆分。
这取决于程序如何缓冲其输出。大多数程序在编写时使用的stdio库使用缓冲区来提高输出效率。该程序不会在程序调用库函数写入文件后立即输出数据,而是将数据存储在缓冲区中,并且仅在缓冲区填满后才实际输出数据。这意味着输出是分批完成的。更准确地说,有三种输出模式:
程序可以将每个文件重新编程为不同的行为,并且可以显式刷新缓冲区。当程序关闭文件或正常退出时,缓冲区将自动刷新。
如果所有正在写入同一管道的程序都使用行缓冲模式或使用非缓冲模式,并通过对输出函数的单次调用来写每行,并且如果这些行足够短,则可以写入单个块中,则输出将是整行的交织。但是,如果其中一个程序使用全缓冲模式,或者如果行太长,那么您将看到混合的行。
这是一个示例,其中我交错了两个程序的输出。我在Linux上使用了GNU coreutils。这些实用程序的不同版本可能会有所不同。
yes aaaa
aaaa
以本质上等同于行缓冲模式的方式永久写入。该yes
实用程序实际上一次写入多行,但是每次发出输出时,输出都是整数行。echo bbbb; done | grep b
bbbb
以完全缓冲模式永久写入。它使用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%的线被打断,但还是发生了。输出是随机的,因此中断的数量会有所不同,但是我每次至少看到一些中断。如果线路较长,则中断线路的比例会更高,因为随着每个缓冲区的线路数量减少,中断的可能性也会增加。
有几种方法可以调整输出缓冲。主要的是:
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。我稍后再说。
只要每个程序一次输出一行,并且这些行足够短,输出行就会整齐地分开。但是线路可以工作多长时间是有限制的。管道本身具有传输缓冲区。当程序输出到管道时,数据会从写入器程序复制到管道的传输缓冲区,然后再从管道的传输缓冲区复制到读取器程序。(至少从概念上讲,内核有时可能会将其优化为单个副本。)
如果要复制的数据超出管道传输缓冲区的容量,则内核一次复制一个缓冲。如果有多个程序正在写入同一管道,并且内核选择的第一个程序要编写多个缓冲区,则不能保证内核第二次再次选择相同的程序。例如,如果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脚本可以做些什么吗?
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产生了一行。
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)并不能保证是原子的。