bash 使用 KeyboardInterrupt 异常捕获 SIGINT 在终端中有效,而不是在脚本中

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/40775054/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-18 15:26:55  来源:igfitidea点击:

Capturing SIGINT using KeyboardInterrupt exception works in terminal, not in script

pythonbashshellsigint

提问by Velda

I'm trying to catch SIGINT(or keyboard interrupt) in Python 2.7 program. This is how my Python test script testlooks:

我正在尝试SIGINT在 Python 2.7 程序中捕获(或键盘中断)。这是我的 Python 测试脚本的test外观:

#!/usr/bin/python

import time

try:
    time.sleep(100)
except KeyboardInterrupt:
    pass
except:
    print "error"

Next I have a shell script test.sh:

接下来我有一个 shell 脚本test.sh

./test & pid=$!
sleep 1
kill -s 2 $pid

When I run the script with bash, or sh, or something bash test.sh, the Python process teststays running and is not killable with SIGINT. Whereas when I copy test.shcommand and paste it into (bash) terminal, the Python process testshuts down.

当我使用 bash 或 sh 或其他东西运行脚本时,bash test.shPython 进程test保持运行并且不能被SIGINT. 而当我复制test.sh命令并将其粘贴到 (bash) 终端时,Python 进程test将关闭。

I cannot get what's going on, which I'd like to understand. So, where is difference, and why?

我无法理解发生了什么,我想了解一下。那么,差异在哪里,为什么?

This is not about how to catch SIGINTin Python!According to docs– this is the way, which should work:

这不是关于如何SIGINT在 Python 中捕捉!根据文档- 这是应该工作的方式:

Python installs a small number of signal handlers by default: SIGPIPE ... and SIGINT is translated into a KeyboardInterrupt exception

Python 默认安装少量信号处理程序: SIGPIPE ... 和 SIGINT 被翻译成 KeyboardInterrupt 异常

It is indeed catching KeyboardInterruptwhen SIGINTis sent by killif the program is started directly from shell, but when the program is started from bash script run on background, it seems that KeyboardInterruptis never raised.

如果程序直接从shell启动,它确实会捕获KeyboardInterruptwhenSIGINT发送kill,但是当程序从后台运行的bash脚本启动时,似乎KeyboardInterrupt从未引发过。

回答by mata

There is one case in which the default sigint handler is notinstalled at startup, and that is when the signal mask contains SIG_IGNfor SIGINTat program startup. The code responsible for this can be found here.

有一种情况是在启动时没有安装默认的 sigint 处理程序,那就是在程序启动时信号掩码包含SIG_IGNfor SIGINT。可以在此处找到对此负责的代码。

The signal mask for ignored signals is inherited from the parent process, while handled signals are reset to SIG_DFL. So in case SIGINTwas ignored the condition if (Handlers[SIGINT].func == DefaultHandler)in the source won't trigger and the default handler is not installed, python doesn't override the settings made by the parent process in this case.

忽略信号的信号掩码继承自父进程,而处理的信号则重置为SIG_DFL。因此,如果SIGINT忽略if (Handlers[SIGINT].func == DefaultHandler)源中的条件不会触发并且未安装默认处理程序,则在这种情况下,python 不会覆盖父进程所做的设置。

So let's try to show the used signal handler in different situations:

因此,让我们尝试显示在不同情况下使用的信号处理程序:

# invocation from interactive shell
$ python -c "import signal; print(signal.getsignal(signal.SIGINT))"
<built-in function default_int_handler>

# background job in interactive shell
$ python -c "import signal; print(signal.getsignal(signal.SIGINT))" &
<built-in function default_int_handler>

# invocation in non interactive shell
$ sh -c 'python -c "import signal; print(signal.getsignal(signal.SIGINT))"'
<built-in function default_int_handler>

# background job in non-interactive shell
$ sh -c 'python -c "import signal; print(signal.getsignal(signal.SIGINT))" &'
1

So in the last example, SIGINTis set to 1 (SIG_IGN). This is the same as when you start a background job in a shell script, as those are non interactive by default (unless you use the -ioption in the shebang).

所以在最后一个例子中,SIGINT被设置为 1 ( SIG_IGN)。这与在 shell 脚本中启动后台作业时相同,因为默认情况下它们是非交互式的(除非您-i在 shebang 中使用该选项)。

So this is caused by the shell ignoring the signal when launching a background job in a non interactive shell session, not by python directly. At least bashand dashbehave this way, I've not tried other shells.

所以这是由 shell 在非交互式 shell 会话中启动后台作业时忽略信号引起的,而不是直接由 python 引起的。至少bash并且dash以这种方式行事,我还没有尝试过其他外壳。

There are two options to deal with this situation:

有两种选择来处理这种情况:

  • manually install the default signal handler:

    import signal
    signal.signal(signal.SIGINT, signal.default_int_handler)
    
  • add the -ioption to the shebang of the shell script, e.g:

    #!/bin/sh -i
    
  • 手动安装默认信号处理程序:

    import signal
    signal.signal(signal.SIGINT, signal.default_int_handler)
    
  • -i选项添加到 shell 脚本的shebang,例如:

    #!/bin/sh -i
    


edit: this behaviour is documented in the bash manual:

编辑:此行为记录在 bash 手册中:

SIGNALS
...
When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

SIGNALS
...
当作业控制无效时,除了这些继承的处理程序之外,异步命令还会忽略 SIGINT 和 SIGQUIT。

which applies to non-interactive shells as they have job control disabled by default, and is actually specified in POSIX: Shell Command Language

这适用于非交互式 shell,因为它们默认禁用作业控制,实际上在 POSIX 中指定:Shell Command Language