为什么从 Bash 脚本中的后台作业发送的 USR1 信号不能被等待完成的 parent shell 进程可靠地接收?

Why may USR1 signals sent from background jobs in a Bash script not be reliably received by the parent shell process waiting for their completion?

我有一个 Bash 脚本 运行 并行执行大量后台作业。 在某些情况下,在后台作业完成之前,它会发送 向生成 Bash 进程发送 USR1 信号(例如,通知 作为工作的一部分 运行 的某些进程已终止 非零退出代码)。

在简化形式中,该脚本等同于下面显示的脚本。 这里,为简单起见,每个后台作业总是发送一个 USR1 信号 在完成之前,无条件地(通过 signalparent() 函数)。

signalparent() { kill -USR1 $$; }
handlesignal() { echo 'USR1 signal caught' >&2; }
trap handlesignal USR1

for i in {1..10}; do
    {
        sleep 1
        echo "job $i finished" >&2
        signalparent
    } &
done
wait

当我 运行 上述脚本时(至少在 macOS 11.1 上使用 Bash 3.2.57), 我观察到一些我无法解释的行为,这让我思考 Bash 工作管理和 我忽略的信号捕获。

具体来说,我想对以下内容进行解释 行为。

  1. 几乎总是,当我 运行 脚本时,我看到的 “信号被捕获” 输出中的行(来自 handlesignal() 函数)比那里 作业是在 for 循环中启动的吗?大多数时候是一个到 为正在启动的 10 个作业打印的行中有 4 行。

    为什么在 wait 调用完成时,有 仍然是后台作业,其信号 kill 命令具有 还没被处决?

  2. 同时,每隔一段时间,在脚本的一些调用中, 我观察 kill 命令(来自 signalparent() 函数) 报告有关原始进程的错误 运行ning 脚本 (即 PID 为 $$ 的那个)不再存在——查看 输出如下。

    怎么还有发信号kill命令的作业还在 运行ning 而 parent shell 进程已经终止? 据我了解,parent 是不可能的 进程在所有后台作业完成之前终止,由于 wait呼唤。

    job 2 finished
    job 3 finished
    job 5 finished
    job 4 finished
    job 1 finished
    job 6 finished
    USR1 signal caught
    USR1 signal caught
    job 10 finished
    job 7 finished
    job 8 finished
    job 9 finished
    bash: line 3: kill: (19207) - No such process
    bash: line 3: kill: (19207) - No such process
    bash: line 3: kill: (19207) - No such process
    bash: line 3: kill: (19207) - No such process
    

这两种行为都向我表明存在竞争条件 某种,我不太了解其起源。我会 如果有人能启发我,我将不胜感激,甚至 建议如何更改脚本以避免此类竞争条件。

这在Bash Reference Manual中解释如下。

When bash is waiting for an asynchronous command via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status greater than 128, immediately after which the trap is executed.

因此,您需要重复 wait 直到 returns 0 以确保所有后台作业都已终止,例如:

until wait; do
    :
done

It was my understanding that it is impossible for the parent process to terminate before all background jobs do, due to the wait call.

那是误会; wait 可能 return 由于在后台有 运行 个作业时接收到已设置陷阱的信号,这可能导致程序正常完成,与让这些工作成为孤儿的副作用。

关于“几乎总是,当我运行脚本时,我在输出中看到更少的“信号捕获”行'

根据signal(7)

Standard signals do not queue. If multiple instances of a standard signal are generated while that signal is blocked, then only one instance of the signal is marked as pending (and the signal will be delivered just once when it is unblocked).

更改脚本以使信号不会同时到达的一种方法如下:

signalparent() {
    kill -USR1 $$
}

ncaught=0
handlesignal() {
    (( ++ncaught ))
    echo "USR1 signal caught (#=$ncaught)" >&2
}
trap handlesignal USR1

for i in {1..10}; do
    {
        sleep $i
        signalparent
    } &
done

nwaited=0
while (( nwaited < 10 )); do
    wait && (( ++nwaited ))
done

这是在 macOS 10.15 上使用 Bash 5.1 修改后的脚本的输出:

USR1 signal caught (#=1)
USR1 signal caught (#=2)
USR1 signal caught (#=3)
USR1 signal caught (#=4)
USR1 signal caught (#=5)
USR1 signal caught (#=6)
USR1 signal caught (#=7)
USR1 signal caught (#=8)
USR1 signal caught (#=9)
USR1 signal caught (#=10)