【问题标题】:Ctrl-C crashes Python after importing scipy.stats导入 scipy.stats 后 Ctrl-C 使 Python 崩溃
【发布时间】:2013-03-05 15:58:36
【问题描述】:

我在 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”等,我看到这种问题是由于使用了覆盖 Ctrl-C 处理的 Fortran 库引起的。我在 Scipy 跟踪器上没有看到错误,但鉴于 Scipy 是一个与 Python 一起使用的库,我认为这是一个错误。它打破了 Python 对 Ctrl-C 的处理。有什么解决方法吗?

编辑:按照@cgohlke 的建议,我尝试在导入 scipy 后添加自己的处理程序。 This question 关于一个相关问题表明添加信号处理程序不起作用。我尝试通过 pywin32 使用 Windows API SetConsoleCtrlHandler 函数:

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

在此之后,按 Ctrl-C 会打印“我在这里”,但 Python 仍然会因 forrtl 错误而崩溃。有时我也会收到一条消息说“ConsoleCtrlHandler 功能失败”,然后很快就消失了。

如果我在 IPython 中运行它,我可以在 forrtl 错误之前看到一个正常的 Python KeyboardInterrupt 回溯。如果我引发其他错误而不是 KeyboardInterrupt(例如 ValueError),我还会看到一个正常的 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) 做出反应并自行崩溃。有什么办法可以消除吗?

【问题讨论】:

  • 您是否使用由Christoph Gohlke 构建的[半非官方] MKL 链接版本的 64 位 NumPy 和 Scipy?
  • @eryksun:是的,这就是我正在使用的。
  • @eryksun:是的。好的,我会研究更新的版本。
  • @eryksun:我从该页面下载了最新版本,但错误仍然存​​在。
  • 英特尔 Fortran 运行时拥有自己的 CTRL-C 处理程序。此问题并非特定于 Scipy 或 Python。只是谷歌的错误信息。您可以尝试在from scipy import stats 之后设置自己的处理程序。

标签: python scipy keyboardinterrupt


【解决方案1】:

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

time module(第 868-876 行)和_multiprocessing module(第 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。最后,如果func 是signal.default_int_handler,你会得到一个KeyboardInterrupt 异常。

【讨论】:

  • 谢谢,这很有帮助! thread.interrupt_main 是我需要的关键工具。这就是让您从处理程序返回 True (以停止 Ctrl-C 传播)并获得 KeyboardInterrupt 引发的原因。只需使用该处理程序就可以解决 KeyboardInterrupt 问题,这是我的主要症结所在。中断sleep 的延迟不太重要,当我交互处理数据时,我无法控制是否已经导入time,所以我认为神秘的DLL加载更多比我在实践中真正需要的。
  • 这对我很有用。不幸的是,IPython 在启动期间会导入时间和大量其他模块。我可以通过将此代码放在 C:\Python27\Scripts\ipython-script.py 的开头来解决这个问题。现在 ctrl-c 可以正常工作,甚至会中断 time.sleep()。
  • 对 python3 有什么想法吗?谢谢!
【解决方案2】:

将环境变量 FOR_DISABLE_CONSOLE_CTRL_HANDLER 设置为 1 seems to fix the issue,但前提是它在加载违规包之前设置。

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

[...]

EDIT:虽然 Ctrl+C 不再使 python 崩溃,但它也无法停止当前的计算。

【讨论】:

  • 为我工作。 (Anaconda 安装)
  • 为我工作(anaconda,ipython)
  • 有效,但需要在导入违规包之前完成。如果您使用 PyCharm,请在 Python 控制台设置(环境部分)中设置此环境变量。 Python 控制台设置会在我收到提示之前导入我的所有包,但这让我认为上述解决方案不起作用。但它不起作用,因为我已经导入了有问题的包 sklearn、seaborn 和 statsmodels。设置好 env 后,加载有问题的包就可以了。
  • 在 VS Code 中也为我工作,放在所有其他导入之前。
  • 这只让我在程序执行中捕获一次 Ctrl-C 事件。任何后续的时间它都被忽略了。
【解决方案3】:

我已经能够通过这样做获得一半的解决方法:

from scipy import stats
import win32api
def doSaneThing(sig, func=None):
    return True
win32api.SetConsoleCtrlHandler(doSaneThing, 1)

在处理程序中返回 true 会停止处理程序链,以便不再调用干预的 Fortran 处理程序。但是,这种解决方法只是部分的,原因有两个:

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

尽管如此,这仍然比让整个会话崩溃要好。对我来说,这引发了一个问题,为什么 SciPy(以及任何其他依赖于这些英特尔库的 Python 库)不自己这样做。

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

【讨论】:

    【解决方案4】:

    以下是修补 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 修补lea 指令(即0xeb 后跟要跳转的字节数)

    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>
    

    所以现在jmping 将参数压入堆栈和函数调用。所以不会安装它的 Ctrl-C 处理程序。

    【讨论】:

    • 哇!您能解释一下“类似方法”是什么意思吗?在我看来,这一切都很神秘。您是如何决定在其中输入哪些数字的?
    【解决方案5】:

    解决方法:补丁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
    

    【讨论】:

    • 这似乎对我不起作用。崩溃仍然发生。 (另外,你用这种奇怪的函数风格编写代码有什么原因吗?)
    • 啊,如果我将导入更改为import scipy.stats,它会起作用。 (我不认为顶级 scipy 模块设置了处理程序。)但是,我对 Kevin Smyth 有同样的问题:你是如何决定在其中放置哪些魔法字节的?我对这样的解决方案有点紧张的原因是,对于不同版本的 Python、scipy 或 Intel 库(或者我错了),直接修补字节可能会失败(或更糟)?
    • @BrenBarn:我现在实际上更新了它,x86 和 x64 的代码不同。 C3 只是一个 retn 指令,而 C2 是一个 retn 指令,它也会从堆栈中弹出参数。后者在 x86 上是必需的,因为它需要从堆栈中弹出 8 个字节(2 个参数,每个 4 个)。在 x64 上,调用者进行清理。这是一个非常简单的补丁,所以它真的不应该破坏任何东西。一个例外是,如果英特尔库决定在此函数失败时报告失败,但它目前没有这样做,而且我不希望它在未来检查这一点。您可能应该使用它。
    • 要彻底,在ret 说明之前应该有一个mov eax,0。
    【解决方案6】:

    试试

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

    【讨论】:

    【解决方案7】:

    这对我有用:

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

    【讨论】:

    • 有效,但需要在导入有问题的包之前完成。如果您使用 PyCharm,请在 Python 控制台设置(环境部分)中设置此环境变量。 Python 控制台设置会在我收到提示之前导入我的所有包,但这让我认为上述解决方案不起作用。但它不起作用,因为我已经导入了有问题的包 sklearn、seaborn 和 statsmodels。设置好 env 后,加载有问题的包就可以了。
    猜你喜欢
    • 1970-01-01
    • 2016-06-18
    • 1970-01-01
    • 1970-01-01
    • 2018-04-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多