19 - IO 重定向与管道深入

重定向和管道是 Linux 哲学”小工具组合”的核心机制。理解标准流、重定向操作符和管道缓冲,能让你编写出灵活高效的命令行流水线。


19.1 标准流概述

Linux 每个进程默认打开三个文件描述符:

文件描述符名称缩写默认目标惯常用途
0标准输入stdin键盘接收数据
1标准输出stdout终端输出正常结果
2标准错误stderr终端输出错误/诊断信息
# 验证三个文件描述符
echo "这是标准输出"
echo "这是标准错误" >&2
 
# 通过 /proc 查看进程的文件描述符
ls -la /proc/$$/fd/
# 输出示例:
# 0 -> /dev/pts/0 (stdin)
# 1 -> /dev/pts/0 (stdout)
# 2 -> /dev/pts/0 (stderr)
# 255 -> /dev/pts/0
 
# $0 不是标准流!$0 是脚本文件名(Shell 特殊变量),与文件描述符无关

19.2 输出重定向

基本操作符

# 重定向 stdout 到文件(覆盖)
command > file
command 1> file # 1> 等同于 >
 
# 重定向 stdout 到文件(追加)
command >> file
command 1>> file
 
# 重定向 stderr 到文件
command 2> error.log
command 2>> error.log # 追加模式
 
# 分别重定向 stdout 和 stderr 到不同文件
command > output.log 2> error.log
command >> output.log 2>> error.log # 追加
 
# 合并重定向 — stdout 和 stderr 写入同一个文件
command &> all.log # Bash 特有
command > all.log 2>&1 # POSIX 方式(推荐,更可移植)
 
# 追加模式合并
command &>> all.log # Bash
command >> all.log 2>&1 # POSIX
 
# 只重定向 stderr 到 stdout
command 2>&1 # stderr 合并到 stdout (当前 stdout 指向)
command 2>&1 > /dev/null # stderr 合并到原 stdout,然后 stdout 到 /dev/null
 # 结果: stdout 丢弃, stderr 显示在终端

重定向顺序陷阱

# 重定向顺序很重要!从左到右解释
 
# 正确:将 stderr 定向到当前 stdout 所指向的位置,然后 stdout 重定向到文件
command > file 2>&1 # stdout 到 file,stderr 到 file 
 
# 错误:
command 2>&1 > file # stderr 到终端(stdout 原始位置),stdout 到 file
 # 结果:stderr 仍在终端,只有 stdout 写入 file 
 
# 可视化理解:
# 初始: stdout → 终端, stderr → 终端
# 执行 2>&1 后: stderr → 终端 (stdout 的当前指向)
# 执行 > file 后: stdout → file, stderr 仍指向终端(不受后来 stdout 改变的影响)

|& — 管道中的 stderr

# |& 将 stdout 和 stderr 一起传给管道(Bash 4.0+)
command |& other_command
# 等价于:
command 2>&1 | other_command
 
# 示例:搜索包含错误信息的输出
make |& grep -E "error|warning"
 
# 只将 stderr 传给管道
# 方案一:先交换 stderr 和 stdout
command 3>&1 1>&2 2>&3 | grep "error"
# 方案二:进程替换
command 2> >(grep "error" > error.log)

19.3 输入重定向

# 从文件读取输入
command < file
command 0< file
 
# 标准输入到变量(通过管道或重定向)
# 方式一:Here String
read -r var <<< "hello world"
 
# 方式二:重定向往脚本传递
wc -l < /etc/passwd # 只输出行数(不输出文件名)
 
# 方式三:多条命令共享输入
{
 read -r line1
 read -r line2
 echo "$line1 | $line2"
} < file.txt
 
# 方式四:在脚本中内嵌数据
while read -r line; do
 echo "$line"
done << 'EOF'
这是内嵌数据第一行
这是内嵌数据第二行
EOF
 
# 方式五:用 exec 永久重定向
exec < input.txt # 此后所有 read 从 input.txt 读取
while read -r line; do
 echo "$line"
done
exec < /dev/tty # 恢复从终端读取

19.4 高级重定向技巧

自定义文件描述符(3-9)

# 打开文件描述符进行读写
exec 3> output3.txt # 打开 fd 3 写入
exec 4< input4.txt # 打开 fd 4 读取
exec 5<> fifo_pipe # 打开 fd 5 读写(常用于 FIFO)
 
# 写入自定义 fd
echo "写到 fd 3" >&3
echo "标准输出" >&1
 
# 从自定义 fd 读取
read -r line <&4
 
# 关闭自定义 fd
exec 3>&- # 关闭 fd 3
exec 4<&- # 关闭 fd 4
 
# 重定向到多个输出
echo "同时输出到终端和文件" | tee /tmp/output.txt
 
# 使用 fd 保存原始 stdout 以便恢复
exec 3>&1 # 保存 stdout 到 fd 3
exec > /tmp/output.txt # stdout 重定向到文件
echo "这行写入文件"
exec 1>&3 # 恢复 stdout
exec 3>&- # 关闭 fd 3
echo "这行回到终端"

批量应用重定向

# 在代码块中应用重定向
{
 echo "第一行"
 echo "第二行"
 date
 whoami
} > combined_output.txt
 
# 函数输出重定向
myfunc() {
 echo "函数输出1"
 echo "函数输出2" >&2
 echo "函数输出3"
}
myfunc > stdout.log 2> stderr.log
 
# for 循环重定向
for i in {1..5}; do
 echo "Line $i"
done > loop_output.txt
 
# 条件块重定向
if grep -q "root" /etc/passwd; then
 echo "root 用户存在"
else
 echo "root 用户不存在"
fi > check_result.txt

19.5 管道机制

管道基础

# 管道将 stdout 连接到一个命令的 stdin
command1 | command2 | command3
 
# 示例
cat /var/log/pacman.log | grep "installed" | sort | uniq -c | sort -rn
ps aux | grep nginx | awk '{print $2}' | xargs kill -HUP
 
# 管道缓冲
# 管道有缓冲区(通常 64KB)
# 产生者写满缓冲区 → 阻塞 → 消费者读取 → 空间释放
# 这实现了自然的背压(backpressure)机制
 
# 观察管道缓冲行为
(while true; do echo "data"; done) | (sleep 1; head -3)
 
# SIGPIPE 信号
# 当管道读取端关闭而写入端继续写入时,写入端收到 SIGPIPE
# 默认行为:进程终止
# 示例:
yes | head -5 # yes 在 head 退出后收到 SIGPIPE 终止
 
# 查看管道中的退出码
false | true
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}" # 1 0
# PIPESTATUS 是 Bash 数组,保存管道中每个命令的退出码
 
# 配合 set -o pipefail
set -o pipefail
false | true
echo "$?" # 1 (非零,因为有命令失败)

管道实战模式

# 模式 1:多级过滤链
ps aux | grep -v grep | grep "python" | awk '{print $2, $11}'
 
# 模式 2:产生 → 排序 → 去重 → 计数
find . -type f -name "*.*" | sed 's/.*\.//' | sort | uniq -c | sort -rn
 
# 模式 3:控制管道中的子 shell
# 问题:管道右侧在子 shell 中执行,变量修改不会保留
count=0
find . -type f | while read -r file; do
 ((count++))
done
echo "count=$count" # 0!在子 shell 中的修改丢失了
 
# 解决方案 1:进程替换
count=0
while read -r file; do
 ((count++))
done < <(find . -type f)
echo "count=$count" # 正确的值
 
# 解决方案 2:使用命名管道
mkfifo /tmp/count_pipe
find . -type f > /tmp/count_pipe &
while read -r file; do
 ((count++))
done < /tmp/count_pipe
rm /tmp/count_pipe
 
# 解决方案 3:lastpipe 选项(Bash 4.2+)
shopt -s lastpipe # 管道最后一个命令在当前 shell 中执行
count=0
find . -type f | while read -r file; do
 ((count++))
done
echo "count=$count" # 正确!

19.6 tee — 分流输出

# 基本用法:同时输出到文件和终端
echo "hello" | tee output.txt
 
# 追加模式
echo "world" | tee -a output.txt
 
# 写入多个文件
echo "data" | tee file1.txt file2.txt file3.txt
 
# 配合管道继续传递
./long_running_script 2>&1 | tee full.log | grep -E "ERROR|WARN"
 
# 需要 sudo 权限写入文件时
echo "config_line" | sudo tee /etc/some_config.conf
echo "config_line2" | sudo tee -a /etc/some_config.conf
 
# 丢弃 stdout,只写文件(模拟正常重定向)
echo "data" | tee file.txt > /dev/null
 
# 使用进程替换(Bash)分流到多个命令
echo "data" | tee >(gzip > data.gz) >(wc -c > size.txt) > /dev/null
 
# 实用模式:调试管道
# 在管道的任意位置插入 tee 查看中间结果
command1 | tee /dev/stderr | command2 | tee /dev/stderr | command3

19.7 xargs — 将标准输入转换为命令行参数

基础用法

# 基本:将输入转为参数
echo "file1 file2 file3" | xargs rm
# 等价于: rm file1 file2 file3
 
# 控制参数放置位置(-I 占位符)
find . -name "*.log" | xargs -I {} mv {} /tmp/logs/{}
find . -name "*.txt" | xargs -I % sh -c 'echo "文件: %"; wc -l %'
 
# 控制每次调用传递的参数数量(-n)
echo {1..10} | tr ' ' '\n' | xargs -n 3 echo
# 输出:
# 1 2 3
# 4 5 6
# 7 8 9
# 10
 
# 并行执行(-P)
echo {1..20} | tr ' ' '\n' | xargs -P 4 -I {} sh -c 'echo "处理 {}"; sleep 2'
 
# 交互确认(-p)
find . -name "*.tmp" | xargs -p rm
# 每个命令执行前询问

处理特殊字符

# 默认情况下 xargs 会解释引号和反斜杠,并按空白分割输入
# 这可能导致文件名中的空格和特殊字符造成问题
 
# 使用 -0(空字符分隔,配合 find -print0)
find . -name "*.log" -print0 | xargs -0 rm
find . -type f -print0 | xargs -0 grep "pattern"
 
# 处理包含空格的文件名
find . -name "*.txt" -print0 | xargs -0 -I {} cp {} /backup/
 
# 限制最大参数长度(-s)
find /usr -name "*.so" -print0 | xargs -0 -s 65536 ls -l

xargs 实战例子

# 批量重命名文件
ls *.jpg | xargs -I {} mv {} {}.bak
 
# 并行压缩多个文件
find . -name "*.log" -print0 | xargs -0 -P 4 -I {} gzip {}
 
# 批量下载(配合 curl 和文件中的 URL 列表)
xargs -n 1 -P 8 curl -O < urls.txt
 
# 批量删除 Docker 容器
docker ps -a -q | xargs docker rm
 
# 批量 kill 进程
ps aux | grep "worker" | grep -v grep | awk '{print $2}' | xargs kill -TERM
 
# 安全批量操作(先 dry-run)
find . -name "*.tmp" | xargs echo rm # 回显将执行的命令
find . -name "*.tmp" -print0 | xargs -0 rm # 确认无误后再实际执行
 
# 统计目录下所有文件的行数
find . -name "*.py" -print0 | xargs -0 wc -l | tail -1

19.8 特殊设备文件

# /dev/null — 数据黑洞(丢弃所有写入,读取返回 EOF)
command > /dev/null 2>&1 # 完全静默
grep -q "pattern" file # 无需输出时用 -q
large_process > /dev/null # 丢弃输出
 
# /dev/zero — 无限零字节流
dd if=/dev/zero of=empty.img bs=1M count=100 # 创建 100MB 空文件
cat /dev/zero > /dev/null & # 消耗 CPU(无 I/O 等待)
 
# /dev/random — 真随机数(熵池不足时阻塞)
head -c 32 /dev/random | base64 # 生成随机密钥
 
# /dev/urandom — 伪随机数(不阻塞,推荐)
head -c 32 /dev/urandom | base64
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 # 随机密码
dd if=/dev/urandom of=random_data.bin bs=1K count=1
 
# /dev/full — 始终返回"磁盘已满"错误(测试用)
echo "test" > /dev/full
echo "$?" # 1(写入失败)
 
# /dev/null 的重定向技巧
# 静默输出
command &> /dev/null
 
# 创建空文件
> file.txt # 清空或创建
: > file.txt # 同上(: 是空命令)
 
# 丢弃 stderr
command 2>/dev/null
 
# 测试命令退出码但不显示输出
if command &>/dev/null; then
 echo "成功"
fi

19.9 命名管道(FIFO)深入

# 创建命名管道
mkfifo /tmp/data_pipe
# 或指定权限
mkfifo -m 600 /tmp/private_pipe
 
# 命名管道 vs 匿名管道
# 匿名管道 |: 只能连接父子进程或兄弟进程,生命周期与进程绑定
# 命名管道: 任意两个进程可以使用,以文件系统节点形式存在
 
# 基本用法 —— 必须同时有读方和写方,否则阻塞
# 终端 1:
mkfifo /tmp/myfifo
cat < /tmp/myfifo # 阻塞,等待写入
 
# 终端 2: 
echo "Hello Pipe!" > /tmp/myfifo
 
# 防止阻塞的技巧
# 读方超时
timeout 5 cat < /tmp/myfifo || echo "超时"
# 写方使用后台
echo "data" > /tmp/myfifo &
 
# 应用场景 1:进程间单向通信
mkfifo /tmp/trigger
# 守护进程
while read -r cmd < /tmp/trigger; do
 echo "收到命令: $cmd"
done
# 控制端
echo "restart" > /tmp/trigger
 
# 应用场景 2:并行处理的扇出
mkfifo /tmp/pipe{1,2,3,4}
for i in 1 2 3 4; do
 grep "ERROR" /tmp/pipe$i > "errors_$i.log" &
done
tee /tmp/pipe{1,2,3,4} < /var/log/app.log
rm /tmp/pipe{1,2,3,4}
 
# 应用场景 3:netcat 配合 FIFO 做双向通信
mkfifo /tmp/nc_fifo
nc -l -p 1234 < /tmp/nc_fifo | while read -r msg; do
 echo "收到: $msg"
 echo "回复: 已收到" > /tmp/nc_fifo
done

19.10 /proc/self/fd/ 文件描述符

# /proc/PID/fd/ 目录包含进程所有打开的文件描述符
ls -l /proc/$$/fd/
# lrwx------ 0 -> /dev/pts/0
# lrwx------ 1 -> /dev/pts/0
# lrwx------ 2 -> /dev/pts/0
# lr-x------ 255 -> /path/to/script.sh
 
# /dev/fd/N 等价于 /proc/self/fd/N
# /dev/stdin = /dev/fd/0 = /proc/self/fd/0
# /dev/stdout = /dev/fd/1 = /proc/self/fd/1
# /dev/stderr = /dev/fd/2 = /proc/self/fd/2
 
# 实用技巧
# 1. 让支持"从文件读取"的工具从 stdin 读取
echo "hello" | diff - /dev/stdin # diff 比较 stdin 和文件
 
# 2. 在管道中使用需要文件参数的命令
some_command | while read -r line; do
 echo "$line"
done
 
# 3. 查看进程打开了哪些文件(调试用)
ls -l /proc/$(pidof nginx | tr ' ' '\n' | head -1)/fd/
# 查看监听的 socket
ls -l /proc/$(pidof nginx | awk '{print $1}')/fd/ | grep socket
 
# 4. 恢复被删除但仍被进程打开的文件
# 假设某日志文件被误删但进程仍在写入:
# 1) 找到进程打开的 fd
ls -l /proc/PID/fd/ | grep deleted_file
# 2) 恢复文件内容
cat /proc/PID/fd/3 > recovered_file

19.11 实用模式集合

模式 1:同时记录到终端和文件

# 全部输出(含 stderr)
./script 2>&1 | tee script.log
 
# 只 stdout 到文件,stderr 到终端
./script > script.log
 
# stdout 和 stderr 分别记录
./script > stdout.log 2> stderr.log
 
# stdout 到终端+文件,stderr 到独立文件
./script 2> error.log | tee output.log

模式 2:多级过滤流水线

# 分析日志:提取特定时间段、特定级别的日志
grep "2026-07-24" /var/log/app.log \
 | grep -E "ERROR|FATAL" \
 | awk '{print $1, $2, $NF}' \
 | sort | uniq -c | sort -rn \
 | tee analysis.txt
 
# 直接修改:格式化 JSON 日志
tail -f /var/log/app.log \
 | grep -E '^{.*}$' \
 | jq '.message' \
 | tee -a formatted.log

模式 3:条件执行与重定向

# 只有成功时才输出
command > result.txt && cat result.txt
 
# 失败时输出错误
command 2> error.tmp && rm error.tmp || cat error.tmp
 
# 条件日志
if command &>/dev/null; then
 echo "[OK] command 执行成功" | tee -a status.log
else
 echo "[FAIL] command 执行失败" | tee -a status.log >&2
fi

模式 4:批量文件操作

# 查找并处理
find . -type f -name "*.md" -print0 \
 | xargs -0 grep -l "TODO" \
 | xargs -0 sed -i 's/TODO/FIXME/g'
 
# 批量压缩
find /var/log -name "*.log" -mtime +7 -print0 \
 | xargs -0 -P 4 -I {} gzip {}
 
# 批量修改权限
find /var/www -type d -print0 | xargs -0 chmod 755
find /var/www -type f -print0 | xargs -0 chmod 644

模式 5:子 shell 与重定向

# 将一组命令的输出整体重定向
(
 echo "=== 报告开始 ==="
 date
 echo ""
 echo "--- 磁盘使用 ---"
 df -h
 echo ""
 echo "--- 内存使用 ---"
 free -h
 echo ""
 echo "=== 报告结束 ==="
) > system_report.txt 2>&1
 
# 切换工作目录执行命令
(cd /tmp && tar czf /backup/archive.tar.gz .)

19.12 重定向速查表

操作符含义示例
cmd > filestdout 覆盖写入 fileecho "hi" > out.txt
cmd >> filestdout 追加到 fileecho "hi" >> out.txt
cmd < file从 file 读取 stdinwc -l < data.txt
cmd 2> filestderr 覆盖写入 filecmd 2> err.log
cmd 2>> filestderr 追加到 filecmd 2>> err.log
cmd &> filestdout + stderr 写入 filecmd &> all.log
cmd > file 2>&1stdout + stderr 写入 file(POSIX)cmd > all.log 2>&1
cmd >> file 2>&1stdout + stderr 追加到 filecmd >> all.log 2>&1
cmd1 | cmd2cmd1 stdout → cmd2 stdinps aux | grep nginx
cmd1 |& cmd2cmd1 stdout + stderr → cmd2 stdinmake |& grep error
cmd | tee filestdout 写入 file 并传至管道cmd | tee log.txt
xargs cmdstdin → cmd 的参数find . | xargs rm
exec N> file打开 fd N 写入 fileexec 3> data.txt
cmd N>&-关闭 fd Nexec 3>&-
cmd > /dev/null丢弃 stdoutcmd > /dev/null
cmd <<< "string"Here String 作为 stdinread x <<< "hello"
cmd << EOF ... EOFHere Document 多行 stdincat << EOF

延伸阅读: 18-Bash编程基础 讲解 Shell 变量、循环等基础语法。20-正则与文本处理三剑客 展示 grep/sed/awk 在管道中的实战应用。19-Bash编程进阶 涵盖进程替换 <( )>( ) 等高级用法。