Linux 重定向与管道
进程读写文件、终端或管道时,使用的不是文件名,而是内核分配的编号,称为文件描述符(file descriptor,fd)。从终端启动的程序,会从 shell 继承三个已经打开的 fd:fd 0 用于读取输入(stdin),fd 1 用于输出结果(stdout),fd 2 用于输出诊断信息(stderr),三者都指向终端。

程序只按编号读写,不需要关心 fd 背后是终端、文件还是管道。因此,shell 只要在程序启动前修改 fd 的指向,就能改变程序的输入来源和输出去向,而无需改动程序本身:
把 fd 指向文件,就是重定向;
用 | 把一个程序的 fd 1 通过管道连接到另一个程序的 fd 0,就是管道。
以下常见误区,都可以由这一机制解释:
- “重定向写在命令后面,所以命令先执行,再把输出写入文件”。 无论重定向写在命令的左边还是右边,shell 都会先处理完所有重定向,再启动程序;”从左到右”仅指多个重定向之间的执行顺序。
- “
cmd > file会把命令的所有输出写入文件”。>是1>的省略,只重定向 fd 1;错误信息写入 fd 2,而 fd 2 仍指向终端。 - “stderr 中都是错误信息”。 stderr 用于输出诊断信息,进度条、统计结果也属于诊断信息,例如
curl的进度条、git clone的进度都写入 stderr。 - “
2>&1 > file与> file 2>&1等价”。2>&1只在执行的那一刻,把 fd 1 此刻的指向复制给 fd 2,之后 fd 1 再变,fd 2 不会跟着变。写在> file之前时,fd 1 此刻还指向终端,fd 2 复制到的就是终端,错误信息不会写入 file。 - “终端上看到的输出,会全部通过管道(管道符|)传给下一个命令”。 终端上的输出来自 fd 1 和 fd 2 两个通道,管道只连接前一个命令的 fd 1,fd 2 的内容仍直接显示在终端上。
1. 文件描述符与命令的启动过程
1.1 默认的三个 fd
之所以称为”文件”描述符,是因为在 Linux 中,终端、管道、网络连接都和普通文件一样,通过 read、write 等同一组系统调用读写。
| fd | 名称 | 方向 | 默认指向 | 约定用途 |
|---|---|---|---|---|
| 0 | stdin(标准输入) | 输入 | 终端 | 读取输入 |
| 1 | stdout(标准输出) | 输出 | 终端 | 结果数据 |
| 2 | stderr(标准错误) | 输出 | 终端 | 错误、警告、进度等诊断信息 |

程序读写时只指定 fd,例如 write(1, …) 写入 fd 1,read(0, …) 从 fd 0 读取。
查看当前 shell 的 fd:

ls -l /proc/$$/fd列出当前 shell 打开的每个 fd 及其指向的对象($$是当前 shell 的进程号)。
三个 fd 都指向同一个终端 /dev/pts/0,因此在终端上无法区分某行输出来自 fd 1 还是 fd 2。
1.2 shell 如何启动一条命令
以 ls > out.txt 为例,shell 执行外部命令分为三步:
- 创建子进程(fork)。 子进程是 shell 的副本,继承 shell 的全部 fd。shell 自身的 fd 0、1、2 指向终端,所以子进程的 fd 也指向终端,这就是”默认指向终端”的由来。
- 在子进程中处理重定向。 此时子进程运行的仍是 shell 的代码。它按书写顺序逐条处理重定向:先以截断方式打开 out.txt,得到一个新的 fd(如 fd 3),再用系统调用
dup2把它复制到 fd 1,使 fd 1 指向 out.txt。重定向在子进程中进行,因此只影响这一条命令,shell 自身的 fd 不变。 - 加载目标程序(exec)。 进程号不变,运行的程序由 shell 换成 ls,fd 保持不变。ls 开始运行时,fd 1 已经指向 out.txt。

由此可以得出三个结论:
- 重定向在程序运行之前就已完成,与书写位置无关。 程序启动时,fd 已经设置好;
wc -l < /etc/passwd与< /etc/passwd wc -l完全等价。 - 重定向由 shell 以当前用户身份执行。 打开文件时的权限检查与之后运行的程序无关。
- 多个重定向按书写顺序执行,后一条看到的是前一条修改后的结果。 例如
cmd > out.txt 2>&1,先把 fd 1 指向 out.txt,再让 fd 2 复制 fd 1 此时的指向,因此 fd 2 也指向 out.txt;顺序颠倒,结果就不同。
内置命令(如 echo、cd)不创建子进程,shell 会临时修改自身的 fd,执行完毕后再恢复,效果相同。
重定向,就是 shell 在 fork 之后、exec 之前,修改子进程的 fd。
2. > 即 1>,2> 是同一种操作
重定向的完整写法是 n>文件,即把 fd n 重定向到文件。
n 省略时,> 默认为 fd 1(stdout,标准输出),< 默认为 fd 0(stdin,标准输入)。
下面两行完全等价:
1 | ls /etc > out.txt |
同理,2> 把 fd 2 重定向到文件:
1 | ls /etc/hostname /nope 2> err.txt |
需要注意的细节:
- 数字和
>之间不能有空格。 shell 只把紧贴>的数字识别为 fd。ls 2 > f中的2会被当作 ls 的参数,被重定向的是 fd 1。 >在命令运行前就会清空文件。 shell 处理重定向时以截断方式打开文件,此时命令尚未运行。因此即使命令执行失败,文件也已被清空;sort file > file也因此得到空文件。>>表示追加,同样可以指定 fd:2>> err.log。
<:输入重定向
< 即 0<,把 fd 0 重定向到文件。对比下面两条命令:
1 | $ wc -l /etc/passwd |
第一条命令中,文件名是 wc 的参数,wc 自己打开文件,因此能输出文件名。
第二条命令中,由 shell 打开文件,并在启动 wc 之前把它设置为 wc 的 fd 0。wc 没有收到任何参数,只从 fd 0 读取数据,无从得知文件名,因此只输出行数。这说明数据来源由 shell 设置,程序只负责读写 fd。
无论重定向写在命令的左边还是右边,shell 都会先处理完所有重定向,再启动程序;”从左到右”仅指多个重定向之间的执行顺序。
>、2>、< 是同一种操作,区别仅在于重定向的是哪个 fd。
3. stdout 与 stderr 的区别
3.1 为什么需要两个输出通道
如果程序只有一个输出通道,结果数据和错误信息会混在一起:把输出写入文件或交给下一个命令处理时,错误信息会混入数据,用户在终端上也看不到出错。
因此标准 I/O 把输出分为两个通道:fd 1 输出结果数据,fd 2 输出供人阅读的诊断信息。
执行 cmd > result.txt 或 cmd | grep x 时,结果数据被重定向,错误、警告等诊断信息仍显示在终端上。
3.2 内容没有区别,写入哪个 fd 由程序决定
系统不会检查写入的是不是错误信息:程序把一行 Error 写入 fd 1,它就是 stdout;把一行 OK 写入 fd 2,它就是 stderr。各种语言的输出函数最终都调用系统调用 write(fd, buf, len),写入哪个 fd 由程序的作者选择。
既然 stderr 的用途是诊断信息,而不只是错误,那么以下输出写入 stderr 都符合约定:
curl的进度条、git clone的进度、ffmpeg的运行日志。time的统计结果。
约定也并非所有程序都遵守,有些程序会把错误信息写入 stdout。因此判断一行输出来自哪个 fd,应通过实验,而不是看内容:
1 | cmd 2>/dev/null # 终端上剩下的都是 stdout |
3.3 用途不同,缓冲方式也不同
两个通道的用途不同,C 标准库为它们设置了不同的默认缓冲策略:
- stdout 输出数据,注重效率。 指向文件或管道时采用全缓冲,缓冲区满后一次写出,以减少系统调用;指向终端时采用行缓冲,遇到换行即写出,以便交互时及时显示。
- stderr 输出诊断信息,注重及时。 始终不缓冲,写入后立即输出,即使程序随后崩溃,诊断信息也不会丢失。
| 指向终端时 | 指向文件或管道时 | |
|---|---|---|
| stdout | 行缓冲 | 全缓冲 |
| stderr | 无缓冲 | 无缓冲 |
这导致重定向后的输出顺序可能与终端上不一致:执行 cmd > log 2>&1 时,stderr 立即写入,stdout 仍在缓冲区中,日志里错误信息会出现在前面。解决方法:在程序中调用 fflush;Python 使用 python -u 或设置 PYTHONUNBUFFERED=1;其他命令可使用 stdbuf -oL cmd。
stdout 和 stderr 在内容上没有区别,区别在于用途:fd 1 输出数据,注重效率;fd 2 输出诊断信息,注重及时。两者默认都指向终端,因此在终端上无法区分。
4. 2>&1 的含义与顺序
2>&1 的含义是:把 fd 2 指向 fd 1 此刻的指向。 这是一次性的复制,而不是绑定:复制发生在执行 2>&1 的那一刻,之后 fd 1 再改变,fd 2 不会跟着改变。底层对应系统调用 dup2(1, 2)。

&表示其后是 fd 编号,而不是文件名。写成2>1会创建一个名为1的文件。>&2即1>&2,把 fd 1 重定向到 fd 2 当前指向的位置,常用于在脚本中输出错误信息:echo "出错了" >&2。
4.1 顺序:从左到右依次执行
由于 shell 按书写顺序逐条处理重定向,可以把 fd 看作变量,2>&1 相当于赋值语句 fd2 = fd1:赋值时取 fd 1 的当前值,之后 fd 1 再变化,fd 2 不受影响。
写法 A:cmd > out.txt 2>&1(正确)
| 步骤 | 操作 | fd 1 | fd 2 |
|---|---|---|---|
| 初始 | — | 终端 | 终端 |
| 1 | > out.txt |
out.txt | 终端 |
| 2 | 2>&1 |
out.txt | out.txt |
结果:stdout 和 stderr 都写入 out.txt。
写法 B:cmd 2>&1 > out.txt(常见错误)
| 步骤 | 操作 | fd 1 | fd 2 |
|---|---|---|---|
| 初始 | — | 终端 | 终端 |
| 1 | 2>&1 |
终端 | 终端(复制了 fd 1 此时的指向) |
| 2 | > out.txt |
out.txt | 终端 |
结果:只有 stdout 写入文件,stderr 仍输出到终端。
4.2 为什么不能写成 > out.txt 2> out.txt
这种写法会两次独立打开同一个文件,两个 fd 各自维护写入位置,相互覆盖,内容会错乱。2>&1 使两个 fd 共享同一次打开的文件,写入位置也共享,因此能正确地交替写入。
2>&1 复制的是 fd 1 当前的指向,因此必须先设置好 fd 1,再复制给 fd 2。
5. 管道 | 只传递 stdout
5.1 管道的建立过程
执行 A | B 时,shell 按以下顺序操作:

- 创建一个管道,包含写入端和读取端。
- 为 A 和 B 分别创建子进程。在 A 的子进程中,把 fd 1 指向写入端;在 B 的子进程中,把 fd 0 指向读取端。
- 在各自的子进程中处理 A、B 自己的重定向。
- 分别加载 A、B 程序。
A 的 fd 2 在整个过程中没有改变,仍指向终端,因此 stderr 不经过管道,直接输出到终端:
1 | $ ls /etc/hostname /nope | wc -l |
5.2 管道先于重定向建立
从上述顺序可以看出,管道在第 2 步就已连接,A 自己的重定向在第 3 步才处理。

所以在 A 2>&1 | B 中,执行 2>&1 时 fd 1 已指向管道,fd 2 复制到的正是管道。
同理,A 2>&1 >/dev/null | B 可以只让 stderr 进入管道:先把 fd 2 指向 fd 1 当前指向的管道,再把 fd 1 重定向到 /dev/null,结果只有 stderr 进入管道。这也说明 2>&1 写在前面并非总是错误,关键在于执行时 fd 1 指向哪里。
6. 补充知识
6.1 &> 与 >&:bash 的简写
cmd &> file 等价于 cmd > file 2>&1,&>> file 表示追加(bash 4+)。它们不是 POSIX 语法。在 #!/bin/sh 脚本中(如 Debian 的 dash),cmd &> file 会被解析为”后台运行 cmd,再执行 > file“,且不会报错。需要可移植的脚本应使用 > file 2>&1。
cmd >& file 是来自 csh 的旧写法,效果相同;但文件名为数字时会被当作 fd 编号,bash 官方推荐使用 &>。
6.2 /dev/null:空设备
写入的数据被直接丢弃,读取时立即返回 EOF。常见用法:cmd >/dev/null 2>&1(丢弃全部输出),cmd 2>/dev/null(只丢弃错误信息)。
6.3 tee:同时输出到终端和文件
cmd 2>&1 | tee build.log 在终端显示输出的同时写入文件;tee -a 表示追加。
向需要 root 权限的文件写入时,sudo echo x > /etc/f 会失败。原因是重定向由 shell 在启动 sudo 之前、以当前用户身份执行,打开 /etc/f 时就因权限不足而失败,sudo 根本不会运行。正确的做法是让以 root 身份运行的程序去打开文件:echo x | sudo tee /etc/f >/dev/null,或 sudo sh -c 'echo x > /etc/f'。
6.4 exec:对整个脚本生效的重定向
不带命令的 exec 不会加载新程序,而是直接修改当前 shell 自身的 fd。之后启动的每个命令都从 shell 继承 fd,因此该重定向对之后的所有命令生效:
1 | exec > >(tee -a run.log) 2>&1 # 此后的全部输出同时写入日志 |
6.5 Here Document 与 Here String
两者都用于向 fd 0 提供输入:
1 | cat <<EOF # 多行输入,变量会展开;写作 <<'EOF' 则不展开 |
6.6 重定向的位置
重定向不必写在命令末尾:> out.txt echo hi 与 echo hi > out.txt 等价。对 for、while、{ … } 等复合命令,重定向作用于整体:{ cmd1; cmd2; } > all.log 2>&1 可一次收集多条命令的输出。