Ale*_*lls 18 osx shell stdout output stderr
假设我运行了一些进程:
#!/usr/bin/env bash
foo &
bar &
baz &
wait;
Run Code Online (Sandbox Code Playgroud)
我像这样运行上面的脚本:
foobarbaz | cat
Run Code Online (Sandbox Code Playgroud)
据我所知,当任何进程写入 stdout/stderr 时,它们的输出永远不会交错——stdio 的每一行似乎都是原子的。这是如何运作的?什么实用程序控制每行的原子性?
Gil*_*il' 28
他们确实交错!您只尝试了保持未拆分的短输出突发,但实际上很难保证任何特定输出保持未拆分。
这取决于程序如何缓冲其输出。大多数程序在编写时使用的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
Run Code Online (Sandbox Code Playgroud)
如您所见,是的有时会中断 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
Run Code Online (Sandbox Code Playgroud)
所以这次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
Run Code Online (Sandbox Code Playgroud)
如果行长度小于管道缓冲区大小,则行缓冲保证输出中不会有任何混合行。
如果行长度可以更大,当多个程序写入同一个管道时,就无法避免任意混合。为了保证分离,你需要让每个程序写入不同的管道,并使用一个程序来组合这些行。例如GNU Parallel默认情况下会这样做。