管道与重定向
Linux 运维基础 · 第八章 目标:理解 Linux I/O 模型,掌握管道串联和重定向的完整用法
1. 概述#
Linux 的哲学是"小工具组合完成大任务"。管道(|)和重定向(> / >> / <)正是实现这种组合的核心机制。
如果说单个命令是"零件",管道就是把零件串成流水线的传送带,重定向则决定了数据从哪来、到哪去。
2. stdin / stdout / stderr#
2.1 三个标准流#
Linux 下每个进程启动时,系统自动打开三个文件描述符:
| 文件描述符 | 名称 | 缩写 | 含义 | 默认 |
|---|---|---|---|---|
| 0 | stdin | 标准输入 | 进程读取数据的来源 | 键盘输入 |
| 1 | stdout | 标准输出 | 进程正常输出的去处 | 终端屏幕 |
| 2 | stderr | 标准错误 | 进程错误输出的去处 | 终端屏幕(和 stdout 分开) |
2.2 为什么要分 stdout 和 stderr#
bash
# 假设一个命令输出正常结果 + 警告
./process.sh
# Processing file1... Done
# Processing file2... Done
# WARNING: skipping file3 (permission denied) ← 这是 stderr
# Processing file4... Done
# 如果 stdout 和 stderr 混在一起:
# - 想把输出结果重定向到文件保存 → 警告也进去了,污染数据
# - 想把警告单独收集 → 需要分开处理
# 结论:stdout 是"产出",stderr 是"诊断",分开处理才合理2.3 查看区分#
bash
# 正常情况下,两者都打印到终端
ls /tmp /nonexist
# /tmp: ← stdout
# file1 file2
# ls: /nonexist: No such file or directory ← stderr
# 把 stdout 重定向后,stderr 仍然显示在终端
ls /tmp /nonexist > /dev/null
# ls: /nonexist: No such file or directory ← 只有 stderr 了3. 重定向#
3.1 输出重定向#
stdout 重定向#
bash
# > 覆盖写入
ls -la > file_list.txt # 覆盖(文件不存在则创建)
echo "new line" > file.txt # 每次执行都覆盖原有内容
# >> 追加写入
echo "another line" >> file.txt # 追加到文件末尾stderr 重定向#
bash
# 2> 只重定向 stderr
./script.sh 2> error.log # stderr 进文件,stdout 照常显示
# 只保存错误信息,忽略正常输出
./script.sh 2> error.log 1>/dev/null # stdout 丢弃,stderr 保存合并重定向#
bash
# 把 stderr 合并到 stdout
./script.sh > output.log 2>&1 # stdout 和 stderr 都进 output.log
# 解释:2>&1 意思是"把文件描述符 2 重定向到文件描述符 1 当前指向的地方"
# 全部重定向(bash 简写)
./script.sh &> all_output.log # bash 专有简写(非 POSIX sh;追加形式 &>> 才需 bash 4.0+)
./script.sh >& all_output.log # 另一种写法
# 追加全部
./script.sh &>> all_output.log
./script.sh >> all_output.log 2>&1 # 等价⚠️ 顺序陷阱:2>&1 必须放在 > file 之后#
重定向是从左到右按顺序生效的,2>&1 复制的是"此刻 fd 1 指向哪",不是"最终指向哪"。
bash
# ✅ 对:先把 stdout 指向文件,再让 stderr 跟着 stdout(一起进文件)
./script.sh > out.log 2>&1
# ❌ 错:stderr 先复制了“此刻的 stdout(还是终端)”,之后 stdout 才改指向文件
./script.sh 2>&1 > out.log
# 结果:stderr 打到终端,只有 stdout 进了文件 —— 常见踩坑,日志里死活看不到报错记忆:> file 在前,2>&1 在后;顺序反了就等于没合并。
3.2 常用的"静默"写法#
bash
# 丢弃 stdout 和 stderr
./script.sh > /dev/null 2>&1
./script.sh &> /dev/null # 等效简写
# cron 任务常用写法:只输出错误
./script.sh > /dev/null # 成功不输出,失败才报
# 检查命令是否成功(不关心输出)
if grep -q "pattern" file.txt 2>/dev/null; then
echo "Found"
fi
# -q = quiet,配合 2>/dev/null 抑制一切输出3.3 输入重定向#
bash
# < 从文件读取输入
mysql < schema.sql # 把 .sql 文件作为 mysql 的输入
# << Here Document(脚本中嵌入多行文本)
cat << EOF > config.yaml
server:
host: 0.0.0.0
port: 8080
EOF
# <<< Here String(单行输入)
grep "error" <<< "This is an error message"4. 管道 |#
4.1 管道的本质#
管道把前一个命令的 stdout 接到后一个命令的 stdin。
bash
command1 | command2 | command3
# ↑ stdout → stdin ↑ stdout → stdin
# 注意:管道只传递 stdout,stderr 不经过管道!
# 所以下面这样是不行的:
cat error.log | grep "error" # 即使 stderr 有内容也不经过管道
# 正确做法:把 stderr 重定向到 stdout 再管道
./script.sh 2>&1 | grep "error"4.2 经典管道组合#
bash
# 统计文件数
ls | wc -l
# 找进程
ps aux | grep nginx
# 查看端口监听
ss -tlnp | grep 8080
# 找大文件
du -sh /* 2>/dev/null | sort -rh | head -10
# 找 Top10 访问 IP(运维经典)
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
# 看磁盘使用率(排除 tmpfs)
df -h | grep -v tmpfs
# 实时监控特定服务日志
tail -f /var/log/syslog | grep -i "error\|timeout"4.3 管道的退出码陷阱(pipefail)#
一条管道的退出码,默认只取最后一个命令的退出码——中间命令失败会被"吃掉",脚本里极易漏判错误。
bash
# 前一个命令失败,但整条管道仍然“成功”(因为 sort 成功了)
false | sort
echo $? # 0 —— 明明 false 失败了!
# 真实场景:curl 挂了,但因为 grep 成功,脚本以为一切正常
curl http://api/xxx | grep "ok"
echo $? # 只反映 grep,curl 的失败被掩盖两种解法:
bash
# 解法 1:set -o pipefail —— 管道里任一命令失败,整条就算失败(脚本首选)
set -o pipefail
false | sort; echo $? # 1 —— 现在能感知到了
# 生产脚本常见开头:set -euo pipefail
# 解法 2:查 $PIPESTATUS 数组,逐段看每个命令的退出码(bash)
false | sort | cat
echo "${PIPESTATUS[@]}" # 1 0 0 —— 第一个命令返回 1
set -e(遇错即停)单独用时,管道中间命令的失败不会触发退出,必须配pipefail才完整。
5. tee — 同时输出到文件和屏幕#
5.1 为什么需要 tee#
管道中,数据是"单向流动"的。你无法在管道的中间节点看到数据。tee 在中间的分支上"偷看一眼"。
bash
# 不使用 tee:输出要么进文件,要么显示在屏幕,不能同时
./script.sh > output.log # 只进文件,屏幕没输出
# 使用 tee:既看输出又保存
./script.sh | tee output.log # 屏幕有输出,同时写入文件
# tee -a 追加
./script.sh | tee -a output.log # 追加不覆盖5.2 典型场景#
bash
# 场景 1:运行长时间脚本,想看着进度又怕 SSH 断线后丢输出
./long_running.sh 2>&1 | tee build.log
# 场景 2:调试管道——在中间看看数据长什么样
cat access.log | awk '{print $1}' | tee ips.txt | sort | uniq -c | sort -rn | head
# 中间把 IP 也保存到 ips.txt
# 场景 3:同时输出到多个文件
./script.sh | tee file1.log file2.log file3.log6. xargs — 把管道输出变成命令参数#
6.1 什么时候需要 xargs#
有些命令不接受管道输入(参数 ≠ stdin)。比如 rm、cp、mkdir、echo、kill。
bash
# 错误:rm 不接受 stdin
echo "old_file.txt" | rm
# rm: missing operand ← rm 等的是参数,不是 stdin
# 正确:用 xargs 把 stdin 转换成命令行参数
echo "old_file.txt" | xargs rm6.2 核心用法#
bash
# 查找并删除
find . -name "*.tmp" | xargs rm -f
# 查找并查看文件大小
find . -name "*.log" | xargs ls -lh
# 查找并打包
find . -name "*.conf" | xargs tar czf configs.tar.gz
# 批量创建目录
echo "dir1 dir2 dir3" | xargs mkdir -p
# 批量 kill 进程
ps aux | grep "sleep" | grep -v grep | awk '{print $2}' | xargs kill6.3 处理特殊文件名(空格、换行)#
bash
# 隐患:文件名含空格会导致 xargs 误判
touch "my file.txt"
find . -name "my file.txt" | xargs rm
# rm: cannot remove 'my': No such file or directory
# rm: cannot remove 'file.txt': No such file or directory
# 解法 1:find 的 -print0 + xargs -0(推荐)
find . -name "*.tmp" -print0 | xargs -0 rm -f
# 解法 2:find ... -exec(更安全)
find . -name "*.tmp" -exec rm -f {} \;6.4 xargs 高级参数#
bash
# -n:每次传递 N 个参数
echo "1 2 3 4 5 6" | xargs -n 2 echo
# 1 2
# 3 4
# 5 6
# -I:自定义占位符
find . -name "*.conf" | xargs -I {} cp {} /backup/{}
# 把 {} 替换成找到的文件名
# -p:执行前确认(安全)
find . -name "*.tmp" | xargs -p rm -f
# rm -f ./a.tmp ./b.tmp?... → 按 y 确认,n 取消7. 实战场景#
实战 1:脚本输出全部进日志#
bash
#!/bin/bash
# 所有输出(含错误)都写入日志文件
LOG="/var/log/myapp/deploy_$(date +%Y%m%d_%H%M%S).log"
# 方式 1:在脚本开头重定向
exec > >(tee -a "$LOG") 2>&1
echo "部署开始..."
./install.sh
echo "部署完成"
# 方式 2:运行时重定向
./deploy.sh > /var/log/deploy.log 2>&1实战 2:find + xargs 批量操作#
bash
# 删 30 天前的日志
find /var/log/myapp -name "*.log" -mtime +30 -print0 | xargs -0 rm -f
# 将 7 天前的日志压缩归档
find /var/log/myapp -name "*.log" -mtime +7 | xargs gzip
# 批量修改文件权限
find /var/www -type f -print0 | xargs -0 chmod 644实战 3:管道跨机器备份#
bash
# 不落磁盘,直接通过网络备份到远程机器
tar czf - /data | ssh user@backup-server "cat > /backup/data_$(date +%Y%m%d).tar.gz"
# ^ tar 的 -f - 表示输出到 stdout(不写文件)
# ^ cat > 接收 stdin 写入文件
# 跨机器同步目录(rsync 更好,但这展示了管道的灵活性)
ssh source-server "tar czf - /app/data" | tar xzf - -C /local/restore/8. 调试技巧#
bash
# 管道出问题时,分段测试
# 不要一次性写完整条管道再试
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
# 而是逐步构建
cat access.log | awk '{print $1}' | head # 第 1 步:列对吗?
cat access.log | awk '{print $1}' | sort | head # 第 2 步:排序对吗?
# 逐级验证,问题一目了然
# 在管道中间用 tee 看中间结果
cat access.log | awk '{print $1}' | tee /tmp/ips.txt | sort | uniq -c
# 检查 /tmp/ips.txt 确认 IP 提取是否正确9. 小结#
| 知识点 | 一句话记忆 |
|---|---|
| 三个文件描述符 | 0=stdin, 1=stdout, 2=stderr |
> vs >> | > 覆盖,>> 追加 |
2>&1 | 把 stderr 合并到 stdout(顺序重要:>file 2>&1 对,2>&1 >file 错) |
| 管道 | 前一个 stdout → 后一个 stdin,stderr 不经过管道 |
| 管道退出码 | 默认只看最后一个命令;脚本用 set -o pipefail 才不漏错 |
tee | 同时输出到屏幕和文件,管道中间"偷看" |
xargs | 把 stdin 转成命令参数,配合 -print0 -0 处理空格 |