【问题标题】:Bash: Why does parent script not terminate on SIGINT when child script traps SIGINT?Bash:当子脚本捕获 SIGINT 时,为什么父脚本不会在 SIGINT 上终止?
【发布时间】:2013-08-30 22:18:31
【问题描述】:

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 在同一个进程组中,当 Ctrl+C kbd> 在命令行上被按下。这就是为什么我不希望 script2.sh 退出时 script1.sh 继续。

  • 当 script2.sh 中的“trap handler SIGINT”行被注释掉时,script1.sh 会在 script2.sh 存在后立即退出。我想知道为什么它的行为会有所不同,因为 script2.sh 会产生相同的退出代码 (130)。

【问题讨论】:

  • 也许使用set -e

标签: linux bash process


【解决方案1】:

正确的工作方式是通过 setpgrp()。 shell 的所有孩子都应该放在同一个 pgrp 中。当 tty 驱动程序发出 SIGINT 信号时,它将被汇总传递给所有进程。任何级别的shell都应该注意信号的接收,等待孩子退出然后杀死自己,再一次,没有信号处理程序,有sigint,这样他们的退出代码是正确的。

此外,当 SIGINT 被其父进程设置为在启动时忽略时,他们应该忽略 SIGINT。

shell 不应将“检查孩子是否以 sigint 退出”作为逻辑的任何部分。 Shell 应始终仅将其直接接收到的信号作为采取行动然后退出的理由。

在真正的 UNIX 时代,SIGINT 只需按一下键即可停止 shell 和所有子进程。 shell 的退出和子进程继续运行从来没有任何问题,除非它们自己将 SIGINT 设置为忽略。

对于任何 shell 管道,它们应该是从右到左的管道创建的子进程关系。最右边的命令是 shell 的直接子级,因为那是正常退出的最后一个进程。在此之前的每个命令行都是紧邻下一个管道符号或 && 或 || 右侧的进程的子进程。象征。 && 和 || 周围有明显的一群孩子自然脱落。

最后,进程组保持干净,以便 nohup 与所有接收 SIGINT 或 SIGQUIT 或 SIGHUP 或其他 tty 驱动程序信号的子进程一样工作。

【讨论】:

    【解决方案2】:

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

    在该链接中,“即使您查找系统的数值,您也不能通过具有特殊数值的 exit(3) 来“伪造”正确的退出状态”。事实上,这就是@Hermann Speiche 的 script2.sh 所尝试的。

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

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

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

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

    #!/bin/bash
    
    function handler {
      trap INT
      kill -2 $$
    }
    
    trap handler INT
    badprocess "$@"
    

    【讨论】:

      【解决方案3】:

      新答案:

      这个问题比我最初想象的要有趣得多。答案基本上在这里给出:

      What happens to a SIGINT (^C) when sent to a perl script containing children?

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

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

      This explanation 是我所见过的关于可以做出的各种选择的最佳选择。它还说 Bash 采用 WCE 方法。也就是说,当父进程收到 SIGINT 时,它会一直等到其子进程返回。如果处理的该进程从 SIGINT 退出,它也会以 SIGINT 退出。如果孩子以任何其他方式退出,它会忽略 SIGINT。

      调用shell还有一种方法可以判断被调用的 程序在 SIGINT 上退出并且如果它忽略 SIGINT(或将其用于 其他用途)。与 WUE 方式一样,shell 等待孩子 完全的。它计算程序是否在 SIGINT 上结束,如果 所以,它停止了脚本。如果程序执行了任何其他退出,则 脚本将继续。我将做事的方式称为 本文档其余部分的“WCE”(表示“等待和合作退出”)。

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

      旧答案:

      Bash 脚本中命令的非零退出状态不会终止程序。如果您在./script2.sh 之后执行echo $?,它将显示130。您可以按照phs 的建议使用set -e 终止脚本。

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

      【讨论】:

      • 这并不能完全回答问题。请注意,当在终端上按下 Ctrl+C 时,SIGINT 会同时发送到 script1.sh 和 script2.sh。我已经对这个问题添加了一些额外的评论。
      • 这很有趣。不过,我仍然想知道带有和不带有“陷阱处理程序 SIGINT”的版本之间的区别。我怀疑信号首先传递给孩子,然后传递给父母。当在子进程中安装陷阱处理程序时,可能需要更长的时间才能终止,因此当父进程接收到信号(它会忽略)时,它仍然是活动的。如果没有处理程序,当父级接收到信号时,子级可能已经终止,因此 不会 忽略它并终止。谁能证实我的猜测?
      【解决方案4】:

      您还可以让您的第二个脚本通过 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
      

      您在 script1.sh 中的原始最后一行也可能是这样的,具体取决于您的脚本预期实现。

      ./script2.sh || exit
      ...
      

      或者

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

      【讨论】:

        【解决方案5】:

        原因是您的script1.sh 没有终止是script2.sh 正在子shell 中运行。要退出前一个脚本,您可以按照 phs 和 seanmcl 的建议设置 -e,或者强制 script2.sh 在同一个 shell 中运行:

        . ./script2.sh
        

        在您的第一个脚本中。如果您在执行脚本之前执行set -x,那么您所观察到的将是显而易见的。 help set 告诉:

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

        【讨论】: