如何在Windows机器上处理python中的信号

时间:2016-03-03 12:17:35

标签: python windows signals

我正在尝试下面粘贴在Windows上的代码,但它不是处理信号,而是杀死了这个过程。 但是,相同的代码在Ubuntu中工作。

import os, sys
import time
import signal
def func(signum, frame):
    print 'You raised a SigInt! Signal handler called with signal', signum

signal.signal(signal.SIGINT, func)
while True:
    print "Running...",os.getpid()
    time.sleep(2)
    os.kill(os.getpid(),signal.SIGINT)

1 个答案:

答案 0 :(得分:60)

Python os.kill在Windows上包含两个不相关的API。当sig参数为CTRL_C_EVENTCTRL_BREAK_EVENT时,它会调用GenerateConsoleCtrlEvent。在这种情况下,pid参数是进程组ID。如果后一个调用失败,并且对于所有其他sig值,则调用OpenProcess然后调用TerminateProcess。在这种情况下,pid参数是进程ID,sig值作为退出代码传递。终止Windows进程类似于将SIGKILL发送到POSIX进程。通常这应该避免,因为它不允许该过程干净地退出。

请注意,os.kill的文档错误地声称" kill()还会使进程句柄被杀死#34;这从来都不是真的。它调用OpenProcess来获取进程句柄。

使用WinAPI CTRL_C_EVENTCTRL_BREAK_EVENT而不是SIGINTSIGBREAK的决定对于跨平台代码来说是不幸的。它还没有定义GenerateConsoleCtrlEvent在传递的进程ID不是进程组ID时所执行的操作。在采用进程ID的API中使用此函数充其量是可疑的,并且可能非常错误。

根据您的特定需求,您可以编写一个适配器函数,使os.kill对于跨平台代码更友好一些。例如:

import os
import sys
import time
import signal

if sys.platform != 'win32':
    kill = os.kill
    sleep = time.sleep
else: 
    # adapt the conflated API on Windows.
    import threading

    sigmap = {signal.SIGINT: signal.CTRL_C_EVENT,
              signal.SIGBREAK: signal.CTRL_BREAK_EVENT}

    def kill(pid, signum):
        if signum in sigmap and pid == os.getpid():
            # we don't know if the current process is a
            # process group leader, so just broadcast
            # to all processes attached to this console.
            pid = 0
        thread = threading.current_thread()
        handler = signal.getsignal(signum)
        # work around the synchronization problem when calling
        # kill from the main thread.
        if (signum in sigmap and
            thread.name == 'MainThread' and
            callable(handler) and
            pid == 0):
            event = threading.Event()
            def handler_set_event(signum, frame):
                event.set()
                return handler(signum, frame)
            signal.signal(signum, handler_set_event)                
            try:
                os.kill(pid, sigmap[signum])
                # busy wait because we can't block in the main
                # thread, else the signal handler can't execute.
                while not event.is_set():
                    pass
            finally:
                signal.signal(signum, handler)
        else:
            os.kill(pid, sigmap.get(signum, signum))

    if sys.version_info[0] > 2:
        sleep = time.sleep
    else:
        import errno

        # If the signal handler doesn't raise an exception,
        # time.sleep in Python 2 raises an EINTR IOError, but
        # Python 3 just resumes the sleep.

        def sleep(interval):
            '''sleep that ignores EINTR in 2.x on Windows'''
            while True:
                try:
                    t = time.time()
                    time.sleep(interval)
                except IOError as e:
                    if e.errno != errno.EINTR:
                        raise
                interval -= time.time() - t
                if interval <= 0:
                    break

def func(signum, frame):
    # note: don't print in a signal handler.
    global g_sigint
    g_sigint = True
    #raise KeyboardInterrupt

signal.signal(signal.SIGINT, func)

g_kill = False
while True:
    g_sigint = False
    g_kill = not g_kill
    print('Running [%d]' % os.getpid())
    sleep(2)
    if g_kill:
        kill(os.getpid(), signal.SIGINT)
    if g_sigint:
        print('SIGINT')
    else:
        print('No SIGINT')

讨论

Windows无法在系统级[*]实现信号。 Microsoft的C运行时实现标准C所需的六个信号:SIGINTSIGABRTSIGTERMSIGSEGVSIGILL和{ {1}}。

SIGFPESIGABRT仅针对当前流程实施。您可以通过C raise调用处理程序。例如(在Python 3.5中):

SIGTERM

>>> import signal, ctypes >>> ucrtbase = ctypes.CDLL('ucrtbase') >>> c_raise = ucrtbase['raise'] >>> foo = lambda *a: print('foo') >>> signal.signal(signal.SIGTERM, foo) <Handlers.SIG_DFL: 0> >>> c_raise(signal.SIGTERM) foo 0 没用。

使用信号模块也无法对SIGTERM做很多事情,因为abort函数会在处理程序返回后终止进程,这在使用信号模块时会立即发生内部处理程序(它为在主线程中调用的已注册Python可调用的标记跳转)。对于Python 3,您可以使用faulthandler模块。或者通过ctypes调用CRT的signal函数将ctypes回调设置为处理程序。

CRT通过为相应的Windows例外设置Windows structured exception handler来实现SIGABRTSIGSEGVSIGILL

SIGFPE

CRT对这些信号的实现与Python的信号处理不兼容。异常过滤器调用已注册的处理程序,然后返回EXCEPTION_CONTINUE_EXECUTION。但是,Python的处理程序只会为解释器跳转一个标志,以便稍后在主线程中调用已注册的可调用对象。因此,触发异常的错误代码将继续在无限循环中触发。在Python 3中,您可以使用faulthandler模​​块来处理这些基于异常的信号。

离开STATUS_ACCESS_VIOLATION SIGSEGV STATUS_ILLEGAL_INSTRUCTION SIGILL STATUS_PRIVILEGED_INSTRUCTION SIGILL STATUS_FLOAT_DENORMAL_OPERAND SIGFPE STATUS_FLOAT_DIVIDE_BY_ZERO SIGFPE STATUS_FLOAT_INEXACT_RESULT SIGFPE STATUS_FLOAT_INVALID_OPERATION SIGFPE STATUS_FLOAT_OVERFLOW SIGFPE STATUS_FLOAT_STACK_CHECK SIGFPE STATUS_FLOAT_UNDERFLOW SIGFPE STATUS_FLOAT_MULTIPLE_FAULTS SIGFPE STATUS_FLOAT_MULTIPLE_TRAPS SIGFPE ,Windows添加了非标准SIGINT。控制台和非控制台进程都可以SIGBREAK这些信号,但只有控制台进程可以从另一个进程接收它们。 CRT通过SetConsoleCtrlHandler注册控制台控件事件处理程序来实现这一点。

控制台通过在附加进程中创建新线程来发送控制事件,该进程在kernel32.dll或kernelbase.dll(未记录)中的raise处开始执行。处理程序不在主线程上执行会导致同步问题(例如在REPL中或使用CtrlRoutine)。此外,如果在等待同步对象或等待同步I / O完成时它被阻止,则控制事件不会中断主线程。如果input可以中断,则需要注意避免在主线程中阻塞。 Python 3尝试通过使用Windows事件对象解决此问题,该事件对象也可用于SIGINT可以中断的等待。

当控制台发送进程SIGINTCTRL_C_EVENT时,CRT的处理程序分别调用已注册的CTRL_BREAK_EVENTSIGINT处理程序。还会为窗口关闭时控制台发送的SIGBREAK调用SIGBREAK处理程序。 Python默认通过在主线程中发起CTRL_CLOSE_EVENT来处理SIGINT。但是,KeyboardInterrupt最初是默认的SIGBREAK处理程序,它调用CTRL_BREAK_EVENT

您可以通过ExitProcess(STATUS_CONTROL_C_EXIT)将控制事件发送到连接到当前控制台的所有进程。这可以定位属于进程组的进程子集,或目标组0,以将事件发送到连接到当前控制台的所有进程。

进程组不是Windows API的详细记录方面。没有公共API来查询进程组,但Windows会话中的每个进程都属于进程组,即使它只是wininit.exe组(服务会话)或winlogon.exe小组(互动会议)。通过在创建新进程时传递创建标志GenerateConsoleCtrlEvent来创建新组。组ID是已创建进程的进程ID。据我所知,控制台是唯一使用进程组的系统,而且只适用于CREATE_NEW_PROCESS_GROUP

当目标ID未定义进程组ID且不应依赖时,控制台会执行的操作。如果进程及其父进程都附加到控制台,则向其发送控制事件基本上就像目标是组0.如果父进程未附加到当前控制台,则GenerateConsoleCtrlEvent失败,GenerateConsoleCtrlEvent来电os.kill。奇怪的是,如果你的目标是&#34;系统&#34;进程(PID 4)及其子进程smss.exe(会话管理器),调用成功但没有任何反应,除了目标被错误地添加到附加进程列表(即GetConsoleProcessList)。这可能是因为父进程是&#34; Idle&#34;因为它的PID 0被隐含地接受为广播PGID。父进程规则也适用于非控制台进程。定位非控制台子进程不会做任何事情 - 除了通过添加未附加的进程错误地破坏控制台进程列表。我希望很清楚,您应该只将控制事件发送到组0或通过TerminateProcess创建的已知进程组。

不要依赖于能够将CREATE_NEW_PROCESS_GROUP发送给除0组以外的任何内容,因为它最初在新进程组中被禁用。将此事件发送给新组并非不可能,但目标流程首先必须通过调用CTRL_C_EVENT启用CTRL_C_EVENT

SetConsoleCtrlHandler(NULL, FALSE)是您可以依赖的所有内容,因为它无法被禁用。发送此事件是一种优雅地杀死以CTRL_BREAK_EVENT开头的子进程的简单方法,假设它具有Windows CREATE_NEW_PROCESS_GROUP或C CTRL_BREAK_EVENT处理程序。如果没有,默认处理程序将终止进程,将退出代码设置为SIGBREAK。例如:

STATUS_CONTROL_C_EXIT

请注意,>>> import os, signal, subprocess >>> p = subprocess.Popen('python.exe', ... stdin=subprocess.PIPE, ... creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) >>> os.kill(p.pid, signal.CTRL_BREAK_EVENT) >>> STATUS_CONTROL_C_EXIT = 0xC000013A >>> p.wait() == STATUS_CONTROL_C_EXIT True 未发送到当前进程,因为该示例以子进程的进程组为目标(包括连接到控制台的所有子进程,依此类推) 。如果示例使用了组0,那么当前进程也会被杀死,因为我没有定义CTRL_BREAK_EVENT处理程序。让我们尝试一下,但设置一个处理程序:

SIGBREAK

[*]

Windows有asynchronous procedure calls(APC)将目标函数排入线程。有关Windows APC的深入分析,请参阅文章Inside NT's Asynchronous Procedure Call,尤其是阐明内核模式APC的作用。您可以通过QueueUserAPC将用户模式APC排队到线程。对于I / O完成例程,它们也会被ReadFileExWriteFileEx排队。

当线程进入可警告等待时(例如WaitForSingleObjectExSleepEx>>> ctrl_break = lambda *a: print('^BREAK') >>> signal.signal(signal.SIGBREAK, ctrl_break) <Handlers.SIG_DFL: 0> >>> os.kill(0, signal.CTRL_BREAK_EVENT) ^BREAK bAlertable),用户模式APC执行。另一方面,内核模式APC立即被调度(当IRQL低于TRUE时)。它们通常由I / O管理器用于在发出请求的线程的上下文中完成异步I / O请求包(例如,将数据从IRP复制到用户模式缓冲器)。有关显示APC如何影响可警告和不可警报等待的表,请参阅Waits and APCs。请注意,内核模式APC不会中断等待,而是由等待例程在内部执行。

Windows可以使用APC实现类似POSIX的信号,但实际上它使用其他方法来实现相同目的。例如:

可以发送窗口消息并将其发布到共享调用thread's desktop且处于相同或更低完整性级别的所有线程。当线程调用PeekMessageGetMessage时,发送窗口消息会将其置于系统队列中以调用窗口过程。发布消息会将其添加到线程的消息队列中,该消息队列的默认配额为10,000条消息。具有消息队列的线程应该有一个消息循环来通过APC_LEVELDispatchMessage处理队列。仅控制台进程中的线程通常没有消息队列。但是,控制台主机进程conhost.exe显然可以。单击关闭按钮时,或者通过任务管理器或taskkill.exe终止控制台的主进程时,会在控制台窗口的线程的消息队列中发布GetMessage消息。控制台轮流向其所有附加进程发送WM_CLOSE。如果某个进程处理了该事件,则在它被强行终止之前,它会在5秒内正常退出。