在调试另一个问题时,我发现如果使用
&
从 shell 脚本启动 Python,SIGINT 的信号处理设置将更改为忽略它。
x.py
内容:
import signal
print(signal.getsignal(signal.SIGINT))
noproblem.sh
内容:
python3 x.py
problem.sh
内容:
python3 x.py &
直接运行
x.py
、直接使用&
或通过noproblem.sh
运行时,SIGINT的信号处理程序是默认的signal.default_int_handler
,它负责引发键盘中断:
07:14 ~ $ python3 x.py
<built-in function default_int_handler>
07:14 ~ $ python3 x.py &
[1] 126909
07:14 ~ $ <built-in function default_int_handler>
[1]+ Done python3 x.py
07:14 ~ $ bash noproblem.sh
<built-in function default_int_handler>
但是通过
x.py
运行 problem.sh
,SIGINT 会被忽略:
07:14 ~ $ bash problem.sh
07:14 ~ $ Handlers.SIG_IGN
signal
模块文档没有提及此行为。这是故意的还是bug?
经过进一步挖掘,我成功找到了原因。
首先,如果 Python 从其父进程继承 SIG_IGN 的操作系统级别设置,则它不会安装其默认的 SIGINT 处理程序。它仅在继承 SIG_DFL 设置时安装该处理程序。我们可以在 signal
模块的
源代码中看到这一点:
IntHandler = PyDict_GetItemString(d, "default_int_handler");
if (!IntHandler)
goto finally;
Py_INCREF(IntHandler);
_Py_atomic_store_relaxed(&Handlers[0].tripped, 0);
for (i = 1; i < NSIG; i++) {
void (*t)(int);
t = PyOS_getsig(i);
_Py_atomic_store_relaxed(&Handlers[i].tripped, 0);
if (t == SIG_DFL)
Handlers[i].func = DefaultHandler;
else if (t == SIG_IGN)
Handlers[i].func = IgnoreHandler;
else
Handlers[i].func = Py_None; /* None of our business */
Py_INCREF(Handlers[i].func);
}
if (Handlers[SIGINT].func == DefaultHandler) {
/* Install default int handler */
Py_INCREF(IntHandler);
Py_SETREF(Handlers[SIGINT].func, IntHandler);
PyOS_setsig(SIGINT, signal_handler);
}
其次,shell 脚本默认在禁用作业控制的情况下运行,并且当禁用作业控制时以
&
启动的进程会继承 SIGINT 和 SIGQUIT 的 SIG_IGN 设置。引用POSIX:
如果在 shell 执行异步列表时禁用作业控制(请参阅 set -m 的说明),则列表中的命令将从 shell 继承 SIGINT 和 SIGQUIT 信号的忽略 (SIG_IGN) 信号操作。
我找不到明确的标准引用说在 shell 脚本中默认禁用作业控制,只有 quotes 说交互式 shell 默认启用作业控制(隐含地暗示非交互式 shell 的相反设置):
-米
如果实现支持用户可移植性实用程序选项,则应支持此选项。所有作业都应在其自己的进程组中运行。在后台作业完成后 shell 立即发出提示之前,应将报告后台作业退出状态的消息写入标准错误。如果前台作业停止,shell 应向标准错误写入一条消息以达到该效果,其格式如作业实用程序所描述。此外,如果作业更改状态而不是退出(例如,如果它停止输入或输出或被 SIGSTOP 信号停止),则 shell 应在写入下一个提示之前立即写入类似的消息。 交互式 shell 默认启用此选项。