Linux 基础 06:管道与重定向——三条流、一个顺序问题

发布于 2022-04-14 20:30 更新于 2022-04-14 22:10 1303 字 7 min read ... 访问量 手写 每个字都是我自己敲的。

Linux 基础 25:收尾——自查清单与继续深入的方向Linux 基础 24:启动流程与救援——UEFI、GRUB、initramfs 与 emergency modeLinux 基础 23:容器与内核边界——namespace、cgroup 与 docker exec 背后Linux 基础 22:日志与故障排查方法论——从现象到根因的六步Linux 基础 21:定时任务——crontab 的坑与 systemd timerLinux 基础 20:Shell 脚本(下)——trap、日志、幂等与并发锁Linux 基础 19:Shell 脚本(上)——引号、条件与退出码Linux 基础 18:性能观测与 strace——机器到底卡在哪Linux 基础 17:防火墙——iptables、nftables、firewalld 与 ufw 的关系Linux 基础 16:SELinux——当权限都对,服务还是被拒绝Linux 基础 15:权限进阶——sudoers 的通配陷阱、ACL 与 capabilitiesLinux 基础 14:SSH 与远程运维——密钥、权限与三种隧道Linux 基础 13:网络基础——自下而上的排错顺序Linux 基础 12:磁盘与文件系统——从块设备到挂载点Linux 基础 11:systemd——unit、systemctl 与 journalctl 的正确用法Linux 基础 10:包管理——apt 到底做了什么,以及四个高频报错Linux 基础 09:用户与组——passwd、shadow、sudoers 三件套Linux 基础 08:环境变量与 shell 启动文件——为什么 ssh 跑命令找不到你的 PATHLinux 基础 07:进程与信号——TERM 和 KILL 到底差在哪Linux 基础 06:管道与重定向——三条流、一个顺序问题Linux 基础 05:文本处理——把 grep/sort/uniq 变成你的分析工具Linux 基础 04:权限与属主——rwx 在文件和目录上不是一回事Linux 基础 03:ls、cp、mv、rm 的真实语义,以及那个由 shell 展开的 *Linux 基础 02:/ 底下为什么是这些目录,inode 里到底存了什么Linux 基础 01:终端、Shell,和命令是怎么被找到的
stdin/stdout/stderr 的本质、`2>&1` 为什么"顺序比内容重要"、`>` 会先清空文件、管道退出码与 pipefail、tee 与进程替换,以及 sudo 重定向失败的真实原因。

这一节要解决什么

重定向和管道是 shell 里最像”魔法”的部分:> log 2>&1 & 这一串符号为什么能同时决定输出去哪、错误去哪、进程在哪。它们的答案都在文件描述符上——而且有一个真实事故级别的顺序问题:2>&1 > file 和 > file 2>&1 完全不是一个意思。

一、先承认一件事:程序只有三个出口

每个进程启动时,内核都会给它三个已打开的”文件”:

fd名字默认指向用途
0stdin你的键盘(pty)输入
1stdout终端正常输出
2stderr终端错误与日志

重定向的本质是 dup2():shell 在 fork 出子进程、exec 之前,把 fd 1(或 2)换成你指定的文件。所以”重定向是 shell 做的”这句话决定了后面所有的坑。

ls /etc > out.txt              # stdout → out.txt,错误仍然打在终端
ls /nope 2> err.txt             # stderr → err.txt
ls /etc /nope > out.txt 2>&1    # 两个都进 out.txt ← 注意顺序

二、2>&1 的顺序:一个真实事故级别的细节

2>&1 的含义是”把 stderr 复制到当前 stdout 指向的地方”,所以它的效果取决于它出现的位置:

ls /etc /nope > all.txt 2>&1    # ✅ 先改 1 → all.txt,再把 2 也指到同一处:都进 all.txt
ls /etc /nope 2>&1 > all.txt    # ❌ 先把 2 指到"当时的 1"(还是终端),再改 1 → all.txt

批注:第二种写法里,错误会打在终端、正常输出进文件——很多”日志里怎么少了报错”的疑难都是这一行。记法:从左到右、即时生效。bash 还提供了简写 &>(等价 > file 2>&1),zsh 里是 >&。

有意思的是 > 的行为:它是”打开 + 截断”,所以

echo hi > f; echo there > f; cat f        # there(旧的 hi 被截掉)
set -o noclobber; echo x > f 2>&1 | head -1   # 拒绝覆盖已存在文件
echo x >| f                                   # 强制覆盖(noclobber 下的例外语法)

三、管道:shell 搭的两根管子

ls -l /etc | grep passwd          # 两个进程 + 一个 pipe(2)
# 等价于: ls 的 stdout 接到 pipe 写端,grep 的 stdin 接到 pipe 读端

管道是并发的:a | b 里 a、b 同时在跑,不是”a 跑完再跑 b”。这也是为什么 yes | head -1 不会无限运行——head 读够就退出,yes 收到 SIGPIPE 后死掉。

退出码的陷阱:$? 取的是管道最后一个命令的退出码。

false | true; echo $?            # 0 ← 前面的失败被吞掉了
set -o pipefail; false | true; echo $?   # 1 ✅ 管道中任一环节失败即失败
echo "${PIPESTATUS[@]}"          # 每个环节的退出码(bash 专有)

四、tee、进程替换与 here-doc

make 2>&1 | tee build.log                     # 屏幕一份、文件一份
make 2>&1 | tee -a build.log | grep -i error  # 追加 + 继续往下传
diff <(sort a) <(sort b)          # 把命令的输出当文件用(bash/zsh)
cat <<'EOF' > cfg.sh
echo "$HOME"        # 单引号 EOF:变量不会被展开(写配置模板的常用手法)
EOF
wc -c <<< 'hello'                 # here-string:等价于 printf 'hello\n' | wc -c

五、真实故障:sudo echo x > /root/f 为什么会 Permission denied

sudo echo hello > /root/f
# bash: /root/f: Permission denied     ← 明明用了 sudo!

原因:重定向是 shell 做的事,不是 sudo 做的事。 shell 用你自己的身份打开 /root/f 失败,sudo echo 根本没轮到执行。

三种正解:

sudo sh -c 'echo hello > /root/f'            # 让 root 的 shell 去做重定向
echo hello | sudo tee /root/f                # tee 以 root 打开文件
sudo dd of=/root/f <<< hello 2>/dev/null     # 少见但可行

同理,sudo cat /etc/shadow > shadow.bak 也拿不到内容——重定向发生在当前身份下。

六、动手清单

ls /etc /nope > a.txt 2>&1; wc -l a.txt        # 1. 顺序正确的合并
ls /etc /nope 2>&1 > b.txt; wc -l b.txt        # 2. 顺序错误的对照(stderr 去了终端)
set -o pipefail; false | true; echo $?         # 3. 管道失败可见
yes | head -3                                  # 4. 并发管道与 SIGPIPE
printf 'x\n' | tee c.txt | wc -l               # 5. tee 分支
diff <(seq 3) <(seq 4)                         # 6. 进程替换
sudo sh -c 'echo ok > /tmp/rootfile'; ls -l /tmp/rootfile   # 7. root 侧重定向

小结

  • 重定向 = 在 exec 前用 dup2 改掉 fd 1/2,由 shell 执行——所以 2>&1 的位置决定结果,sudo cmd > f 也救不了权限。
  • > 是打开并截断;noclobber 可以禁止覆盖,>| 强制覆盖。
  • 管道是并发的两根管子;$? 只反映最后一个命令,set -o pipefail 才看整条链。
  • tee 用于”一边看一边存”,<(...) 把命令输出当文件,<<'EOF' 写模板时长这样用。

下一节预告

下一节:进程与信号——ps 的状态码怎么读、TERM 与 KILL 的根本差别、前台/后台与 Ctrl+Z 的真实信号、僵尸与孤儿进程,以及”为什么关掉终端进程就死了”。

执笔 手写

喜欢的话,留下你的评论吧~

加载中...