Linux 文件描述符与重定向:2>&1 到底把什么接到哪里
从一个常见的日志坑说起
在服务器上跑后台任务时,很多人写过这样的命令:
nohup ./job.sh 2>&1 > app.log &
结果标准错误还是打到了终端,app.log 里只有标准输出。问题不在 nohup,而在 2>&1 的位置。要把这件事讲清楚,得先回到文件描述符。
三个默认描述符
Linux 给每个进程都维护一张打开文件表,下标就是文件描述符(file descriptor,fd)。进程启动时内核会预置三个:
| fd | 名称 | 默认指向 |
|---|---|---|
| 0 | stdin | 键盘 / 管道输入 |
| 1 | stdout | 终端 |
| 2 | stderr | 终端 |
关键点:stdout 和 stderr 是两条独立的通道,只是默认都连到同一个终端,所以看起来混在一起。这也是为什么 cmd > out.log 只能收走 stdout,报错信息依旧刷屏。
重定向本质上只是「把某个 fd 重新指向一个文件」。> file 等价于 1> file,2> file 改的是 fd 2,< file 改的是 fd 0。
2>&1 是复制,不是别名
2>&1 的含义是:把 fd 2 指向 fd 1 当前指向的那个对象。它是一次性的复制动作,不是建立永久绑定。所以顺序至关重要:
cmd > app.log 2>&1 # 正确:先把 1 指向文件,再把 2 复制到同一处
cmd 2>&1 > app.log # 错误:2 先复制了当时的 1(终端),随后 1 才改到文件
第二种写法里,fd 2 复制的是「复制那一刻」fd 1 的指向,之后 fd 1 再改去哪里都与 fd 2 无关。这就是开头那个坑的根因。
Bash 提供了更不容易写错的简写 &> app.log,等价于 > app.log 2>&1,推荐在脚本里直接用。如果只想丢弃错误,用 2>/dev/null;只想保留错误,用 1>/dev/null。
管道场景下的一点差异
管道只接 stdout,所以 cmd | grep x 默认会把 stderr 漏在屏幕上。想连错误一起过滤,必须先把 2 合并进 1,再交给管道:
cmd 2>&1 | grep -i error
注意 cmd | grep x 2>&1 改的是 grep 的 fd 2,不是 cmd 的,方向完全不同。另外一个高频组合是 set -o pipefail:管道默认只返回最后一条命令的退出码,加上它之后,中间任何一环失败都会让整条管道返回非零,配合 set -e 才能真正让脚本在出错时停下来。
三个实用排障命令
不确定某个进程把输出写到哪儿了,直接看它的 fd 表最靠谱:
ls -l /proc/$PID/fd # 0/1/2 各自指向的文件或管道
readlink /proc/$PID/fd/1 # 单独看 stdout 的去向
想确认程序到底往哪个 fd 写了内容,可以用 strace -e trace=write -p $PID,输出里的 write(1, ...) 是 stdout,write(2, ...) 是 stderr。另外,echo $? 只看退出码,别把它和输出重定向混为一谈——重定向影响的是字节流,退出码是另一套机制。
小结
记住三句话就够用了:fd 是进程级的下标,> 改的是 fd 1;2>&1 是把 fd 2 复制成 fd 1 当时的指向,所以必须写在 > file 之后;管道只吃 stdout,要连 stderr 一起处理得先合并。把这些规则理顺,后台任务丢日志、CI 里看不到报错、脚本明明失败却返回成功这几类问题,基本都能一次定位。
- 点赞
- 收藏
- 关注作者
评论(0)