Ctrl-C在导入scipy.stats后使Python崩溃



我在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计算。

搜索"Fortran"之类的,我看到这种问题是由于使用了覆盖Ctrl-C处理的Fortran库。我没有在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仍然会因为fortl错误而崩溃。有时我也得到一个消息说"ConsoleCtrlHandler函数失败",这很快就消失了。

如果我在ippython中运行这个,我可以在fortl错误之前看到一个正常的Python KeyboardInterrupt跟踪。如果我引发其他错误而不是KeyboardInterrupt(例如ValueError),我还会看到一个正常的Python回溯,后面跟着fortl错误:

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)做出反应并使自己崩溃。有什么办法可以消除这种情况吗?

这是你发布的解决方案的一个变化,可能会起作用。也许有更好的方法来解决这个问题——或者甚至可以通过设置一个环境变量来告诉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预加载libicoremd .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异常。

将环境变量FOR_DISABLE_CONSOLE_CTRL_HANDLER设置为1似乎可以解决此问题,但仅当加载问题包之前设置

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

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

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

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

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

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

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

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

下面是修补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, 'xebx0b'),
  '0f86dcd44a1c2e217054c50262f727bf': (0x3fdd9, 'xebx10')}[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: .venvScriptspython.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:WINDOWSSYSTEM32python27.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:UserskevinDocuments\venvlibsite-packagesnumpycorelibifcoremd.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: .venvScriptspython.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>

所以现在我们在jmp上压入堆栈上的参数和函数调用。因此不会安装它的Ctrl-C处理程序。

解决方法:patch SetControlCtrlHandler

import ctypes
SetConsoleCtrlHandler_body_new = b'xC2x08x00' 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 os
os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1'
from scipy.stats import zscore

Try

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

相关内容

  • 没有找到相关文章

最新更新