0

wait后台任务退出码精确捕获方法:3种方案让并行脚本失败无处遁形

2026.07.27 | youres | 16次围观

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辅助作者原创,未经许可,转载请保留原文链接。

发表评论