如何在Windows OS机器上处理信号.SIGINT?

2022-03-07 00:00:00 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)

Python

推荐答案的os.kill封装了两个不相关的API。当sig参数为CTRL_C_EVENTCTRL_BREAK_EVENT时,它调用GenerateConsoleCtrlEvent。在本例中,pid参数是一个进程组ID。如果后一个调用失败,并且对于所有其他sig值,它会调用OpenProcess,然后调用TerminateProcess。在本例中,pid参数是一个进程ID,sig值作为退出代码传递。终止Windows进程类似于向POSIX进程发送SIGKILL。通常应避免这种情况,因为它不允许进程干净地退出。

请注意,os.kill的文档错误地声称"kill()另外需要终止进程句柄",这从来都不是真的。它调用OpenProcess以获取进程句柄。

决定使用WinAPICTRL_C_EVENTCTRL_BREAK_EVENT而不是SIGINTSIGBREAK,这对于跨平台代码来说是不幸的。它也没有定义当传递一个不是进程组ID的进程ID时GenerateConsoleCtrlEvent会做什么。在接受进程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:SIGINTSIGABRTSIGTERMSIGSEGVSIGILLSIGFPE所需的六个信号。

SIGABRTSIGTERM仅针对当前进程实现。您可以通过Craise调用处理程序。例如(在Python3.5中):

>>> 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的标志)。对于Python3,您可以使用faulthandler模块。或者通过ctype调用CRT的signal函数,将ctype回调设置为处理程序。

CRT通过为相应的Windows异常设置Windowsstructured exception handler来实现SIGSEGVSIGILLSIGFPE

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

剩下SIGINT,Windows在其中添加了非标准的SIGBREAK。控制台进程和非控制台进程都可以raise这些信号,但只有控制台进程可以从另一个进程接收它们。CRT通过SetConsoleCtrlHandler注册控制台控制事件处理程序来实现这一点。

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

当控制台向进程发送CTRL_C_EVENTCTRL_BREAK_EVENT时,CRT的处理程序将分别调用已注册的SIGINTSIGBREAK处理程序。SIGBREAK处理程序也为控制台在其窗口关闭时发送的CTRL_CLOSE_EVENT调用。Python默认通过在主线程中发出一个KeyboardInterrupt来处理SIGINT。但是,SIGBREAK最初是默认的CTRL_BREAK_EVENT处理程序,它调用ExitProcess(STATUS_CONTROL_C_EXIT)

您可以通过GenerateConsoleCtrlEvent向连接到当前控制台的所有进程发送控制事件。这可以将属于进程组的进程子集或目标组0作为目标,以便将事件发送到附加到当前控制台的所有进程。

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

当目标ID不是进程组ID时,控制台执行的操作是未定义的,不应该依赖。如果进程及其父进程都附加到控制台,则向其发送控制事件基本上就像目标是组0一样。如果父进程没有附加到当前控制台,则GenerateConsoleCtrlEvent失败,os.kill调用TerminateProcess。奇怪的是,如果您以"system"进程(PID 4)及其子进程smss.exe(会话管理器)为目标,调用会成功,但除了将目标错误地添加到附加进程列表(即GetConsoleProcessList)之外,什么也不会发生。这可能是因为父进程是"空闲"进程,因为它的PID为0,所以被隐式接受为广播PGID。父进程规则也适用于非控制台进程。以非控制台子进程为目标不做任何事情--只是通过添加未附加的进程错误地损坏了控制台进程列表。我希望您清楚地知道,您应该只将控制事件发送到组0或您通过CREATE_NEW_PROCESS_GROUP创建的已知进程组。

不要依赖于能够将CTRL_C_EVENT发送到组0以外的任何位置,因为它最初在新进程组中被禁用。将此事件发送到新组并非不可能,但目标进程必须先调用SetConsoleCtrlHandler(NULL, FALSE)启用CTRL_C_EVENT

CTRL_BREAK_EVENT是您唯一可以依赖的,因为它不能被禁用。假设以CREATE_NEW_PROCESS_GROUP启动的子进程具有WindowsCTRL_BREAK_EVENT或CSIGBREAK处理程序,发送此事件是优雅终止该子进程的一种简单方法。如果不是,默认处理程序将终止该进程,并将退出代码设置为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

请注意,CTRL_BREAK_EVENT没有发送到当前进程,因为该示例的目标子进程的进程组(包括附加到控制台的所有子进程,依此类推)。如果示例使用了组0,那么当前进程也会被终止,因为我没有定义SIGBREAK处理程序。让我们试一试,但是设置了一个处理程序:

>>> ctrl_break = lambda *a: print('^BREAK')
>>> signal.signal(signal.SIGBREAK, ctrl_break)
<Handlers.SIG_DFL: 0>
>>> os.kill(0, signal.CTRL_BREAK_EVENT)
^BREAK

[*]

Windows有asynchronous procedure calls(APC)将目标函数排队到线程。有关Windows APC的深入分析,特别是澄清内核模式APC的作用,请参阅文章Inside NT's Asynchronous Procedure Call。您可以通过QueueUserAPC将用户模式的APC排队到线程。它们还按ReadFileExWriteFileEx排队等待I/O完成例程。

用户模式APC在线程进入警报等待时执行(例如,WaitForSingleObjectExSleepExbAlertableTRUE)。另一方面,内核模式的APC会立即被分派(当IRQL低于APC_LEVEL时)。它们通常由I/O管理器用来在发出请求的线程上下文中完成异步I/O请求数据包(例如,将数据从IRP复制到用户模式缓冲区)。有关APC如何影响可报警和不可报警等待的表,请参阅Waits and APCs。请注意,内核模式APC不会中断等待,而是由等待例程在内部执行。

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

  • Structured Exception Handling,如__try, __except, __finally, __leaveRaiseExceptionAddVectoredExceptionHandler
  • Kernel Dispatcher Objects(即Synchronization Objects),如SetEventSetWaitableTimer

  • 窗口消息,例如SendMessage(到窗口过程),PostMessage(到线程的消息队列调度到窗口过程),PostThreadMessage(到线程的消息队列),WM_CLOSEWM_TIMER

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

相关文章