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


42

我编写了一个带有循环的简单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怎么回事?

Answers:


26

什么情况是,双方bashping收到SIGINT(bash不是互动,无论是pingbash已经由您运行该脚本的交互式shell创建并设置为终端的前台进程组相同的进程组中运行)。

但是,bash仅在退出当前正在运行的命令之后,才异步处理该SIGINT。bash仅当当前正在运行的命令死于SIGINT时,才在接收到该SIGINT时退出(即,其退出状态表明它已被SIGINT杀死)。

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

以上bashshsleep收到SIGINT当我按下Ctrl-C,但sh出口通常与一个0退出代码,所以bash忽略了SIGINT,这就是为什么我们看到的“这里”。

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

如果您sleep 1在该循环中添加一个并Ctrl-Csleep运行时按下,因为sleepSIGINT上没有特殊的处理程序,它将死并报告bash它已死于SIGINT,在这种情况下bash将退出(它实际上会被SIGINT杀死,因此将此中断报告给其父项)。

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

我发现,唯一表现类似的其他外壳是ksh93(如@Jilles所述,sh更新为FreeBSD)。在那里,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脚本也会被中断)。这样做exit 130与SIGINT的消亡并不相同(尽管$?在两种情况下某些shell都将设置为相同的值),但是它通常用于报告SIGINT的死亡情况(在SIGINT为2的系统上)。

然而,对于bashksh93或FreeBSD sh,不起作用。SIGINT不会将130退出状态视为死亡,并且父脚本不会在那里终止。

因此,一个可能更好的选择是在收到SIGINT时用SIGINT杀死我们自己:

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

1
吉尔斯的答案解释了“为什么”。作为说明性示例,请考虑  for f in *.txt; do vi "$f"; cp "$f" newdir; done。如果用户在编辑文件之一时键入Ctrl + C,则vi仅显示一条消息。在用户完成编辑文件后,循环应该继续进行似乎是合理的。(是的,我知道您可以说vi *.txt; cp *.txt newdir;我只是以提交for循环为例。)
Scott

@斯科特,好点。尽管vivim至少(至少))isig在编辑时确实禁用了tty (:!cmd不过,当您运行时并不会明显禁用tty ,在这种情况下非常适用)。
斯特凡Chazelas

@Tim,请查看我的修改内容以进行更正。
斯特凡Chazelas

@StéphaneChazelas谢谢。这是因为ping收到SIGINT后以0退出。当bash脚本包含sudo而不是时ping,我发现了类似的行为,但是sudo在收到SIGINT后以1退出。unix.stackexchange.com/questions/479023/…–
蒂姆(Tim)

13

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

捕获SIGINT或SIGQUIT的程序会出现不利的一面,但由于它而终止,但使用正常的exit()而不是通过将信号重新发送给自己而终止。可能无法中断调用此类程序的脚本。我认为ping和ping6等程序确实可以解决此问题。

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


谢谢,这很有意义。我注意到当cmd以exit(130)退出时,FreeBSD sh都不会中止,这是通过SIGINT报告孩子死亡的一种常用方法(exit(130)例如,如果您打断,mksh会执行此操作mksh -c 'sleep 10;:')。
斯特凡Chazelas

5

如您所料,这是由于SIGINT被发送到下级进程,并且在该进程退出后Shell继续运行。

为了更好地处理此问题,您可以检查正在运行的命令的退出状态。Unix返回代码对进程退出的方法(系统调用或信号)以及传递给哪个值exit()或什么信号终止该进程进行编码。这一切都相当复杂,但是使用它的最快方法是知道被信号终止的进程将具有非零的返回码。因此,如果您检查脚本中的返回码,则子进程终止时可以退出自己,从而无需诸如不必要的sleep调用之类的智能操作。在整个脚本中执行此操作的快速方法是使用set -e,尽管对于退出状态为预期非零的命令可能需要进行一些调整。


1
除非使用bash-4,否则Set -e在bash中无法正常工作
schily

什么是“无法正常工作”?我已经在bash 3上成功使用了它,但是可能存在一些边缘情况。
汤姆·亨特

在一些简单的情况下,bash3确实在错误时退出。但是,这在一般情况下不会发生。典型的结果是,创建目标失败时make不会停止,而这是来自处理子目录中目标列表的makefile的。大卫·科恩(David Korn)和我不得不与bash维护者邮寄许多星期,以说服他修复bash4的错误。
schily

4
请注意,这里的问题是,ping在收到SIGINT时返回退出状态为0,bash然后忽略了接收到的SIGINT(如果是这种情况)。添加“ set -e”或检查退出状态在这里无济于事。在SIGINT上添加显式陷阱将有所帮助。
斯特凡Chazelas

4

终端会注意到Control-c并向尚未包含新的前台进程组INT的前台进程组(此处包括外壳程序)发送信号ping。这很容易通过陷印来验证INT

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

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

INT处理在弹可惊人复杂,顺便说一下,作为壳有时需要忽略该信号,并且有时不源潜水如果好奇,或思考:tail -f /etc/passwd; echo foo


在这种情况下,问题不是信号处理,而是bash在脚本中进行了jobcontrol的事实,尽管事实并非如此,请参阅我的答案以获取更多信息
schily

为了使SIGINT转到新的进程组,该命令还必须对终端执行ioctl()使其成为终端的前台进程组。ping没有理由在这里启动新的进程组,并且可以用来重现OP问题的ping版本(Debian上的iputils)不会创建进程组。
斯特凡Chazelas

1
请注意,不是终端发送SIGINT,而是接收到未转义(通常由下一个^ V表示)^ C字符的tty设备(/ dev / ttysomething设备的驱动程序(内核中的代码))的行规。从终端。
斯特凡Chazelas

2

好吧,我试图sleep 1在bash脚本中添加一个,然后砰!
现在我可以用两个停止它Ctrl+C

按下时Ctrl+CSIGINT将向当前执行的进程发送信号,该命令在循环内运行。然后,子shell进程继续执行循环中的下一个命令,从而启动另一个进程。为了能够停止脚本,必须发送两个SIGINT信号,一个信号中断正在执行的当前命令,另一个信号中断子shell进程。

在没有sleep调用的脚本中,Ctrl+C真正快速多次按下似乎不起作用,并且不可能退出循环。我的猜测是,按下两次还不够快,无法使其恰好在当前执行的过程中断到下一个过程开始之间的正确时机。每Ctrl+C按一次,将向SIGINT循环内执行的进程发送a ,但不向subshel​​l发送

在带有的脚本中sleep 1,此调用将暂停执行一秒钟,并且在被第一个Ctrl+C(第一个SIGINT)中断时,子shell将花费更多时间执行下一个命令。因此,现在,第二个Ctrl+C(second SIGINT)将进入subshel​​l,脚本执行将结束。


您误会了,在正确运行的shell上,单个^ C就足够了,请参阅我对背景的回答。
schily

好吧,考虑到您已被否决,并且目前您的答案得分为-1,我不太确信我应该认真对待您的答案。
nephewtom

有些人不赞成的事实并不总是与答复的质量有关。如果您需要键入两次^ c,那么您肯定是bash错误的受害者。您是否尝试过其他外壳?您尝试过真正的Bourne Shell吗?
schily 2015年

如果外壳程序运行正常,那么它将在同一进程组中运行脚本中的所有内容,那么一个^ c就足够了。
schily 2015年

1
@nephewtom在此答案中描述的行为可以由脚本中的不同命令(当它们收到Ctrl-C时表现不同)来解释。如果存在睡眠,则在执行睡眠的过程中极有可能会收到Ctrl-C(假设循环中的所有其他内容都很快)。睡眠被杀死,出口值为130。睡眠的父级(外壳)注意到睡眠被Sigint杀死,然后退出。但是,如果脚本不包含任何睡眠,则Ctrl-C转到ping,它以0退出作为响应,因此父外壳继续执行下一个命令。
乔纳森·哈特利

0

尝试这个:

#!/bin/bash
while true; do
   echo "Ctrl-c works during sleep 5"
   sleep 5
   echo "But not during ping -c 5"
   ping -c 5 127.0.0.1
done

现在将第一行更改为:

#!/bin/sh

然后重试-查看ping是否现在可中断。


0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

例如,pgrep -f firefox将grep正在运行的PID firefox并将其保存到名为的文件中any_file_name。'sed'命令将kill在'any_file_name'文件的PID编号的开头添加。第三行将any_file_name文件可执行。现在第四行将杀死文件中可用的PID any_file_name。将以上四行写入文件并执行该文件可以执行Control- C。对我来说工作绝对好。


0

如果有人对这个bash功能的修复感兴趣,而不是对它背后原理感兴趣,那么可以提出以下建议:

不要直接运行有问题的命令,而是从一个包装器开始:a)等待它终止b)不会弄乱信号,并且c)本身实现WCE机制,而只是在收到a时死掉SIGINT

这样的包装器可以用awk+ system()函数来制作。

$ while true; do awk 'BEGIN{system("ping -c5 localhost")}'; done
PING localhost(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.087 ms
^C
--- localhost ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.082/0.084/0.087/0.009 ms
[3]-  Terminated              ping -c5 localhost

放入像OP这样的脚本:

#!/bin/bash
while true; do
        echo -e "\n*** DATE:" `date` " ***";
        echo "********************************************"
        awk 'BEGIN{system(ARGV[1])}' "ping -c5 ${1-localhost}"
done

-3

您是一个众所周知的bash错误的受害者。Bash对脚本执行作业控制,这是一个错误。

发生的情况是bash在与脚本本身不同的进程组中运行外部程序。由于TTY进程组被设置为当前前台进程的进程组,因此只有该前台进程被杀死,shell脚本中的循环继续进行。

验证:获取并编译将pgrp(1)作为内置程序实现的最新Bourne Shell,然后将/ bin / sleep 100(或/ usr / bin / sleep取决于您的平台)添加到脚本循环,然后启动伯恩·壳。在使用ps(1)获得sleep命令和运行脚本的bash的进程ID之后,调用pgrp <pid>“ <pid>”并将其替换为sleep和运行脚本的bash的进程ID。您将看到不同的进程组ID。现在调用类似pgrp < /dev/pts/7(用脚本使用的tty替换tty名称)来获取当前的tty进程组。TTY进程组等于sleep命令的进程组。

解决方法:使用其他外壳。

Bourne Shell的最新资源在我的schily工具包中,您可以在这里找到:

http://sourceforge.net/projects/schilytools/files/


那是什么版本bashbash仅当您传递-m或-i选项时,AFAIK 才会这样做。
斯特凡Chazelas

看来这不再适用于bash4,但是当OP遇到此类问题时,他似乎使用bash3
schily

无法使用bash3.2.48,bash 3.0.16或bash-2.05b(已尝试bash -c 'ps -j; ps -j; ps -j')复制。
斯特凡Chazelas

当您将bash称为时,肯定会发生这种情况/bin/sh -ce。我必须添加一个丑陋的解决方法smake,以明确^C终止当前正在运行的命令的进程组,以允许中止分层的make调用。您是否检查过bash是否从其发起的进程组ID更改了该进程组?
schily

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'确实在所有3 ps调用中为ps和bash报告了相同的pgid。(ARGV0 = sh是zsh传递argv [0]的方法)。
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.