Shell脚本里用「&」把任务扔到后台跑,速度是快了,但退出码经常收不回来——脚本最后一行 wait 一执行,$? 永远是 0,哪个任务失败了根本不知道。这篇把 wait 后台任务退出码精确捕获的几种方法一次讲透,全是能直接抄的代码。
为什么直接 wait 拿不到退出码
先说清楚问题根源。不带参数的 wait 会等所有后台任务结束,但它的返回值和子任务的成败没有关系——在 bash 里,裸 wait 的退出码通常就是 0(除非 wait 本身被信号打断)。所以下面这种写法是无效的:
task1 & task2 & wait echo $? # 恒为0,毫无参考价值
想精确捕获每个后台任务的退出码,核心就一条:wait 后面必须跟具体的 PID。「wait PID」的返回值才是那个进程真实的退出状态。
方法一:PID 数组逐个 wait(最通用)
用「$!」记下每个后台任务的 PID,存进数组,最后逐个 wait 收码。这是兼容性最好的写法,bash 3.x 都能跑:
#!/bin/bash
pids=()
names=()
for host in web1 web2 db1; do
ping -c 3 "$host" >/dev/null 2>&1 &
pids+=($!)
names+=("$host")
done
fail=0
for i in "${!pids[@]}"; do
wait "${pids[$i]}"
rc=$?
if [ $rc -ne 0 ]; then
echo "[FAIL] ${names[$i]} 退出码=$rc"
fail=$((fail+1))
else
echo "[OK] ${names[$i]}"
fi
done
echo "失败数: $fail"
exit $((fail > 0 ? 1 : 0))
两个细节要注意:一是「$!」必须紧跟在「&」之后取,中间插其他后台命令就串号了;二是同一个 PID 只能 wait 收一次码,bash 会在收割后清掉记录,二次 wait 会报「not a child of this shell」并返回 127。
方法二:wait -n 谁先结束收谁(bash 4.3+)
逐个 wait 有个缺点:按启动顺序阻塞等待。如果第一个任务最慢,后面已经完成的任务也得排队。bash 4.3 引入的「wait -n」可以等"任意一个"后台任务结束,立刻拿到它的退出码:
#!/bin/bash for f in a.log b.log c.log; do gzip "$f" & done fail=0 while wait -n; rc=$?; [ $rc -ne 127 ]; do [ $rc -ne 0 ] && fail=$((fail+1)) done echo "失败数: $fail"
当没有后台任务可等时,「wait -n」返回 127,正好用来退出循环。缺点是这种写法拿不到"是哪个 PID 结束了"。要定位到具体任务,得用 bash 5.1 新增的「-p」选项:
# bash 5.1+ while wait -n -p done_pid; rc=$?; [ -n "$done_pid" ]; do echo "PID $done_pid 退出码=$rc" done
「-p」会把刚结束的进程号写进变量 done_pid,退出码和任务身份就都齐了。先用「bash --version」确认版本再上这招。
方法三:退出码落盘(跨进程最稳)
如果后台任务本身是一段子 shell 逻辑,还有一种土办法但极其可靠:让每个任务自己把退出码写进文件,主进程 wait 完统一汇总。这种方式不依赖 bash 版本,还能天然留下日志:
#!/bin/bash
dir=$(mktemp -d)
for host in web1 web2 db1; do
(
curl -sf --max-time 10 "https://$host/health" >/dev/null
echo $? > "$dir/$host.rc"
) &
done
wait
for f in "$dir"/*.rc; do
host=$(basename "$f" .rc)
rc=$(cat "$f")
[ "$rc" -ne 0 ] && echo "[FAIL] $host rc=$rc"
done
rm -rf "$dir"
批量巡检、定时任务这类需要事后追查的场景,我个人首推这个方案:wait 只负责等,退出码的精确捕获交给文件,两边职责干净。
4个高频踩坑点
- 管道会吃掉退出码:「task | tee log &」里 $! 是整条管道的 PID,wait 收到的是管道最后一个命令的状态。要拿 task 本身的码,开「set -o pipefail」或改用落盘法。
- set -e 与 wait 的关系:「wait $pid」返回非零会触发 set -e 直接退出脚本。想自己汇总失败数,写成「wait $pid || rc=$?」的形式兜住。
- 128+N 是被信号杀的:退出码 137 = 128+9(SIGKILL)、143 = 128+15(SIGTERM)。批量任务里看到 124 一般是 timeout 命令干的,和 wait 无关。
- 子 shell 里 wait 无效:后台任务是当前 shell 的子进程,如果你在「$( )」或管道产生的子 shell 里 wait,等的是另一个进程空间,必然 127。
怎么选
脚本简单、任务数固定——方法一 PID 数组,十行搞定;任务耗时差异大、想尽早发现失败——方法二 wait -n;需要日志留痕、或者环境里 bash 版本参差不齐——方法三落盘。三种方法的核心是同一句话:wait 后台任务退出码精确捕获,靠的从来不是裸 wait,而是让每一个 PID 的退出状态都有明确的去处。
相关文章
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论