Bash脚本如何实时读取子进程stdout 无需等待进程终止
问题背景
现有一个C++编写的计数程序,逻辑为从0开始计数,每秒数值加1,通过printf向标准输出打印当前计数值,程序源码如下:
#include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { int ctr = 0; for (int i = 0; i < 10; ++i) { printf("%i\n", ctr++); sleep(1); } return 0; }
需求是通过Bash脚本调用该程序,每次程序输出新的计数值时立刻执行对应处理逻辑,且不能修改C++程序源码,仅可调整Bash脚本实现。
原有存在问题的Bash脚本如下,该脚本会等待C++程序完全执行结束后,才一次性处理所有输出值,无法满足实时处理要求:
#!/bin/bash for c in $(./script-test) do echo $c done
故障原因
原有脚本无法实时处理输出由两个原因共同导致:
- 命令替换语法
$(命令)的固有逻辑:Shell会等待括号内的命令完全执行终止,收集完该命令的所有标准输出内容后,才会将内容拆分传入for循环遍历,天生不支持实时读取运行中进程的输出流。 - 标准输出缓冲机制:当程序的标准输出连接的是管道/文件而非交互终端时,C标准库的
printf默认会开启全缓冲模式,缓冲区攒够4k左右数据才会真正把内容写入管道,哪怕输出内容带换行符也不会立刻刷新缓冲区,进一步加剧了输出延迟。
可行实现方案
通过系统自带的流缓冲修改工具+管道逐行读逻辑即可实现实时输出处理,无需修改C++代码,调整后的Bash脚本如下:
#!/bin/bash # 将目标程序的标准输出设为行缓冲,通过管道逐行实时读取输出 stdbuf -oL ./script-test | while IFS= read -r line do # 此处替换为实际需要执行的处理逻辑,示例为打印读取到的值 echo "$line" done
关键参数说明
stdbuf -oL:是GNU coreutils自带的标准工具,可在不修改程序源码的前提下,调整启动程序的流缓冲策略;参数-oL表示将标准输出(stdout)设置为行缓冲模式,即每次输出遇到换行符就立刻刷新缓冲区将内容写入管道,完美解决printf默认全缓冲导致的输出不实时问题。while IFS= read -r line:是Bash中逐行读取管道流的标准写法,管道传入的内容每到一行就会立刻触发循环体执行,不会等待上游程序终止;IFS=用于保留行首尾的空白字符,-r参数用于禁止自动转义反斜杠字符,避免读取内容被意外修改。
极端场景兼容方案
如果遇到极少数静态编译、不支持stdbuf注入缓冲配置的程序,可以换用unbuffer工具实现相同效果,该工具随expect包提供,脚本写法如下:
#!/bin/bash # 需提前安装expect包:Debian/Ubuntu执行apt install expect,RHEL/CentOS执行yum install expect unbuffer ./script-test | while IFS= read -r line do echo "$line" done
内容的提问来源于stack exchange,提问作者Stuart Bailey
相关产品推荐
相关产品推荐

