我编写了一个简单的 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
和都收到ping
SIGINT (bash
不是交互式,两者都ping
在bash
同一个进程组中运行,该进程组已由运行该脚本的交互式 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,这就是我们看到“此处”的原因。sleep
sh
bash
ping
,至少 iputils 的行为是这样的。中断时,它会打印统计信息并以 0 或 1 退出状态退出,具体取决于其 ping 是否得到回复。因此,当您在ping
运行时按 Ctrl-C 时,bash
会注意到您已按下Ctrl-C
其 SIGINT 处理程序,但由于ping
正常退出,bash
因此不会退出。
如果您sleep 1
在该循环中添加一个并按Ctrl-C
while sleep
is 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 的系统上,这是最多的)。
然而对于bash
或ksh93
FreeBSD来说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
)将转到子外壳,脚本执行将结束。