第2章 输出重定向与控制:stdout、stderr 的缓冲区与 tee 实战
你可能天天都在用命令行,但很多人其实没搞明白一个事:屏幕上那一串串文本,到底是怎么流进来的?
我当初刚工作时也糊里糊涂。总觉得 > 和 2> 差不多,结果有一次线上脚本日志全丢了,排查半天才发现是 stderr 根本没被重定向。从那以后,我才认真啃了一遍输出流和缓冲区。今天咱就把这块讲透。
2.1 标准输出 vs 标准错误——两条水管
每个 Linux 进程启动时,系统会分配三个文件描述符:
- 0 (stdin):输入,默认连键盘
- 1 (stdout):标准输出,正常结果写这里
- 2 (stderr):标准错误,错误消息写这里
说白了,这就是两条独立的水管。默认情况下,它们都接到终端屏幕上,所以你看不出区别。但一旦你想把日志存文件,麻烦就来了——你以为是“全部输出”,结果只存了正常消息,错误信息全漏了。
run_command > log.txt 跑脚本,以为万无一失。结果脚本执行失败了,log.txt 里只有部分输出,真正有用的错误信息一条没写进去。后来才发现,stderr 根本没被重定向。
2.2 重定向操作符——让水管拐弯
这些符号就是控制水流方向的阀门:
| 操作符 | 作用 | 举例 |
|---|---|---|
> file |
将 stdout 重定向到文件(覆盖) | ls > list.txt |
>> file |
将 stdout 追加到文件 | echo "hello" >> log.txt |
2> file |
将 stderr 重定向到文件 | cmd 2> err.log |
2>&1 |
将 stderr 合并到 stdout | cmd > out.log 2>&1 |
&> file |
同时重定向 stdout 和 stderr(Bash 4+) | cmd &> all.log |
注意 2>&1 的写法:它必须放在重定向目标之后,而且顺序有讲究。我习惯把输出文件写在前面,再写 2>&1,逻辑更清晰。
2.3 合并重定向——别再写错了
最常见的需求就是:把 stdout 和 stderr 全写进一个文件。很多人会写:
cmd 2> file 1> file
这么做虽然也能合并,但两个文件描述符各开各的流,有可能出现覆盖错乱。标准做法是用 2>&1 或 &>。
# 方式一:先重定向 stdout,再合并 stderr
command > output.log 2>&1
# 方式二:Bash 简写
command &> output.log
# 方式三:追加模式
command >> output.log 2>&1
你想想看,线上脚本用第一种写法最稳妥,兼容所有 shell。方式二只适用于 bash 4+,万一环境是 sh 或旧版 bash 就炸了。
2.4 缓冲区管理——为什么日志会“卡住”
有个现象你可能遇到过:脚本里写 echo "start" >> log,然后程序崩溃了,结果 log 文件里什么都没有。为什么?
因为 缓冲区 在作怪。C 标准库默认有三种缓冲模式:
- 全缓冲:只有当缓冲区填满(通常 4096 字节)或显式 flush 时才写入。重定向到文件时,stdout 默认变成全缓冲。
- 行缓冲:遇到换行符就输出。当 stdout 连到终端时,默认是行缓冲。
- 无缓冲:立即输出。stderr 默认就是无缓冲——这也是为什么错误消息总能立刻看到。
回到刚才的问题:脚本把 stdout 重定向到文件,变成了全缓冲。程序在填满缓冲区之前崩溃,那段 "start" 就永远留在内存里了。我个人习惯在关键位置加 flush(),或者干脆用 stderr 输出关键日志——反正它不带缓冲。
stdbuf -o 0 ./your_program 强制 stdout 无缓冲,避免日志丢失。不过生产环境不建议这么干,性能会下降。
2.5 tee 命令实践——看日志同时存文件
如果你既想实时看输出,又想存到文件里,tee 就是干这个的。它的名字来自管道工(T形三通),把输入流“复制”一份。
# 同时显示并保存 stdout
ls -la | tee output.txt
# 追加模式(-a)
ls -la | tee -a output.txt
# 同时保存 stderr(需要先重定向)
command 2>&1 | tee output.txt
注意 tee 本身只管 stdin,不管 stderr。所以保存错误日志要先用 2>&1 把 stderr 合并到 stdout 再传给它。
我有个真实案例:用 make build 2>&1 | tee build.log 编译项目,既能实时看到编译错误(快速定位),又能在 build.log 里回溯完整记录。出问题时,我把 log 发给同事,对方一眼就看到最后几行的错误栈——比截图靠谱多了。
# 保存完整日志 + 实时显示
./long_script.sh 2>&1 | tee -a /var/log/app/run_$(date +%Y%m%d).log
# 不希望用户看到重复输出?把 tee 写进文件即可
./long_script.sh 2>&1 | tee /dev/null # 不存文件,只打屏(少见)
知识结构概览
下面这张图把这章核心知识点串在一起,画的是“命令输出 → 两条流 → 重定向 → 缓冲区 → tee”的完整路径。你保存下来,以后忘了翻翻看。
2.6 实战避坑清单
最后,我把这些年踩过的坑和学到的经验整理成清单,你照着检查能省不少试错时间:
- 分清 stdout 和 stderr——用
&>或2>&1确保两者都写入日志。 - 重定向顺序:
2>&1必须放在> file之后,否则相当于把 stderr 扔到了当前 stdout(可能还是终端)。 - 缓冲区坑:脚本日志丢失?在关键位置加
sys.stdout.flush()或改用 stderr 输出。 - tee 的 -a:生产日志千万别用
tee > log(覆盖),一定用tee -a log追加。 - 管道与缓冲区:
cmd | grep xxx时,cmd 的 stdout 重定向到管道后会变成全缓冲?不一定,取决于具体实现。安全做法是在 cmd 里加flush或使用stdbuf -oL(行缓冲)来保证实时性。
嗯,到这里这章的核心内容就讲完了。你再看一眼那张流程图,从命令到两条流,再到重定向、缓冲区,最后用 tee 兜底——整个链路就通了。下次写脚本时,多留个心眼,别让日志再丢了。