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

Posted

技术标签:

【中文标题】使用 KeyboardInterrupt 异常捕获 SIGINT 在终端中有效,而不是在脚本中【英文标题】:Capturing SIGINT using KeyboardInterrupt exception works in terminal, not in script 【发布时间】:2017-04-08 02:06:23 【问题描述】:

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

#!/usr/bin/python

import time

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

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

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

当我使用 bash 或 sh 或 bash test.sh 运行脚本时,Python 进程 test 保持运行,并且无法使用 SIGINT 杀死。而当我复制test.sh 命令并将其粘贴到(bash)终端时,Python 进程test 将关闭。

我不知道发生了什么,我想了解一下。那么,差异在哪里,为什么?

这不是关于 如何在 Python 中捕获 SIGINT 根据 docs - 这是应该工作的方式:

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

如果程序直接从shell启动,当kill发送SIGINT时确实捕获KeyboardInterrupt,但是当程序从后台运行的bash脚本启动时,似乎KeyboardInterrupt永远不会提出来。

【问题讨论】:

很好的问题,我今天学到的第一个关于 Python 的新东西。 【参考方案1】:

有一种情况是在启动时安装默认的sigint处理程序,即在程序启动时信号掩码包含SIG_IGN for SIGINT。可以找到负责此操作的代码here。

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

所以让我们尝试在不同的情况下展示使用的信号处理程序:

# 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

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

所以这是由于 shell 在非交互式 shell 会话中启动后台作业时忽略了信号,而不是由 python 直接引起的。至少bashdash 的行为是这样的,我没有尝试过其他shell。

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

手动安装默认信号处理程序:

import signal
signal.signal(signal.SIGINT, signal.default_int_handler)

在shell脚本的shebang中添加-i选项,例如:

#!/bin/sh -i

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

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

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

【讨论】:

感谢您的精彩解释。尽管如此,Python 文档还是有点误导,不是吗?我认为它应该说明,如果在启动信号掩码中没有忽略它,它只会添加 SIGINT 处理程序。 是的,文档可能应该提到它在启动时未设置为 SIG_DFL 时不会设置 sigint 处理程序

以上是关于使用 KeyboardInterrupt 异常捕获 SIGINT 在终端中有效,而不是在脚本中的主要内容,如果未能解决你的问题,请参考以下文章

在 Python 中捕获所有异常的坏主意

如何在 C++ 中传递/捕获/响应 Python 的 KeyboardInterrupt?

Python 3 基础之异常及其捕获

python中--try except 异常捕获以及正则化替换异常值

线程忽略 KeyboardInterrupt 异常

3-4 Python异常机制常考题