Bash 中的 SIGINT 行为

Bash 中的 SIGINT 行为

我正在运行这个 Bash 脚本:

#!/bin/bash

for i in $(seq 1 100);
do
    echo $i
    sleep 1
done

当我发出时kill -INT <pid>什么也没有发生——脚本继续运行。如果我发出kill -TERM <pid>,脚本和 sleep 命令都会被终止。<pid>这里指的是脚本的 PID,而不是进程sleep

我想了解这里信号处理的区别。我知道发出Ctrl+C将实现与发送相同的结果,-TERM因为信号被发送到进程组,但我不太明白为什么发出的-INT行为与发出 不同-TERM。据我所知,SIGINT 的默认操作是终止进程。

一些背景信息:我的用例是,此脚本作为另一个程序的子进程启动,并且该程序SIGINT在关闭期间发送一个,然后等待 X 秒并发送一个SIGKILL。我将 改为SIGINT一个SIGTERM,它突然终止了进程而不等待,这让我想到了这里的行为差异。

答案1

你观察到的是一种叫做等待并合作退出(WCE)。它是关于处理 SIGINT/SIGQUIT。

您可以在这里阅读有关 WCE (以及替代方法) 的信息:正确处理 SIGINT/SIGQUIT。这篇文章很全面。WCE 的简短描述来自格雷格的维基足以让我们明白:

bash 是少数几个实现等待并合作退出处理 SIGINT/SIGQUIT 传递的方法。解释脚本时,收到 SIGINT 后,它不会立即退出,而是等待当前正在运行的命令返回,并且只有当该命令也被该 SIGINT 终止时,它才会退出(通过使用 SIGINT 终止自身)。这个想法是,如果您的脚本调用vi,并且您在其中按Ctrl+取消操作,则不应将其视为中止脚本的请求。Cvi

这对你的剧本意味着什么?

如果<pid>是脚本的 PID(即bash解释脚本的 PID),则您kill -INT <pid>仅向此进程发送 SIGINT。Ctrl+Ckill -INT -- -<pid>(“负”PID)将向整个进程组发送 SIGINT。 SIGINT 在设计上是“键盘中断”(参见man 7 signal),因此Ctrl+C是发送 SIGINT 的设计方式,因此将 SIGINT 发送到前台进程组(而不是发送到单身的进程(process)是 SIGINT 的设计用法。

bash解释脚本在等待sleep 1返回时收到 SIGINT 时,它会处理信号并做出反应或不做出反应,具体取决于 的反应sleep。实际上,shell 无法判断 是否sleep也收到了信号;它假设。shell 假设信号来自Ctrl+ C(或等效的东西),因为这是 SIGINT 的设计用法;因此 shell 假设 currentsleep也收到了信号。

如果sleep真的收到了信号(即,如果您使用了Ctrl+Ckill -INT -- -<pid>),那么它将因信号而终止。通常,进程能够判断其子进程是否被信号终止,因此这里 shell 能够判断哪个进程被sleep信号终止。如果sleep被 SIGINT 终止,shell 会断定用户已按下Ctrl+C以终止整个脚本,因此 shell 会使用 SIGINT 终止自身(注意:不仅仅是退出;它会通过停止处理信号并将信号发送给自身来终止自身,因此如果 shell 的父进程也实现了 WCE,那么它会相应地做出反应;依此类推)。

sleep没有收到信号,但bash脚本解释器假设它已经收到。然后 shell 观察到sleep已经正常退出(没有被信号终止),因此它sleep假设处理或忽略信号;所以它假设是的,有Ctrl+ C,但作为普通的子进程的操作,而不是试图终止整个脚本。这就是为什么 shell 没有退出,而是继续解释脚本,就像什么都没发生一样,因为sleep已经退出了,就像什么都没发生一样。

换句话说,实现 WCE 的父进程根据当前运行的子进程对 SIGINT/SIGQUIT 做出反应。通过向 发送 SIGINTbash而不是向 发送sleep,你有点欺骗了 shell:你让它假设sleep已经处理或忽略了信号并且一切都很好,因此退出整个脚本是错误的做法。

我把 改为SIGINTa SIGTERM,它就突然终止了进程,没有等待

WCE 是关于处理 SIGINT/SIGQUIT 的。对于 SIGTERM,没有这样的机制(我认为也不需要),所以这个信号“就起作用了”。

相关内容