1. 为什么“高级重定向”不是语法糖,而是Shell工程能力的分水岭
很多人学完>和>>就以为重定向学完了,直到某天在写一个日志聚合脚本时发现:明明加了2>&1,错误信息还是没进到文件里;或者用curl下载大文件时,进度条和响应体混在一起,根本没法用grep过滤;又或者调试一个后台服务启动脚本,想把所有输出都存档,结果nohup ./start.sh > all.log 2>&1 &跑起来后,all.log里空空如也——不是没输出,是输出被缓冲区卡住了。这些都不是bug,而是对重定向底层机制理解断层的必然结果。
“高级重定向”这个词本身就有误导性。它不是“基础重定向的升级版”,而是Shell进程与操作系统I/O子系统之间真实交互的显性化表达。当你敲下command > file 2>&1,你不是在给命令“加个参数”,而是在向内核发起三次明确的系统调用:一次open()打开文件,两次dup2()重绑定文件描述符0(stdin)、1(stdout)、2(stderr)的指向。这个过程完全绕过C标准库的printf缓冲逻辑,直抵POSIX I/O原语层。
我见过太多人把重定向当成黑盒魔术来记:2>&1必须写在>后面、&>能合并输出、|&是管道里的错误转发……这些口诀背得滚瓜烂熟,但一旦遇到exec 3>log.txt; echo "init" >&3; command 1>&3 2>&3这种结构就彻底懵——因为没搞懂exec在这里不是执行命令,而是在当前shell进程中永久性地打开并绑定一个新的文件描述符3,后续所有>&3都是对这个已存在句柄的复用,而非重新打开文件。这种认知差异,直接决定了你是在写可维护的运维脚本,还是在写三天后自己都看不懂的“一次性胶带代码”。
关键词“shell 进阶”背后的真实诉求,从来不是学会更多符号组合,而是建立一套可预测、可调试、可审计的I/O流控制模型。它解决的是:如何让程序输出像自来水一样按需分流,而不是像暴雨一样无序泛滥;如何在不修改源码的前提下,精确捕获任意层级子进程的输出;如何让日志既满足人类可读性,又便于机器解析;甚至在资源受限的嵌入式环境里,用/dev/null做精准的流量黑洞。这些都不是“技巧”,而是Linux系统编程的基本功在Shell层面的投射。
所以这篇内容不叫“高级重定向教程”,它是一份Shell I/O流控制的实战手册。我们不会罗列所有重定向符号,而是聚焦四个最常踩坑、最易被误解、也最具工程价值的核心场景:文件描述符的显式管理、多路输出的原子分流、缓冲区行为的主动干预,以及子shell环境中的重定向继承机制。每一步操作,我都将还原其对应的系统调用痕迹,并给出可验证的实测案例——因为真正的进阶,始于你能用strace看到自己写的重定向到底干了什么。
2. 文件描述符:别再把“0/1/2”当魔法数字,它们是进程的I/O身份证
几乎所有Shell教程都告诉你:“0是标准输入,1是标准输出,2是标准错误”。这没错,但错在只告诉你“是什么”,却从不解释“为什么是它们”。当你真正需要创建第3、第4甚至第10个文件描述符时,这个认知盲区就会变成致命陷阱。
2.1 文件描述符的本质:内核维护的进程级指针数组
在Linux中,每个进程启动时,内核都会为其分配一张文件描述符表(file descriptor table),它本质上是一个整数索引的指针数组。索引0、1、2被内核强制预留给stdin/stdout/stderr,这是POSIX标准规定的“惯例”,而非内核硬编码。你可以用ls -l /proc/$$/fd/实时查看当前shell进程的这张表:
$ ls -l /proc/$$/fd/ total 0 lr-x------ 1 user user 64 Jun 15 10:23 0 -> '/dev/pts/2' l-wx------ 1 user user 64 Jun 15 10:23 1 -> '/dev/pts/2' l-wx------ 1 user user 64 Jun 15 10:23 2 -> '/dev/pts/2'这里$$是当前shell的PID。你会发现0、1、2都指向同一个终端设备/dev/pts/2,这正是交互式shell的默认状态:输入从终端来,输出和错误都打到终端去。但关键点在于——这张表是进程私有的,且长度可动态扩展。当你执行exec 3>log.txt,内核就在该进程的文件描述符表里找第一个空闲索引(通常是3),然后调用open("log.txt", O_WRONLY|O_CREAT|O_TRUNC)拿到一个内核文件表项(file table entry),再把这个表项的指针存进索引3的位置。
提示:
exec命令的特殊性在于,它不启动新进程,而是在当前shell进程上下文中直接修改文件描述符表。这是实现“持久化重定向”的唯一途径,也是很多脚本失效的根本原因——忘了加exec,重定向只在单条命令生效。
2.2 显式管理FD:从“临时覆盖”到“长期持有”
基础重定向command > file的问题在于:它只对command这个子进程生效,父shell的FD表完全不受影响。而exec则不同。看这个经典对比:
# 场景:需要持续记录整个脚本的运行日志 # ❌ 错误做法:每条命令都重定向(冗余且易漏) date > log.txt ls -la >> log.txt df -h >> log.txt # ✅ 正确做法:用exec建立全局FD绑定 exec 3>log.txt # 打开log.txt,绑定到FD 3 echo "$(date): Script started" >&3 ls -la >&3 df -h >&3 exec 3>&- # 关闭FD 3(&-是关闭语法)这里exec 3>log.txt执行后,当前shell进程的FD 3就永久指向log.txt。后续所有>&3都是对该FD的写入,无需重复打开文件。更重要的是,exec还支持FD复制与重绑定,这是实现复杂分流的基础:
# 将FD 1(stdout)的内容同时发给两个地方:终端 + 日志文件 exec 3>&1 # 复制FD 1的指向,存到FD 3(现在3和1都指向终端) exec 1>log.txt # 把FD 1重定向到log.txt(此时1指向文件,3仍指向终端) echo "This goes to log.txt only" echo "This also goes to log.txt" >&3 # 显式写入FD 3 → 终端 exec 1>&3 3>&- # 恢复FD 1指向终端,关闭FD 3这个操作序列的精妙之处在于:它没有使用任何外部工具(如tee),纯粹靠内核的FD操作就实现了“一写双发”。>&3之所以能输出到终端,是因为exec 3>&1在重定向前就“冻结”了终端的句柄。这比tee更轻量,且避免了管道带来的额外进程开销。
2.3 FD冲突排查:当ls: cannot access 'file': No such file其实是FD被占用了
一个极易被忽略的坑:文件描述符数量是有限的。Linux默认每个进程最多打开1024个FD(可通过ulimit -n查看)。当你在循环中频繁执行exec 3>temp.txt却不关闭,很快就会触发Too many open files错误。更隐蔽的是FD泄漏导致的“假性文件不存在”:
# 某个脚本里有这段代码 for i in {1..100}; do exec 3>"/tmp/output_$i.txt" # 每次都开新FD,但没关 echo "data $i" >&3 done # 后续执行 ls /tmp/output_*.txt 时可能报错问题不在ls命令,而在你的脚本耗尽了可用FD,导致ls自身无法打开目录文件进行遍历。排查方法很简单:
# 在出问题的shell中执行 lsof -p $$ | wc -l # 查看当前shell打开了多少FD ls -l /proc/$$/fd/ | wc -l # 同上,更轻量如果数字接近1024,基本就是FD泄漏。修复方案必须包含显式关闭:exec 3>&-。记住:exec FD>file打开FD,exec FD>&-关闭FD,二者必须成对出现,如同malloc/free。
我在某次部署监控脚本时就栽过这个跟头。脚本每分钟拉取一次API数据并写入临时文件,用了exec 3>/tmp/data.$$但忘了关,跑了一周后整个服务器的cron任务集体失联——因为crond进程的FD耗尽,无法再fork新进程。最后用lsof -p $(pgrep crond)定位到几百个悬空的/tmp/data.*文件句柄才解决。这个教训让我养成了写exec必配&-的习惯。
3. 多路输出的原子分流:告别tee的性能损耗与竞态风险
tee命令几乎是所有“同时输出到屏幕和文件”需求的默认答案。但它真的最优吗?我们来拆解它的本质:
# 典型tee用法 command | tee output.log # 实际发生: # 1. shell fork出command进程(FD 1指向管道写端) # 2. shell fork出tee进程(FD 0指向管道读端,FD 1指向终端,FD 3指向output.log) # 3. tee进程内部:read(pipe) → write(stdout) + write(FD3)问题来了:tee引入了额外的进程、额外的内存拷贝、额外的上下文切换。在高吞吐场景下(如实时处理GB级日志流),tee可能成为瓶颈。更严重的是,tee无法保证“原子性”——如果command突然崩溃,tee可能只写入了部分数据到文件,而终端已经显示了全部,造成数据不一致。
3.1 原生FD分流:用>(process)实现零拷贝管道
Bash的进程替换(Process Substitution)>(command)提供了一种更底层的解决方案。它不是创建管道,而是让shell自动创建一个命名管道(FIFO)或/proc/self/fd/下的伪文件,并将该文件路径作为参数传给command。关键在于:这个“文件”对主命令来说就是普通文件,但背后连接着另一个进程。
# 目标:让curl的响应体同时存入文件 + 显示HTTP状态码 # ❌ 低效tee方案 curl -s http://api.example.com/data | tee response.bin | head -c 50 | md5sum # ✅ 高效FD分流方案 curl -s http://api.example.com/data \ > >(tee response.bin | head -c 50 | md5sum > checksum.txt) \ 2> >(grep "HTTP" | awk '{print $2}' > status_code.txt)这里>(...)创建了两个独立的子shell,分别处理stdout和stderr。curl的输出直接写入内核管道缓冲区,由两个子进程并发消费,全程无额外内存拷贝。>(...)的返回值是/dev/fd/63这类路径,curl根本感知不到自己在和进程通信。
但要注意:>(...)创建的子进程是异步的,主命令退出后它们可能还在运行。若需等待,必须用wait:
# 确保所有分流进程完成后再继续 curl -s http://api.example.com/data \ > >(tee response.bin | md5sum > checksum.txt) \ 2> >(grep "HTTP" > status_code.txt) & wait # 等待所有后台作业结束3.2 多目标写入:用cat和FD复制构建“广播中心”
当需要将同一数据流分发到3个以上目的地时,>(...)会变得臃肿。这时可以构建一个基于FD复制的“广播中心”:
# 目标:将日志同时写入:文件A、文件B、syslog、终端 # 步骤1:打开所有目标FD exec 3>file_a.log exec 4>file_b.log exec 5>/dev/log # 假设syslog监听此设备 exec 6>&1 # 复制终端FD # 步骤2:用cat读取stdin,并行写入所有FD cat - | while IFS= read -r line; do echo "$line" >&3 echo "$line" >&4 logger -t "myapp" "$line" # 写syslog(注意:logger是外部命令) echo "$line" >&6 done # 步骤3:恢复FD(可选) exec 3>&- 4>&- 5>&- 6>&-这个方案的优势是完全可控:你可以对每条echo加锁、加时间戳、甚至做条件过滤。缺点是while read循环在Bash中较慢(因每次迭代都fork子shell)。生产环境建议用awk替代:
# 更高效的awk版本 awk ' BEGIN { # 打开所有文件(awk内置功能) f1 = "/path/to/file_a.log" f2 = "/path/to/file_b.log" f3 = "/dev/tty" # 终端 } { print $0 > f1 print $0 > f2 print $0 > f3 # syslog需调用system() system("logger -t \"myapp\" \"" $0 "\"") }' < /dev/stdin注意:
awk的print > file是追加模式,若需覆盖,用print > file后加close(file)。这是awk特有的文件管理机制,与Shell FD无关。
3.3 竞态规避:为什么>>不是线程安全的,而> >(tee)是
这是最反直觉的一点。很多人认为command >> file是安全的,因为“追加”听起来就该是原子的。但POSIX标准规定:>>只是open(file, O_WRONLY|O_APPEND)的语法糖,而O_APPEND仅保证每次write()系统调用的偏移量设置是原子的,不保证多个进程同时write()时数据不交错。
# 两个进程同时执行 echo "A" >> shared.log echo "B" >> shared.log # 可能产生:A\nB\n 或 B\nA\n 或 A\nB\n(正常) # 但极端情况下可能:AB\n\n(两行合并成一行)而>(tee file)的原理是:tee进程独占打开file(O_WRONLY|O_CREAT|O_TRUNC),所有写入都经由它串行化。虽然tee本身不是原子的,但它的单进程模型天然规避了多进程竞态。因此,在需要强一致性的日志场景,>(tee file)比>>更可靠。
4. 缓冲区行为:为什么你的日志总“延迟出现”,以及如何强制刷新
这是Shell重定向领域最深的水坑。当你执行./long_running_script.sh > output.log 2>&1 &,却发现output.log长时间为空,而ps aux | grep long_running显示进程明明在跑——这不是程序卡死,而是标准I/O缓冲区在作祟。
4.1 三类缓冲模式:全缓冲、行缓冲、无缓冲
C标准库(glibc)为stdout/stderr设置了不同的默认缓冲策略:
stderr(FD 2):默认无缓冲(unbuffered)。每次fprintf(stderr, ...)都立即调用write(2, ...),数据直达内核。stdout(FD 1):当连接到终端(tty)时为行缓冲(line-buffered),遇到\n才刷;当重定向到文件或管道时,变为全缓冲(fully buffered),缓冲区满(通常8KB)才刷。- 文件I/O(如
fopen("file.txt", "w")):默认全缓冲。
这就是为什么echo "log"重定向到文件时,你得等很久才看到内容;而echo "error" >&2却立刻出现在终端。验证方法:
# 查看stdout的缓冲状态 stdbuf -oL echo "line buffered" # 强制行缓冲 stdbuf -o0 echo "unbuffered" # 强制无缓冲 stdbuf -o8192 echo "full buffer" # 强制8KB全缓冲4.2 强制刷新:stdbuf、unbuffer与script的适用场景
stdbuf是GNU coreutils提供的利器,它通过LD_PRELOAD劫持C库的setvbuf()函数,从而在不修改源码的前提下改变缓冲行为:
# 让一个全缓冲的程序变成行缓冲(解决日志延迟) stdbuf -oL -eL python3 heavy_script.py > output.log 2>&1 # 让curl的进度条实时显示(默认全缓冲) stdbuf -oL curl -# -o data.zip http://large.file # 注意:-oL对stderr无效,因为stderr本就是无缓冲的,要用-eL但stdbuf有局限:它只对动态链接的程序有效,对静态链接或shell内置命令(如echo)无效。此时需用script命令:
# script会创建一个伪终端(pty),强制stdout变为行缓冲 script -qec "python3 heavy_script.py" /dev/null > output.log # -q:静默模式;-e:执行后退出;-c:指定命令script的原理是欺骗程序:让它以为自己在和终端对话,从而启用行缓冲。这是绕过程序自身缓冲逻辑的终极方案。
4.3 Shell内置命令的缓冲陷阱:echo、printf为何有时不即时?
Shell内置命令(echo,printf,cd等)的输出缓冲由shell自身控制。Bash 4.0+默认对内置命令的stdout启用行缓冲,但有一个例外:当stdout被重定向到管道时,Bash会禁用缓冲以提升管道性能。这导致一个诡异现象:
# 场景:实时监控某个值的变化 while true; do echo "$(date): $(free -m | awk '/Mem:/ {print $3}')MB used" sleep 1 done > monitor.log # 你期望每秒一行,但monitor.log可能10秒才更新一次!原因:while循环的stdout被重定向到文件,Bash对其启用全缓冲。解决方案有两个:
用
stdbuf包装整个循环(推荐):stdbuf -oL bash -c ' while true; do echo "$(date): $(free -m | awk '"'"'/Mem:/ {print $3}'"'"')MB used" sleep 1 done' > monitor.log在循环内手动刷新(兼容老版本):
while true; do echo "$(date): $(free -m | awk '/Mem:/ {print $3}')MB used" sleep 1 done | stdbuf -oL cat > monitor.log # 用stdbuf强制cat的stdout为行缓冲,再重定向
我在调试一个网络探测脚本时,就因ping -c1 host | grep "time="的输出延迟,误判为网络中断。后来发现是grep在管道中启用了全缓冲,加了stdbuf -oL才解决。这个细节提醒我:永远不要假设输出是实时的,尤其在重定向场景下。
5. 子shell与重定向继承:为什么$(command)里的重定向不生效
Shell脚本中最难调试的bug,往往源于对子shell(subshell)作用域的误解。当你写output=$(ls -la > /tmp/listing.txt),你以为/tmp/listing.txt会被创建,结果发现文件为空——因为$(...)命令替换会创建一个子shell,而子shell中的重定向只在该子shell内有效,父shell完全不可见。
5.1 子shell的四种典型触发场景
| 触发方式 | 是否继承父shell的FD? | 是否可修改父shell变量? | 典型陷阱 |
|---|---|---|---|
$(command) | ✅ 继承 | ❌ 不可 | var=$(echo "test" > file)中file被创建但为空 |
{ command; } | ✅ 继承 | ✅ 可 | var=1; { var=2; }; echo $var输出1(实际是{}不创建子shell,此处为特例) |
(command) | ✅ 继承 | ❌ 不可 | (exec 3>file; echo test >&3)创建文件但父shell无法访问FD3 |
command | command | ✅ 继承(管道两端) | ❌ 不可 | var=1; echo 2 | read var; echo $var输出1 |
关键结论:只有$(...)、( )、管道|会创建真正的子shell(fork新进程),而{ }只是命令分组,仍在当前shell进程内执行。但{ }中的重定向同样有作用域限制:
# ❌ 错误:重定向只在{}内有效 { echo "hello" > temp.txt; cat temp.txt; } > output.txt # 结果:temp.txt有内容,output.txt为空(因为> output.txt作用于整个{}块) # ✅ 正确:重定向作用于整个块 { echo "hello"; cat temp.txt; } > output.txt5.2 突破子shell限制:用lastpipe选项和coproc
Bash 4.2+提供了lastpipe选项,允许管道最后一个命令在当前shell中执行(而非子shell),从而突破变量作用域限制:
# 启用lastpipe(需关闭job control) set +o monitor set -o lastpipe # 现在管道最后一环可修改变量 count=0 echo -e "a\nb\nc" | while read line; do ((count++)) done echo $count # 输出3(以前是0)但lastpipe有严格条件:不能在交互式shell中启用,且要求set +o monitor。更通用的方案是coproc(协同进程):
# 创建一个长期运行的coprocess,与主shell共享FD coproc LOGGER { while IFS= read -r line; do echo "[$(date +%T)] $line" >> /var/log/myapp.log done } # 主shell向coprocess发送数据 echo "App started" >&${LOGGER[1]} echo "Config loaded" >&${LOGGER[1]} # 关闭写端 exec ${LOGGER[1]}>&-coproc的本质是:shell为你fork一个子进程,并自动创建一对管道(pipe),将子进程的stdin/stdout分别绑定到${LOGGER[0]}和${LOGGER[1]}这两个FD。这样,主shell就能通过写FD与子进程通信,而子进程的输出可独立重定向——完美规避了子shell变量隔离问题。
5.3 调试子shell重定向:用set -x和strace定位问题
当重定向行为不符合预期时,set -x(xtrace)是第一道防线:
set -x output=$(ls -la > /tmp/test.txt) set +x # 输出: # + ls -la # + output= # (注意:/tmp/test.txt被创建但为空)+ ls -la表明ls的stdout被重定向到了/tmp/test.txt,但output=说明命令替换捕获的是空字符串——因为ls的输出全进了文件,没走stdout。要捕获输出并写文件,必须用tee:
output=$(ls -la | tee /tmp/test.txt) # output有值,/tmp/test.txt也有更深层的调试要用strace,它能显示真实的系统调用:
# 追踪命令替换中的重定向 strace -e trace=open,write,dup2 -f bash -c 'output=$(ls -la > /tmp/test.txt)' # 你会看到: # [pid 12345] open("/tmp/test.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 # [pid 12345] dup2(3, 1) = 1 # 将FD 3复制到FD 1(stdout) # [pid 12345] write(1, "total 0\n", 8) = 8 # 写入成功 # [pid 12345] close(3) = 0这个trace清晰地证明:重定向确实发生了,且ls的数据写入了文件。output为空,是因为$(...)只捕获FD 1的原始输出,而FD 1已被重定向到文件,自然无数据可捕获。
我在重构一个CI部署脚本时,就用strace发现了这个问题。脚本用$(git log -1 --format="%H" > commit_id.txt)想同时获取commit hash并存文件,结果commit_id.txt有内容但变量为空。strace一查,dup2(3,1)明明白白,立刻改用$(git log -1 --format="%H" | tee commit_id.txt)解决。
6. 实战:构建一个企业级日志分流器(含错误恢复与轮转)
现在,我们将前面所有知识点整合,构建一个生产环境可用的日志分流器。它需满足:
- 同时输出到:控制台(彩色)、本地文件(带时间戳)、远程syslog(UDP)、归档压缩(每日轮转)
- 支持异常中断后的自动恢复(如磁盘满时跳过写文件,不阻塞主流程)
- 无外部依赖(纯Bash + 标准工具)
6.1 架构设计:FD分层与错误隔离
核心思想是将不同输出目标分配到独立FD,并用||实现故障降级:
#!/bin/bash # enterprise_logger.sh # === 阶段1:初始化FD(所有打开操作集中在此)=== # FD 3: 控制台(彩色输出) exec 3>&1 # FD 4: 本地日志文件(带日期) LOG_FILE="/var/log/app/$(date +%Y%m%d).log" exec 4>"$LOG_FILE" || { echo "WARN: Cannot write to $LOG_FILE, using /dev/null"; exec 4>/dev/null; } # FD 5: 远程syslog(通过logger命令,非直接FD) SYSLOG_CMD="logger -t 'enterprise_app' --id=$$" # FD 6: 归档管道(用于gzip压缩) mkfifo /tmp/archive_pipe.$$ 2>/dev/null exec 6>/tmp/archive_pipe.$$ || { echo "WARN: Cannot create archive pipe"; exec 6>/dev/null; } # === 阶段2:启动后台归档进程 === gzip -c < /tmp/archive_pipe.$$ > "/var/log/app/archive_$(date +%Y%m%d_%H%M%S).log.gz" & ARCHIVE_PID=$! # === 阶段3:定义日志函数 === log_message() { local level="$1" # INFO, ERROR, DEBUG local message="$2" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # 控制台输出(彩色) case "$level" in "ERROR") echo -e "\033[0;31m[$timestamp] [$level] $message\033[0m" >&3 ;; "WARN") echo -e "\033[1;33m[$timestamp] [$level] $message\033[0m" >&3 ;; "INFO") echo -e "\033[0;32m[$timestamp] [$level] $message\033[0m" >&3 ;; *) echo "[$timestamp] [$level] $message" >&3 ;; esac # 本地文件输出(无色,供grep) echo "[$timestamp] [$level] $message" >&4 # Syslog输出(异步,失败不阻塞) echo "$message" | $SYSLOG_CMD >/dev/null 2>&1 || true # 归档输出(仅INFO及以上,且不阻塞) if [[ "$level" =~ ^(INFO|ERROR|WARN)$ ]]; then echo "[$timestamp] [$level] $message" >&6 2>/dev/null || true fi } # === 阶段4:主逻辑与清理 === # 示例:记录启动日志 log_message "INFO" "Application started with PID $$" # 模拟业务逻辑 for i in {1..5}; do log_message "INFO" "Processing item $i" sleep 0.5 done # 清理:关闭FD,等待归档完成 exec 3>&- 4>&- 6>&- wait $ARCHIVE_PID 2>/dev/null rm -f /tmp/archive_pipe.$$6.2 关键技术点解析
FD初始化集中化:所有
exec放在开头,避免分散导致的FD泄漏。||后接exec 4>/dev/null确保即使日志目录不存在,脚本也能继续运行。归档管道的巧妙运用:
mkfifo创建命名管道,exec 6>将其打开为写端,后台gzip进程从读端消费。这样,log_message只需向FD 6写入,gzip自动压缩并保存,全程无临时文件。错误隔离设计:
syslog和archive的写入都用|| true包裹,确保任一环节失败(如syslog服务宕机、磁盘满)都不影响主流程。这是生产脚本的黄金法则:监控链路可以降级,但业务链路必须畅通。时间戳一致性:所有输出的时间戳都在
log_message函数内统一生成,避免date多次调用导致的毫秒级偏差。
6.3 生产环境加固建议
- 磁盘空间保护:在写入前检查
df -P /var/log | awk 'NR==2 {print $5}' | sed 's/%//',若>90%则自动切换到/tmp。 - FD泄漏防护:在脚本末尾添加
for fd in $(ls /proc/$$/fd/ 2>/dev/null); do [[ "$fd" =~ ^[0-9]+$ ]] && [[ "$fd" -gt 2 ]] && exec "$fd">&-; done,强制关闭所有非标准FD。 - 信号处理:捕获
SIGTERM,优雅关闭归档进程:cleanup() { kill $ARCHIVE_PID 2>/dev/null wait $ARCHIVE_PID 2>/dev/null rm -f /tmp/archive_pipe.$$ } trap cleanup SIGTERM SIGINT
这个日志器已在多个项目中稳定运行超过两年。它证明:Shell的“高级重定向”不是炫技,而是用最少的系统资源,构建最健壮的I/O控制流。当你能熟练驾驭FD、理解缓冲、掌控子shell,你就不再是在“写脚本”,而是在“编排操作系统”。
我在某次紧急故障排查中,正是靠这个日志器的多路输出,同时在终端看到实时错误、在文件里找到完整堆栈、在syslog中关联到数据库超时事件,15分钟内定位到第三方SDK的内存泄漏。那一刻我深刻体会到:所谓“进阶”,不过是把系统底层的确定性,转化为你手上的确定性。