无法使用 Ctrl+C 停止 bash 脚本

无法使用 Ctrl+C 停止 bash 脚本

我编写了一个简单的 bash 脚本,其中包含一个循环,用于打印日期并 ping 到远程计算机:

#!/bin/bash
while true; do
    #     *** DATE: Thu Sep 17 10:17:50 CEST 2015  ***
    echo -e "\n*** DATE:" `date` " ***";
    echo "********************************************"
    ping -c5 $1;
done

当我从终端运行它时,我无法使用 来停止它Ctrl+C。看起来它会将 发送^C到终端,但脚本不会停止。

MacAir:~ tomas$ ping-tester.bash www.google.com

*** DATE: Thu Sep 17 23:58:42 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C                                                          <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms

*** DATE: Thu Sep 17 23:58:48 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms

无论我按多少次或按多快。我无法阻止它。
自行测试并实现。

作为一个侧面解决方案,我用 来停止它Ctrl+Z,然后停止它kill %1

这里究竟发生了什么^C

答案1

发生的情况是bash和都收到pingSIGINT (bash不是交互式,两者都pingbash同一个进程组中运行,该进程组已由运行该脚本的交互式 shell 创建并设置为终端的前台进程组)。

但是,bash仅在当前运行的命令退出后才异步处理该 SIGINT。bash如果当前运行的命令因 SIGINT 终止(即其退出状态表明它已被 SIGINT 终止),则仅在收到该 SIGINT 时退出。

$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere

上面,bash当我按 Ctrl-C 时,会收到 SIGINT,但sh会以 0 退出代码正常退出,因此会忽略 SIGINT,这就是我们看到“此处”的原因。sleepshbash

ping,至少 iputils 的行为是这样的。中断时,它会打印统计信息并以 0 或 1 退出状态退出,具体取决于其 ping 是否得到回复。因此,当您在ping运行时按 Ctrl-C 时,bash会注意到您已按下Ctrl-C其 SIGINT 处理程序,但由于ping正常退出,bash因此不会退出。

如果您sleep 1在该循环中添加一个并按Ctrl-Cwhile sleepis running,因为sleep在 SIGINT 上没有特殊处理程序,它将死亡并报告bash它因 SIGINT 死亡,在这种情况下bash将退出(它实际上会用 SIGINT 杀死自己,以便向其父级报告中断)。

至于为什么bash会有这样的行为,我不确定,而且我注意到这种行为并不总是确定性的。我刚刚问过bash开发邮件列表上的问题(更新:@Jilles 现在已经确定了原因他的回答)。

我发现的唯一具有类似行为的其他 shell 是 ksh93(更新,正如 @Jilles 所提到的,FreeBSD 也是如此sh)。在那里,SIGINT 似乎被明显地忽略了。ksh93每当命令被 SIGINT 终止时就会退出。

您会得到与上面相同的行为bash,但还会:

ksh -c 'sh -c "kill -INT \$\$"; echo test'

不输出“测试”。也就是说,如果它正在等待的命令死于 SIGINT,那么它就会退出(通过在那里用 SIGINT 杀死自己),即使它本身没有收到该 SIGINT。

解决方法是添加:

trap 'exit 130' INT

在脚本的顶部,bash在收到 SIGINT 时强制退出(请注意,在任何情况下,SIGINT 都不会被同步处理,只有在当前运行的命令退出后)。

理想情况下,我们希望向父母报告我们死于 SIGINT(因此,如果是另一个bash脚本,则该bash脚本也会被中断)。执行 anexit 130与因 SIGINT 死亡不同(尽管某些 shell 会$?在这两种情况下设置为相同的值),但是它通常用于报告 SIGINT 死亡(在 SIGINT 为 2 的系统上,这是最多的)。

然而对于bashksh93FreeBSD来说sh,这是行不通的。 130 退出状态不被 SIGINT 视为死亡,父脚本会不是在那里中止。

因此,一个可能更好的选择是在收到 SIGINT 后用 SIGINT 自杀:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT

答案2

解释是 bash 为 SIGINT 和 SIGQUIT 实现了 WCE(等待和协作退出)http://www.cons.org/cracauer/sigint.html。这意味着,如果 bash 在等待进程退出时收到 SIGINT 或 SIGQUIT,它将一直等到进程退出,如果进程在该信号上退出,则它会自行退出。这可以确保在用户界面中使用 SIGINT 或 SIGQUIT 的程序将按预期工作(如果信号没有导致程序终止,则脚本将正常继续)。

捕获 SIGINT 或 SIGQUIT 但随后因此终止但使用正常 exit() 而不是通过向自身重新发送信号的程序会出现一个缺点。可能无法中断调用此类程序的脚本。我认为真正的解决办法是诸如 ping 和 ping6 之类的程序。

ksh93 和 FreeBSD 的 /bin/sh 实现了类似的行为,但大多数其他 shell 没有实现。

答案3

终端注意到了 control-cINT并向前台进程组(这里包括 shell)发送一个信号,因为ping尚未创建新的前台进程组。这很容易通过 traping 来验证INT

#! /bin/bash
trap 'echo oh, I am slain; exit' INT
while true; do
  ping -c5 127.0.0.1
done

如果正在运行的命令创建了一个新的前台进程组,则 control-c 将转到该进程组,而不是 shell。在这种情况下,shell 将需要检查退出代码,因为终端不会发出信号。

INT顺便说一句,在 shell 中进行处理可能非常复杂,因为 shell 有时需要忽略信号,有时则不需要。如果好奇,请深入了解源代码,或者思考一下tail -f /etc/passwd; echo foo:)

答案4

好吧,我尝试将 a 添加sleep 1到 bash 脚本中,然后砰!
现在我可以用两个来阻止它Ctrl+C

当按下 时Ctrl+C,一个SIGINT信号被发送到当前执行的进程,该命令在循环内运行。然后,子外壳进程继续执行循环中的下一个命令,该命令启动另一个进程。为了能够停止脚本,需要发送两个SIGINT信号,一个用于中断当前正在执行的命令,另一个用于中断正在执行的命令。子外壳过程。

在没有调用的脚本中sleep,按得Ctrl+C非常快并且多次似乎不起作用,并且不可能退出循环。我的猜测是,按两次不够快,不足以使其恰好在当前执行进程中断和下​​一个进程开始之间的正确时刻。每次Ctrl+C按下都会将 a 发送SIGINT到循环内执行的进程,但不会发送到子外壳

在 with 的脚本中sleep 1,此调用将暂停执行一秒钟,当被第一个Ctrl+C(first SIGINT) 中断时,子外壳将花费更多时间来执行下一个命令。所以现在,第二个Ctrl+C(第二个SIGINT)将转到子外壳,脚本执行将结束。

相关内容