问题 Bash:当子脚本陷阱SIGINT时,为什么父脚本不会在SIGINT上终止?


script1.sh:

 #!/bin/bash    

./script2.sh
 echo after-script

script2.sh:

#!/bin/bash

function handler {
  exit 130
}

trap handler SIGINT

while true; do true; done

当我从终端启动script1.sh,然后使用Ctrl + C将SIGINT发送到其进程组时,该信号被script2.sh捕获,当script2.sh终止时,script1.sh将打印“after-script”。但是,我原本希望script1.sh在调用script2.sh的行之后立即终止。为什么在这个例子中不是这种情况?

补充说明(编辑):

  • 由于script1.sh和script2.sh位于同一进程组中,因此将SIGINT发送给  在命令行上按Ctrl + C时的脚本。这就是为什么我不希望script2.sh在script2.sh退出时继续。

  • 当注释掉script2.sh中的“陷阱处理程序SIGINT”行时,script1.sh会在script2.sh存在后立即退出。我想知道为什么它的行为不同,因为script2.sh只产生相同的退出代码(130)。


10281
2017-08-28 00:59


起源

也许用 set -e? - phs


答案:


新答案:

这个问题比我原先怀疑的要有趣得多。答案主要在这里给出:

发送到包含子节点的perl脚本时,SIGINT(^ C)会发生什么?

这是相关的花絮。我意识到你没有使用Perl,但我认为Bash正在使用C的约定。

Perl的内置系统功能就像C系统一样工作(3)   就信号而言,来自标准C库的功能。   如果您使用的是Perl版本的system()或管道打开或反引号,   那么父母 - 一个呼叫系统而不是一个呼叫系统   它 - 在孩子们的时候会忽略任何SIGINT和SIGQUIT   运行。

这个解释 是我见过的关于可以做出的各种选择的最好的。它还说Bash采用WCE方法。也就是说,当父进程收到SIGINT时,它会等待其子进程返回。如果处理的进程退出SIGINT,它也会退出SIGINT。如果孩子以任何其他方式退出,则忽略SIGINT。

调用shell还有一种方法可以判断是否调用了   程序退出SIGINT并忽略SIGINT(或用于   其他目的)。就像在WUE方式中一样,shell会等待孩子   完成。它确定程序是否在SIGINT上结束,如果   所以,它停止了脚本。如果程序做了任何其他退出,那么   脚本将继续。我会称之为做事的方式   本文其余部分的“WCE”(用于“等待和合作退出”)。

我在Bash手册页中找不到对此的引用,但我会继续查看info文档。但我有99%的信心这是正确的答案。

老答案:

Bash脚本中命令的非零退出状态不会终止该程序。如果你做了 echo $? 后 ./script2.sh 它将显示130.您可以使用终止脚本 set -e 正如phs建议的那样。

$ help set
...
-e  Exit immediately if a command exits with a non-zero status.

11
2017-08-28 03:59



这并没有完全回答这个问题。请注意,在终端上按下Ctrl + C时,SIGINT将发送到script1.sh和script2.sh。我在这个问题上添加了一些额外的评论。 - Hermann Speiche
那非常有趣。我仍然想知道有和没有“陷阱处理程序SIGINT”的版本之间的区别。我怀疑信号首先传递给孩子,然后传递给父母。当子进程中安装陷阱处理程序时,终止可能需要更长的时间,因此当父进程收到信号(它忽略它)时它仍处于活动状态。如果没有处理程序,子节点可能已经在父节点接收到信号时终止 不 忽略它并终止。谁能证实我的猜测? - Hermann Speiche
你的猜测不正确。我已经更新了我的答案。 - seanmcl


@ seanmcl更新答案的第二部分是正确的,并链接到 http://www.cons.org/cracauer/sigint.html 是一个非常好的人仔细阅读。

从那个链接,“即使您查找系统的数值,也不能通过带有特殊数值的出口(3)“伪造”正确的退出状态“事实上,这就是@Hermann Speiche的script2.sh所尝试的内容。

一个答案是修改script2.sh中的函数处理程序,如下所示:

function handler {
  # ... do stuff ...
  trap INT
  kill -2 $$
}

这有效地删除了信号处理程序并“重新抛出”了SIGINT,导致bash进程退出并使用适当的标志,以便其父bash进程正确处理最初发送给它的SIGINT。这样,使用 set -e 或者实际上并不需要任何其他黑客攻击。

值得注意的是,如果你有一个可执行文件在发送一个SIGINT时行为不正确(它不符合上面链接中的“如何成为一个正确的程序”,例如它以正常的返回码退出),单向解决这个问题的方法是使用如下脚本将对该进程的调用包装起来:

#!/bin/bash

function handler {
  trap INT
  kill -2 $$
}

trap handler INT
badprocess "$@"

1
2017-09-17 14:42





原因是你的 script1.sh 不会终止的 script2.sh 正在子shell中运行。要使前一个脚本退出,您可以设置 -e 正如phs和seanmcl所建议或强迫的 script2.sh 通过说:在同一个shell中运行:

. ./script2.sh

在你的第一个脚本中。如果你这样做,你所观察到的将是显而易见的 set -x 在执行脚本之前。 help set 讲述:

  -x  Print commands and their arguments as they are executed.

0
2017-08-28 04:56





您还可以让第二个脚本通过SIGHUP在其父脚本上发送终止信号,或其他安全可用的信号,如SIGQUIT,其中父脚本也可以考虑或陷阱(发送SIGINT不起作用)。

script1.sh:

#!/bin/bash

trap 'exit 0' SIQUIT  ## We could also just accept SIGHUP if we like without traps but that sends a message to the screen.

./script2.sh  ## or "bash script.sh" or "( . ./script.sh; ) which would run it on another process
echo after-script

script2.sh:

#!/bin/bash

SLEEPPID=''

PID=$BASHPID
read PPID_ < <(exec ps -p "$PID" -o "$ppid=")

function handler {
  [[ -n $SLEEPPID ]] && kill -s SIGTERM "$SLEEPPID" &>/dev/null
  kill -s SIGQUIT "$PPID_"
  exit 130
}

trap handler SIGINT

# better do some sleeping:

for (( ;; )); do
  [[ -n $SLEEPPID ]] && kill -s 0 "$SLEEPPID" &>/dev/null || {
    sleep 20 &
    SLEEPPID=$!
  }
  wait
done

您脚本中的原始最后一行也可能就像这样,具体取决于您的脚本预期实现。

./script2.sh || exit
...

要么

./script2.sh
[[ $? -eq 130 ]] && exit
...

0
2017-08-28 06:13