假设我运行一些进程:
#!/usr/bin/env bash
foo &
bar &
baz &
wait;
我像这样运行上面的脚本:
foobarbaz | cat
据我所知,当任何进程写入 stdout/stderr 时,它们的输出永远不会交错 - stdio 的每一行似乎都是原子的。这是如何运作的?什么实用程序控制每行的原子性?
答案1
他们确实交错!您只尝试了短的输出突发,它们保持不分裂,但实际上很难保证任何特定的输出保持不分裂。
输出缓冲
这取决于程序如何缓冲他们的输出。这标准输入输出库大多数程序在写入时使用缓冲区来提高输出效率。该函数不会在程序调用库函数写入文件时立即输出数据,而是将这些数据存储在缓冲区中,只有在缓冲区填满后才实际输出数据。这意味着输出是批量完成的。更准确地说,有三种输出模式:
- 无缓冲:数据立即写入,不使用缓冲区。如果程序将其输出分成小块(例如逐个字符)写入,则速度可能会很慢。这是标准错误的默认模式。
- 全缓冲:仅当缓冲区满时才写入数据。这是写入管道或常规文件时的默认模式(stderr 除外)。
- 行缓冲:在每个换行符之后或缓冲区已满时写入数据。这是写入终端时的默认模式(stderr 除外)。
程序可以重新编程每个文件以使其表现不同,并且可以显式刷新缓冲区。当程序关闭文件或正常退出时,缓冲区会自动刷新。
如果写入同一管道的所有程序都使用行缓冲模式,或者使用无缓冲模式并通过对输出函数的一次调用来写入每一行,并且如果行足够短以写入单个块,则输出将是整行的交错。但如果其中一个程序使用全缓冲模式,或者行太长,那么您将看到混合行。
这是一个示例,其中我交错了两个程序的输出。我在 Linux 上使用 GNU coreutils;这些实用程序的不同版本可能表现不同。
yes aaaa
aaaa
以本质上等同于行缓冲模式的方式永远写入。该yes
实用程序实际上一次写入多行,但每次发出输出时,输出都是整数行。while true; do 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% 的线路被中断,但它确实发生了。输出是随机的,因此中断的数量会有所不同,但我每次都至少看到一些中断。如果行较长,则中断行的比例会更高,因为随着每个缓冲区的行数减少,中断的可能性也会增加。
有几种方法可以调整输出缓冲。主要有:
- 关闭使用 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)。