关闭管道缓冲。

我有一个脚本,调用了两个命令:
long_running_command | print_progress

长时间运行的命令会打印进度,但我对其不满意。我使用“print_progress”使其更美观(即在一行中显示进度)。
问题是:将管道连接到标准输出也会激活一个4K的缓冲区,所以漂亮的打印程序得到了...什么都没有...什么都没有...很多...:)
如何禁用“long_running_command”的4K缓冲区(不,我没有源代码)?

2所以,当您运行长时间运行的命令而不使用管道时,您可以正确地看到进度更新,但是当使用管道时,它们会被缓冲吗? - second
2是的,那正是发生的事情。 - Aaron Digulla
38几十年来,缺乏一种简单的控制缓冲区的方法一直是一个问题。例如,请参阅:http://marc.info/?l=glibc-bug&m=98313957306297&w=4,基本上说“我懒得做这个,并且这里有一些废话来为我的立场辩护”。 - Adrian Pronk
2http://serverfault.com/a/589614/67097 - Nakilon
10实际上,导致等待足够数据时延迟的是stdio而不是管道。管道确实有容量,但只要有任何数据写入管道,它立即准备好在另一端读取。 - Sam Watkins
15个回答

另一种解决这个问题的方法是使用stdbuf程序,它是GNU Coreutils的一部分(FreeBSD也有自己的版本)。
stdbuf -i0 -o0 -e0 command

这将完全关闭输入、输出和错误的缓冲。对于某些应用程序来说,出于性能原因,行缓冲可能更合适。
stdbuf -oL -eL command

请注意,这仅适用于动态链接的应用程序中的stdio缓冲(例如printf(), fputs()...),前提是该应用程序没有自行调整其标准流的缓冲。尽管如此,这应该涵盖了大多数应用程序。

9在Ubuntu中需要安装"unbuffer",它包含在2MB大小的expect-dev软件包中。 - lepe
2这在默认的Raspbian安装上非常有效,可以解决日志缓冲问题。我发现sudo stdbuff … command是可行的,但stdbuff … sudo command则不行。 - natevw
31@qdii stdbuf在与tee一起使用时无效,因为tee会覆盖stdbuf设置的默认值。请参阅stdbuf的手册页面。 - ceving
5@lepe 奇怪的是,unbuffer对x11和tcl/tk有依赖,这意味着如果你在没有它们的服务器上安装它,实际上需要超过80MB的空间。 - lambshaanxy
1太棒了!真希望我能多点赞。我有一个后台进程,但它没有写入日志...即使在终止后,它也不会将缓冲区写出。stdbuf 真是太好用了! - Rado
@lepe,这并不奇怪,因为它是Expect的一部分,所以是用TCL编写的,而且TCL和Tk经常一起打包...而Tk有整个有趣的链条。 - Charles Duffy
@CharlesDuffy - 确实,TCL/Tk似乎是不可分割的。就我所知,在涉及到TCL时,jim更受欢迎。它不会为一个简单的脚本语言加载一些庞大的类似于dot-net的依赖链。 - mikeserv
1就我所知,我根本无法让这个工作起来。我试图将一个长时间运行的命令传输给ts(时间戳),但是我找不到任何可行的变体,即使在内部使用sudo也不行。例如:sudo stdbuf -o0 -e0 -i0 /usr/local/bin/ec2-snapshot-all | stdbuf -o0 -e0 -i0 ts(即使移动sudo也没有帮助)。相比之下,socat解决方案完美地运行。 - rlpowell
@rlpowell,将值设置为0将使其无缓冲。 您需要将其设置为大于0的值,或者如果您愿意,将其设置为L,它将是行缓冲的。 输入“stdbuf --help”以获取所需的所有信息。 - Adrian
@Adrian 是的,无缓冲是我想要的,但它以漂亮而大的多行块方式保持缓冲。 - rlpowell
16@qdii stdbuf使用LD_PRELOAD机制来插入自己的动态加载库libstdbuf.so。这意味着它不能与具有以下特征的可执行文件一起工作:设置了setuid或文件权限,静态链接,不使用标准libc。在这些情况下,最好使用带有unbuffer / script / socat的解决方案。详见stdbuf with setuid/capabilities - pabouk - Ukraine stay strong
stdbuf是否具有混合的行和块缓冲?也就是说,在遇到换行符时输出,或者当缓冲区达到高水位线时输出? - CMCDragonkai
当使用管道命令时,我需要在每个段前面加上 stdbuf 吗? - jchook
1这对我没用,使用mycommand | tee /dev/tty | awk 'awkstuff' 这样的管道命令没有起作用 - 我尝试在每个命令前面加上 stdbuf -o0 -e0 -i0。最终有效的方法是使用 awk(实际上是 mawk)并添加参数 -Winteractive - brandones
@brandones:这可能是因为mawk的内部缓冲区,stdbuf无法对其进行操作。请参考https://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html。 - a3nm
3@jchook 是的,接受答案中使用unbuffer的说法在这里也适用:“对于较长的管道,您可能需要对每个命令进行unbuffer处理”。 - shaneb
1有没有办法在已知PID的情况下,强制对已运行的进程进行管道/printf缓冲区的外部处理? - mvorisek
@Mvorisek:我不知道,我猜这个问题可以单独提出来问。 - a3nm
请注意,如果命令覆盖了缓冲区设置(例如Python命令),则此方法将无效。请参阅https://stackoverflow.com/questions/55654364/why-stdbuf-has-no-effect-on-python - Rufus
@pabouk-Ukrainestaystrong 我们如何验证 stdbuf 是否适用于特定命令,比如 adb - John
1@John 你说的“验证stdbuf”是什么意思?你可以直接尝试使用它。如果你想在尝试之前进行检查,可以验证二进制文件是否调用了IO流函数https://www.gnu.org/software/libc/manual/html_node/I_002fO-on-Streams.html。你可以运行`ltrace -x '*@libc.so*' your_binary`并分析输出结果 :) - pabouk - Ukraine stay strong
@pabouk-Ukrainestaystrong 对不起,我的英语很差。我的意思是,我要如何验证 stdbuf stdbuf -i0 -o0 -e0 是否适用于特定的命令,比如 adb - John
@John 我没有什么新的可以补充的。只是尝试使用它,并检查是否达到你期望的效果。如果它表现不同,你可以使用ltrace来检查库调用,但这需要一些知识和时间。 - pabouk - Ukraine stay strong

你可以使用unbuffer命令(该命令是expect软件包的一部分),例如。
unbuffer long_running_command | print_progress

unbuffer通过伪终端(pty)连接到long_running_command,这使得系统将其视为交互式进程,因此不使用导致延迟的管道中的4-kiB缓冲。

对于较长的管道,您可能需要对每个命令(除了最后一个)进行取消缓冲,例如:

unbuffer x | unbuffer -p y | z

4事实上,使用pty连接到交互式进程是expect的一般情况。 - cheduardo
23当将调用传递给unbuffer时,您应该使用-p参数,以便unbuffer从stdin中读取。 - Chris Conway
34注意:在Debian系统中,这被称为expect_unbuffer,并且位于expect-dev软件包中,而不是expect软件包中。 - bdonlan
当我按下Ctrl+Z暂停程序时,unbuffer不会传递这个命令,所以程序会继续占用CPU。我使用的是Ubuntu 10.04。 - Joey Adams
5@bdonlan:至少在Ubuntu(基于Debian的)上,expect-dev提供了unbufferexpect_unbuffer两个命令(前者是后者的符号链接)。这些链接自expect 5.44.1.14-1(2009年)版本以来可用。 - jfs
3unbuffer现在是Debian主要的expect软件包中的一部分(它仍然是指向expect_unbuffer的符号链接,expect_unbuffer也在主要的expect软件包中)。 - cas
1注意:在Ubuntu 14.04.x系统上,它也包含在expect-dev软件包中。 - Alexandre Mazel
我在使用unbuffer时遇到了一些奇怪的错误。对我来说,一个最小可重现的例子是unbuffer cat /dev/urandom > /dev/null;这会在几秒钟内导致段错误。如果使用/dev/zero或者不使用unbuffer,则不会发生这种情况,这让我觉得expect可能在尝试对输出进行某种操作。这种解释是否合理? - Daniel H
当我尝试取消ffmpeg的缓冲时,这对我来说不起作用。 - Michael

将用于long_running_command的行缓冲输出模式打开的另一种方法是使用运行long_running_command的伪终端(pty)的script命令。

script -q /dev/null long_running_command | print_progress      # (FreeBSD, Mac OS X)
script -q -c "long_running_command" /dev/null | print_progress # (Linux)

19+1 好棒的技巧!因为 script 是一个非常古老的命令,所以它应该在所有类Unix平台上都可用。 - Aaron Digulla
5在Linux上,你还需要使用-q参数:script -q -c 'long_running_command' /dev/null | print_progress - jfs
1看起来脚本从stdin读取,这使得在交互式终端启动时,无法将这样一个long_running_command运行在后台。为了解决这个问题,我能够将stdin重定向到/dev/null,因为我的long_running_command不使用stdin - haridsv
1即使在安卓手机上也适用。 - not2qubit
1我可以确认这也适用于Mac。 - Umur Kontacı
3一个显著的缺点是:ctrl-z不再起作用(即无法暂停脚本)。可以通过以下方式解决:echo | sudo script -c /usr/local/bin/ec2-snapshot-all /dev/null | ts,如果您不介意无法与程序进行交互。 - rlpowell
没有其他命令存在于Busybox中,只有这个命令存在,但是当我尝试使用它时,它没有起作用。可能仅仅是我的情况,我在处理文件描述符方面做了一些奇怪的事情。 - CMCDragonkai
1太好了。这个方法可以让控制字符与 script -q -c 'python -c "import pdb, sys; pdb.set_trace()"' /dev/null | tee -a /tmp/tmp.txt 一起使用。 - blueyed
请注意,在SmartOS/Solaris衍生版中,这与script命令不兼容。 - Ed L
1我发现的一个缺点是 script 命令会用自己的返回状态掩盖命令的返回状态(通常为0)。如果 print_progress 或者其余部分的复合命令依赖于它(通过使用 ||&& 结构),那么结果将不如预期。 - milton
1如果脚本本身位于管道的中间,请非常小心。我之前执行了cat a | script python b.py | sink,结果覆盖了b.py文件。幸好我在vim中打开了该文件,否则我将损失一个小时的工作成果。 - user1055947
3使用script对我有效,而stdbuf则无效。如果你想让script返回<cmd>的退出代码,可以使用script -e -c <cmd> /dev/null - ntc2

对于 grepsedawk,你可以强制输出为行缓冲。你可以使用以下命令:
grep --line-buffered

强制输出为行缓冲。默认情况下,当标准输出是终端时,输出是行缓冲的;否则是块缓冲的。
sed -u

使输出行缓冲。
有关更多信息,请参阅此页面: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

11值得注意的是,python 还支持 -u 参数来禁用缓冲。 - David Parks
1这样使用grep(等等)是不行的。当你执行long_running_command时,已经晚了。它在到达grep之前就已经被缓冲了。 - tgm1024--Monica was mistreated
这仍然是缓冲的。如果有人想要看到行进度怎么办? - Michael
这个命令 grep --line-buffered pattern *many*many*files* | head 是否有效?看起来 grep 在将输出行传递给 head 之前会处理所有文件。 - golimar
我还以为我是一个grep高手。 - BaseZen
啊,太棒了!这是我在观看 ZFS 重建的 IOPS:zpool iostat -vH tank 1 | grep --line-buffered ata-TOSHIBA_HDWG460_NVYXDA0GMFR1H | awk '{print $5;fflush()}' | average 。请注意 awk 中的 fflush(),用于取消缓冲。 - Bill McGonigle

如果libc在输出不到终端时修改其缓冲/刷新是一个问题,您应该尝试使用socat。您可以在几乎任何类型的I / O机制之间创建双向流。其中之一是forked程序与伪tty交谈。
 socat EXEC:long_running_command,pty,ctty STDIO 

它的功能是:
- 创建一个伪终端(pseudo tty) - 使用伪终端的从端作为标准输入/输出,派生出长时间运行的命令(long_running_command) - 在伪终端的主端和第二个地址(这里是STDIO)之间建立一个双向流
如果这与`long_running_command`产生相同的输出,那么你可以继续使用管道。
编辑:哇,我没有看到unbuffer的答案!不过,socat仍然是一个很棒的工具,所以我可能会保留这个答案。

2...而且我不知道socat - 它看起来有点像netcat,只是可能更强大一些。;) 谢谢和+1。 - cheduardo
5我会在这里使用 socat -u exec:long_running_command,pty,end-close - - Stéphane Chazelas

您可以使用

long_running_command 1>&2 |& print_progress

问题在于当将stdout输出到屏幕时,libc会使用行缓冲,而将stdout输出到文件时则会使用块缓冲,但对于stderr,则不使用缓冲。
我认为这并不是管道缓冲的问题,而是与libc的缓冲策略有关。

你说得对,我的问题仍然是:我如何在不重新编译的情况下影响libc的缓冲策略? - Aaron Digulla
@StéphaneChazelas fd1将被重定向到标准错误输出。 - Wang HongQin
@StéphaneChazelas,我不明白你的争论点。请做一个测试,它有效。 - Wang HongQin
6好的,发生的情况是,在zsh(其中|&来自于csh)和bash中,当你执行 cmd1 >&2 |& cmd2时,文件描述符 1 和 2 都连接到外部的标准输出。因此,它能防止缓冲,但这仅适用于外部标准输出是终端的情况下,因为输出不通过管道(所以print_progress什么也不打印)。所以,这与long_running_command & print_progress是一样的(除了print_progress的标准输入是一个没有写入者的管道)。你可以通过ls -l /proc/self/fd >&2 |& catls -l /proc/self/fd |& cat进行验证。 - Stéphane Chazelas
6这是因为|&2>&1 |的简写。所以cmd1 |& cmd2等同于cmd1 1>&2 2>&1 | cmd2。因此,文件描述符1和2都连接到了原始的stderr,没有任何内容被写入管道中。(在我之前的评论中将s/outer stdout/outer stderr/g)。 - Stéphane Chazelas
这并不能解决原始问题,但在使用tee作为print_progress函数时会有所帮助。通过这种解决方案,您可以立即获得控制台输出,但tee的输出仍然是缓冲的。 - Dima Chubarov

过去是这样,现在可能仍然是这样,当标准输出被写入终端时,默认情况下是行缓冲的 - 当写入换行符时,该行将被写入终端。当标准输出被发送到管道时,它是完全缓冲的 - 因此只有当标准I/O缓冲区填满时,数据才会被发送到管道中的下一个进程。
这就是问题的根源。我不确定在不修改写入管道的程序的情况下是否有很多事情可以做来修复它。您可以使用`setvbuf()`函数和`_IOLBF`标志无条件地将`stdout`设置为行缓冲模式。但我没有看到一种简单的方法来强制对程序进行这样的设置。或者程序可以在适当的位置(每行输出后)使用`fflush()`,但同样的评论也适用。
我想,如果您用伪终端替换管道,那么标准I/O库会认为输出是终端(因为它是终端的一种类型),并且会自动进行行缓冲。不过,这是一种复杂的处理方式。

1实际上,当无法更改程序代码时,这是一种处理事情的简单方法,就像问题所说的那样。https://unix.stackexchange.com/a/215071/5132 - JdeBP

我知道这是一个老问题,已经有很多答案了,但如果你想避免缓冲问题,可以尝试类似以下的方法:
stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

这将实时输出日志,并将其保存到logs.txt文件中,缓冲区将不再影响tail -f命令。


7这看起来像是第二个答案 :-/ - Aaron Digulla
2stdbuf是包含在gnu coreutils中的(我在最新版本8.25上进行了验证)。我已经验证了它在嵌入式Linux上的工作。 - zhaorufei
从stdbuf的文档中可以得知,注意:如果命令调整了其标准流(例如'tee')的缓冲方式,那么这将覆盖'stdbuf'所做的相应更改。 - shrewmouse

我认为问题不在于管道。听起来像是你的长时间运行的进程没有频繁地刷新自己的缓冲区。改变管道的缓冲区大小可能是一个绕过此问题的方法,但我认为这是不可能的,除非重新构建内核 - 这是一种你不希望作为一种临时解决方案而做的事情,因为它可能会对许多其他进程产生不利影响。

20根本原因是如果标准输出不是终端,libc会切换到4k缓冲。 - Aaron Digulla
5这非常有趣!因为管道不会造成任何缓冲。它们提供了缓冲,但是如果你从管道中读取数据,你会得到可用的任何数据,而不需要等待管道中的缓冲。所以罪魁祸首应该是应用程序中的stdio缓冲。 - shodanex

Chad的回答类似,你可以编写一个类似这样的小脚本:
# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

然后将这个 scriptee 命令用作 tee 的替代品。
my-long-running-command | scriptee

唉,看起来我无法在Linux上完美地运行这样的版本,所以似乎只限于类似BSD风格的Unix系统。
在Linux上,这个版本很接近,但是当它完成后你不会立即得到提示符(直到你按下回车键等)...
script -q -c 'cat > /proc/self/fd/1' /dev/null

为什么那个有效?"script"会关闭缓冲吗? - Aaron Digulla
@Aaron Digulla:script 模拟终端,所以我认为它会关闭缓冲区。它还会回显发送给它的每个字符 - 这就是为什么在示例中将 cat 发送到 /dev/null 的原因。就运行在 script 中的程序而言,它正在与一个交互式会话交流。我认为在这方面它类似于 expect,但 script 可能是你的基本系统的一部分。 - jwd
我使用tee的原因是将流的副本发送到文件中。文件在scriptee中如何指定? - Bruno Bronosky
@BrunoBronosky:你说得对,这个程序的名字确实不好。它并不真正执行“tee”操作,只是根据原始问题禁用了输出缓冲。也许应该叫它“scriptcat”(尽管它也不是在执行连接操作...)。无论如何,你可以将cat命令替换为tee myfile.txt,就能达到你想要的效果。 - jwd