使用import scipy.stats后按Ctrl-C会导致Python崩溃

44

我正在Win7 64位上运行64位Python 2.7.3。通过执行以下代码,我可以可靠地使Python解释器崩溃:

>>> from scipy import stats
>>> import time
>>> time.sleep(3)

在睡眠期间按Control-C不会引发KeyboardInterrupt异常,而是使解释器崩溃。以下内容将被输出:

forrtl: error (200): program aborting due to control-C event
Image              PC                Routine            Line        Source

libifcoremd.dll    00000000045031F8  Unknown               Unknown  Unknown
libifcoremd.dll    00000000044FC789  Unknown               Unknown  Unknown
libifcoremd.dll    00000000044E8583  Unknown               Unknown  Unknown
libifcoremd.dll    000000000445725D  Unknown               Unknown  Unknown
libifcoremd.dll    00000000044672A6  Unknown               Unknown  Unknown
kernel32.dll       0000000077B74AF3  Unknown               Unknown  Unknown
kernel32.dll       0000000077B3F56D  Unknown               Unknown  Unknown
ntdll.dll          0000000077C73281  Unknown               Unknown  Unknown

这使得中途打断Scipy长时间计算变得不可能。

在谷歌上搜索 "forrtl" 等关键词,我发现有人建议这种问题是由于使用Fortran库覆盖了 Ctrl-C 处理而导致的。虽然Scipy跟踪器上没有发现bug,但考虑到Scipy是用于Python的库,我认为这确实是一个bug。它破坏了Python对Ctrl-C的处理。有没有任何解决方法呢?

编辑:根据 @cgohlke 的建议,在导入 Scipy 后尝试添加自己的处理程序。与相关问题有关的 这个问题 显示添加信号处理程序无效。我尝试使用Windows API SetConsoleCtrlHandler函数通过pywin32:

from scipy import stats
import win32api
def doSaneThing(sig, func=None):
    print "Here I am"
    raise KeyboardInterrupt
win32api.SetConsoleCtrlHandler(doSaneThing, 1)

之后,按下Ctrl-C会打印出“Here I am”,但Python仍会崩溃并显示“forrtl错误”。有时我还会收到一条消息,显示“ConsoleCtrlHandler函数失败”,这个消息很快就消失了。

如果我在IPython中运行这个程序,就会在forrtl错误之前看到一个正常的Python KeyboardInterrupt回溯。如果我引发其他错误(例如ValueError)而不是KeyboardInterrupt,那么我也会看到一个正常的Python回溯,然后是forrtl错误。

ValueError                                Traceback (most recent call last)
<ipython-input-1-08defde66fcb> in doSaneThing(sig, func)
      3 def doSaneThing(sig, func=None):
      4     print "Here I am"
----> 5     raise ValueError
      6 win32api.SetConsoleCtrlHandler(doSaneThing, 1)

ValueError:
forrtl: error (200): program aborting due to control-C event
[etc.]

看起来无论底层处理程序在做什么,它不仅仅是直接捕获了Ctrl-C信号,而是对错误条件(ValueError)做出了反应并崩溃了。有没有办法消除这种情况?


1
英特尔Fortran运行时有自己的CTRL-C处理程序。这个问题不是特定于Scipy或Python的。只需在Google中搜索错误消息即可。您可以尝试在from scipy import stats之后设置自己的处理程序。 - cgohlke
我之前访问过这个问题(当我遇到同样的问题时),并使用了类似下面的代码。我刚刚重新测试了Python 2.7.10和numpy-1.9.2+mkl-cp27-none-win_amd64.whl以及来自Christoph Gohlke的scipy-0.16.0-cp27-none-win_amd64.whl,我无法重现这个问题。这很好。 - Steve Jessop
@SteveJessop:哇,那太好了。我得尝试升级一下。 - BrenBarn
对于 Intel Fortran >=16,请设置 FOR_DISABLE_CONSOLE_CTRL_HANDLER=1 环境变量。不幸的是,该版本不再支持 Python 2.7 所需的 Visual Studio 2008。 - cgohlke
@cgohlke:感谢更新,如果我最终使用那个版本,我会记住这个。令人烦恼的是,旧版Fortran似乎没有提供一种方法来做到这一点。 - BrenBarn
显示剩余7条评论
7个回答

24
这是您发布的解决方案的一个变体,可能会起作用。也许有更好的方法来解决这个问题,甚至可以通过设置环境变量告诉DLL跳过安装处理程序来避免它。希望这可以帮助您找到更好的方法之前使用。

time 模块(第 868-876 行)和 _multiprocessing 模块(第 312-321 行)都会调用 SetConsoleCtrlHandler 函数。在 time 模块中,其控制台控制处理程序设置了一个 Windows 事件,hInterruptEvent。对于主线程,time.sleep 通过 WaitForSingleObject(hInterruptEvent, ul_millis) 等待此事件,其中 ul_millis 是等待毫秒数,除非被 Ctrl+C 中断。由于您安装的处理程序返回 True,因此无法调用 time 模块的处理程序来设置 hInterruptEvent,这意味着无法中断 sleep

我尝试使用imp.init_builtin('time')重新初始化time模块,但显然SetConsoleCtrlHandler忽略了第二次调用。似乎必须删除处理程序,然后重新插入。不幸的是,time模块没有导出此功能。因此,为了应付这种情况,请确保在安装处理程序之后导入time模块。scipy也会导入time,因此需要使用ctypes预加载libifcoremd.dll以正确排序处理程序。最后,添加一个调用thread.interrupt_main,以确保调用Python的SIGINT处理程序[1]
例如:
import os
import imp
import ctypes
import thread
import win32api

# Load the DLL manually to ensure its handler gets
# set before our handler.
basepath = imp.find_module('numpy')[1]
ctypes.CDLL(os.path.join(basepath, 'core', 'libmmd.dll'))
ctypes.CDLL(os.path.join(basepath, 'core', 'libifcoremd.dll'))

# Now set our handler for CTRL_C_EVENT. Other control event 
# types will chain to the next handler.
def handler(dwCtrlType, hook_sigint=thread.interrupt_main):
    if dwCtrlType == 0: # CTRL_C_EVENT
        hook_sigint()
        return 1 # don't chain to the next handler
    return 0 # chain to the next handler

win32api.SetConsoleCtrlHandler(handler, 1)

>>> import time
>>> from scipy import stats
>>> time.sleep(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyboardInterrupt

[1] interrupt_main 调用 PyErr_SetInterrupt。这将触发 Handlers[SIGINT] 并调用 Py_AddPendingCall 来添加 checksignals_witharg。接着,这会调用 PyErr_CheckSignals。由于Handlers[SIGINT]被触发,这会调用 Handlers[SIGINT].func。最后,如果 funcsignal.default_int_handler,则会引发 KeyboardInterrupt 异常。

谢谢,这非常有帮助!thread.interrupt_main是我需要的关键工具。这就是让你从处理程序中返回True(停止Ctrl-C传播)并引发KeyboardInterrupt的方法。只使用该处理程序即可解决KeyboardInterrupt问题,这是我遇到的主要难点。中断sleep的延迟不那么重要,当我在交互式地玩耍数据时,我无法控制是否已经导入了time,因此我认为加载奥妙的DLL超出了我实际需要的范围。 - BrenBarn
这对我非常有效。不幸的是,IPython在启动期间导入了时间和大量其他模块。我能够通过将此代码放在C:\Python27\Scripts\ipython-script.py的开头来解决这个问题。现在ctrl-c可以正确工作,甚至可以中断time.sleep()。 - Evan
有关Python3的任何想法吗?谢谢! - ch271828n

20

把环境变量FOR_DISABLE_CONSOLE_CTRL_HANDLER设置为1似乎可以解决问题,但只有在加载有问题的软件包之前设置才有效。

import os
os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1'

[...]

编辑:虽然现在Ctrl+C不会再让Python崩溃,但它也无法停止当前的计算。


2
安装成功了(Anaconda 安装) - unddoch
对我有用(anaconda,ipython) - Janus
3
需要在导入有问题的包之前完成此操作。如果您使用PyCharm,请在Python Console设置(环境部分)中设置此环境变量。Python控制台设置在我获得提示之前导入了所有的包,作为方便,但这让我认为上述解决方案无效。但是,它无效是因为我已经导入了有问题的sklearn、seaborn和statsmodels包。一旦设置了环境变量,那么导入有问题的包就没有问题了。 - jedi
在 VS Code 中对我也起作用,放在所有其他导入之前。 - user49404
这只允许我在程序执行期间捕获一次Ctrl-C事件。任何后续的尝试都会被忽略。 - jr15
这对我来说并没有停止执行 - 这个解决方案是有效的。 - Joakim

4
我已经能够通过以下方法得到部分解决方案:
from scipy import stats
import win32api
def doSaneThing(sig, func=None):
    return True
win32api.SetConsoleCtrlHandler(doSaneThing, 1)

在处理程序中返回 true 可以停止处理程序链,从而不再调用干扰的 Fortran 处理程序。但是,由于两个原因,这种解决方法仅部分有效:

  1. 它实际上没有引发 KeyboardInterrupt,这意味着我无法在 Python 代码中对其做出反应。它只是将我带回提示符。
  2. 它不能像在 Python 中通常情况下 Ctrl-C 那样完全中断事物。如果在一个新的 Python 会话中执行 time.sleep(3) 并按下 Ctrl-C,则 sleep 立即中止并出现 KeyboardInterrupt。使用上述解决方法,sleep 不会中止,只有在等待时间结束后才返回到提示符。

尽管如此,这仍然比崩溃整个会话要好。对我来说,这引出了一个问题,为什么 SciPy(以及任何依赖这些 Intel 库的 Python 库)不自己做这件事。

我保留这个答案未被接受,希望有人能提供真正的解决方案或解决方法。所谓“真正”,是指在长时间运行的 SciPy 计算过程中按下 Ctrl-C 应该像没有加载 SciPy 时那样工作。(请注意,这并不意味着它必须立即工作。像纯 Python 的 sum(xrange(100000000)) 这样的非 SciPy 计算可能不会立即在 Ctrl-C 上中止,但至少当它们这样做时,它们会引发 KeyboardInterrupt。)


3
这里是修补dll的代码,以删除安装Ctrl-C处理程序的调用:
import os
import os.path
import imp
import hashlib

basepath = imp.find_module('numpy')[1]
ifcoremd = os.path.join(basepath, 'core', 'libifcoremd.dll')
with open(ifcoremd, 'rb') as dll:
    contents = dll.read()

m = hashlib.md5()
m.update(contents)

patch = {'7cae928b035bbdb90e4bfa725da59188': (0x317FC, '\xeb\x0b'),
  '0f86dcd44a1c2e217054c50262f727bf': (0x3fdd9, '\xeb\x10')}[m.hexdigest()]
if patch:
    contents = bytearray(contents)
    contents[patch[0]:patch[0] + len(patch[1])] = patch[1]
    with open(ifcoremd, 'wb') as dll:
        dll.write(contents)
else:
    print 'Unknown dll version'

编辑:以下是我如何添加x64补丁的步骤。在调试器中运行python.exe,并为SetConsoleCtrlHandler设置断点,直到您找到要修补的调用为止:

Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: .\venv\Scripts\python.exe
...
0:000> .symfix
0:000> bp kernel32!SetConsoleCtrlHandler
0:000> g
Breakpoint 0 hit
KERNEL32!SetConsoleCtrlHandler:
00007ffc`c25742f0 ff252af00400    jmp     qword ptr [KERNEL32!_imp_SetConsoleCtrlHandler (00007ffc`c25c3320)] ds:00007ffc`c25c3320={KERNELBASE!SetConsoleCtrlHandler (00007ffc`bfa12e10)}
0:000> k 5
Child-SP          RetAddr           Call Site
00000000`007ef7a8 00000000`71415bb4 KERNEL32!SetConsoleCtrlHandler
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\SYSTEM32\python27.dll -
00000000`007ef7b0 00000000`7035779f MSVCR90!signal+0x17c
00000000`007ef800 00000000`70237ea7 python27!PyOS_getsig+0x3f
00000000`007ef830 00000000`703546cc python27!Py_Main+0x21ce7
00000000`007ef880 00000000`7021698c python27!Py_InitializeEx+0x40c
0:000> g
Python 2.7.11 (v2.7.11:6d1b6a68f775, Dec  5 2015, 20:40:30) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
...
Breakpoint 0 hit
KERNEL32!SetConsoleCtrlHandler:
00007ffc`c25742f0 ff252af00400    jmp     qword ptr [KERNEL32!_imp_SetConsoleCtrlHandler (00007ffc`c25c3320)] ds:00007ffc`c25c3320={KERNELBASE!SetConsoleCtrlHandler (00007ffc`bfa12e10)}
0:000> k 5
Child-SP          RetAddr           Call Site
00000000`007ec308 00000000`7023df6e KERNEL32!SetConsoleCtrlHandler
00000000`007ec310 00000000`70337877 python27!PyTime_DoubleToTimet+0x10ee
00000000`007ec350 00000000`7033766d python27!PyImport_IsScript+0x4f7
00000000`007ec380 00000000`70338bf2 python27!PyImport_IsScript+0x2ed
00000000`007ec3b0 00000000`703385a9 python27!PyImport_ImportModuleLevel+0xc82
0:000> g
...
>>> import scipy.stats
...
Breakpoint 0 hit
KERNEL32!SetConsoleCtrlHandler:
00007ffc`c25742f0 ff252af00400    jmp     qword ptr [KERNEL32!_imp_SetConsoleCtrlHandler (00007ffc`c25c3320)] ds:00007ffc`c25c3320={KERNELBASE!SetConsoleCtrlHandler (00007ffc`bfa12e10)}
0:000> k 5
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Users\kevin\Documents\\venv\lib\site-packages\numpy\core\libifcoremd.dll -
Child-SP          RetAddr           Call Site
00000000`007ed818 00007ffc`828309eb KERNEL32!SetConsoleCtrlHandler
00000000`007ed820 00007ffc`828dfa44 libifcoremd!GETEXCEPTIONPTRSQQ+0xdb
00000000`007ed880 00007ffc`828e59d7 libifcoremd!for_lt_ne+0xc274
00000000`007ed8b0 00007ffc`828e5aff libifcoremd!for_lt_ne+0x12207
00000000`007ed8e0 00007ffc`c292ddc7 libifcoremd!for_lt_ne+0x1232f
0:000> ub  00007ffc`828309eb
libifcoremd!GETEXCEPTIONPTRSQQ+0xbb:
00007ffc`828309cb 00e8            add     al,ch
00007ffc`828309cd df040b          fild    word ptr [rbx+rcx]
00007ffc`828309d0 0033            add     byte ptr [rbx],dh
00007ffc`828309d2 c9              leave
00007ffc`828309d3 ff15bf390e00    call    qword ptr [libifcoremd!for_lt_ne+0x40bc8 (00007ffc`82914398)]
00007ffc`828309d9 488d0d00efffff  lea     rcx,[libifcoremd!for_rtl_finish_+0x20 (00007ffc`8282f8e0)]
00007ffc`828309e0 ba01000000      mov     edx,1
00007ffc`828309e5 ff158d390e00    call    qword ptr [libifcoremd!for_lt_ne+0x40ba8 (00007ffc`82914378)]

我们将使用相对jmp指令(即0xeb加上要跳转的字节数)替换掉lea指令。

0:000> ? 00007ffc`828309eb - 00007ffc`828309d9
Evaluate expression: 18 = 00000000`00000012
0:000> f 00007ffc`828309d9 L2 eb 10
Filled 0x2 bytes
0:000> ub  00007ffc`828309eb
libifcoremd!GETEXCEPTIONPTRSQQ+0xbe:
00007ffc`828309ce 040b            add     al,0Bh
00007ffc`828309d0 0033            add     byte ptr [rbx],dh
00007ffc`828309d2 c9              leave
00007ffc`828309d3 ff15bf390e00    call    qword ptr [libifcoremd!for_lt_ne+0x40bc8 (00007ffc`82914398)]
00007ffc`828309d9 eb10            jmp     libifcoremd!GETEXCEPTIONPTRSQQ+0xdb (00007ffc`828309eb)
00007ffc`828309db 0d00efffff      or      eax,0FFFFEF00h
00007ffc`828309e0 ba01000000      mov     edx,1
00007ffc`828309e5 ff158d390e00    call    qword ptr [libifcoremd!for_lt_ne+0x40ba8 (00007ffc`82914378)]

我不知道这个进程中的.dll文件是如何映射的,所以我会用十六进制编辑器在文件中搜索0d 00 ef ff ff。这是一个独特的命中,因此我们可以计算出要修补的.dll文件中的位置。

0:000> db  00007ffc`828309d0
00007ffc`828309d0  00 33 c9 ff 15 bf 39 0e-00 eb 10 0d 00 ef ff ff  .3....9.........
00007ffc`828309e0  ba 01 00 00 00 ff 15 8d-39 0e 00 48 8d 0d 0e 9c  ........9..H....
00007ffc`828309f0  09 00 e8 09 2e 0a 00 48-8d 0d 32 9f 09 00 e8 fd  .......H..2.....
00007ffc`82830a00  2d 0a 00 48 8d 0d ca ee-0e 00 e8 51 90 00 00 85  -..H.......Q....
00007ffc`82830a10  c0 0f 85 88 02 00 00 e8-38 fa 0a 00 ff 15 4e 39  ........8.....N9
00007ffc`82830a20  0e 00 89 c1 e8 d7 2d 0a-00 48 8d 05 f8 be 11 00  ......-..H......
00007ffc`82830a30  45 32 e4 c7 05 0b 4a 13-00 00 00 00 00 41 bd 01  E2....J......A..
00007ffc`82830a40  00 00 00 48 89 05 06 4a-13 00 ff 15 30 39 0e 00  ...H...J....09..
0:000> ? 00007ffc`828309d9 -  00007ffc`828309d0
Evaluate expression: 9 = 00000000`00000009
0:000> ? 00007ffc`828309d9 -  00007ffc`828309d0 + 3FDD0
Evaluate expression: 261593 = 00000000`0003fdd9
0:000>

好的,我已经在0x3fdd9处修补了dll。现在让我们看看它现在的样子:

Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: .\venv\Scripts\python.exe
...
0:000> bp libifcoremd!GETEXCEPTIONPTRSQQ+c9
Bp expression 'libifcoremd!GETEXCEPTIONPTRSQQ+c9' could not be resolved, adding deferred bp
0:000> g
Python 2.7.11 (v2.7.11:6d1b6a68f775, Dec  5 2015, 20:40:30) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import scipy.stats
...
Breakpoint 0 hit
libifcoremd!GETEXCEPTIONPTRSQQ+0xc9:
00007ffc`845909d9 eb10            jmp     libifcoremd!GETEXCEPTIONPTRSQQ+0xdb (00007ffc`845909eb)
0:000> u
libifcoremd!GETEXCEPTIONPTRSQQ+0xc9:
00007ffc`845909d9 eb10            jmp     libifcoremd!GETEXCEPTIONPTRSQQ+0xdb (00007ffc`845909eb)
00007ffc`845909db 0d00efffff      or      eax,0FFFFEF00h
00007ffc`845909e0 ba01000000      mov     edx,1
00007ffc`845909e5 ff158d390e00    call    qword ptr [libifcoremd!for_lt_ne+0x40ba8 (00007ffc`84674378)]
00007ffc`845909eb 488d0d0e9c0900  lea     rcx,[libifcoremd!GETHANDLEQQ (00007ffc`8462a600)]
00007ffc`845909f2 e8092e0a00      call    libifcoremd!for_lt_ne+0x30 (00007ffc`84633800)
00007ffc`845909f7 488d0d329f0900  lea     rcx,[libifcoremd!GETUNITQQ (00007ffc`8462a930)]
00007ffc`845909fe e8fd2d0a00      call    libifcoremd!for_lt_ne+0x30 (00007ffc`84633800)
0:000>

现在我们跳过把参数推到堆栈和函数调用的过程。所以它的Ctrl-C处理程序将不会被安装。


哇!你能解释一下你所说的“类似的方法”是什么意思吗?对我来说,这看起来很神秘。你是如何决定在那里放入什么数字的呢? - BrenBarn

2
解决方法:补丁SetControlCtrlHandler
import ctypes
SetConsoleCtrlHandler_body_new = b'\xC2\x08\x00' if ctypes.sizeof(ctypes.c_void_p) == 4 else b'\xC3'
try: SetConsoleCtrlHandler_body = (lambda kernel32: (lambda pSetConsoleCtrlHandler:
    kernel32.VirtualProtect(pSetConsoleCtrlHandler, ctypes.c_size_t(1), 0x40, ctypes.byref(ctypes.c_uint32(0)))
    and (ctypes.c_char * 3).from_address(pSetConsoleCtrlHandler.value)
)(ctypes.cast(kernel32.SetConsoleCtrlHandler, ctypes.c_void_p)))(ctypes.windll.kernel32)
except: SetConsoleCtrlHandler_body = None
if SetConsoleCtrlHandler_body:
    SetConsoleCtrlHandler_body_old = SetConsoleCtrlHandler_body[0:len(SetConsoleCtrlHandler_body_new)]
    SetConsoleCtrlHandler_body[0:len(SetConsoleCtrlHandler_body_new)] = SetConsoleCtrlHandler_body_new
try:
    import scipy.stats
finally:
    if SetConsoleCtrlHandler_body:
        SetConsoleCtrlHandler_body[0:len(SetConsoleCtrlHandler_body_new)] = SetConsoleCtrlHandler_body_old

它似乎对我无效。崩溃仍然发生。(另外,你为什么要用那种奇怪的函数式风格编写代码?) - BrenBarn
啊,如果我把导入语句改成import scipy.stats,它就能工作了。(我认为顶层的scipy模块没有设置处理程序。)然而,我有一个和之前问Kevin Smyth的同样问题要问你:你是怎么决定放哪些魔术字节进去的呢?我有点担心这样的解决方案,因为直接修补字节可能会失败(或更糟),针对不同版本的Python、scipy或英特尔库(或者我对此所知不足吗)? - BrenBarn
@BrenBarn:我已经更新了代码,x86和x64的代码是不同的。C3只是一个retn指令,而C2是一个retn指令,它还从堆栈中弹出参数。后者在x86上是必要的,因为它需要从堆栈中弹出8个字节(两个参数,每个4个)。在x64上,调用方会清理。这是一个非常简单的补丁,所以它真的不应该破坏任何东西。唯一的例外是如果英特尔库决定在此函数失败时报告错误,但它目前并没有这样做,我也不指望它将来会检查这个问题。你应该使用它。 - user541686
为了彻底,应该在ret指令之前加上mov eax,0 - Kevin Smyth

2
这对我有用:

这对我有用:

import os
os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1'
from scipy.stats import zscore

2
可以正常运行,但在导入有问题的包之前需要进行操作。如果您使用PyCharm,请在Python Console设置(环境部分)中设置此环境变量。Python Console设置会在我获得提示之前导入所有的包,这让我认为上述解决方案不起作用。但实际上它不起作用是因为我已经导入了有问题的包sklearn、seaborn和statsmodels。一旦设置了环境变量,那么导入有问题的包就没问题了。 - jedi

1

尝试

import os
os.environ['FOR_IGNORE_EXCEPTIONS'] = '1'
import scipy.stats

2
它不起作用。我尝试将其设置为“1”和“TRUE”。它似乎只适用于运行时异常,而不是默认的控制台事件处理。 - Eryk Sun
1
这对我确实起作用,是一种更简单的解决方案。相关的 Github 问题:https://github.com/ContinuumIO/anaconda-issues/issues/905#issuecomment-232498034 - islandman93

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接