【问题标题】:How to handle a signal.SIGINT on a Windows OS machine?如何在 Windows 操作系统机器上处理 signal.SIGINT?
【发布时间】:2016-03-03 12:17:35
【问题描述】:

我正在尝试在 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 windows signals


    【解决方案1】:

    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() 还需要进程句柄被杀死”,这从来都不是真的。它调用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 所需的六个信号:SIGINTSIGABRTSIGTERMSIGSEGVSIGILLSIGFPE

    SIGABRTSIGTERM 仅针对当前进程实现。您可以通过 C raise 调用处理程序。例如(在 Python 3.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 没用。

    你也不能使用信号模块对SIGABRT 做很多事情,因为一旦处理程序返回,abort 函数就会终止进程,这在使用信号模块的内部处理程序时会立即发生(它会触发已注册的标志Python callable 可在主线程中调用)。对于 Python 3,您可以改用 faulthandler 模块。或者通过 ctypes 调用 CRT 的 signal 函数,将 ctypes 回调设置为处理程序。

    CRT 通过为相应的 Windows 异常设置一个 Windows structured 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 的处理程序只会触发一个标志,以便解释器稍后在主线程中调用已注册的可调用对象。因此,触发异常的错误代码将继续在无限循环中触发。在 Python 3 中,您可以将 faulthandler 模块用于这些基于异常的信号。

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

    控制台通过在附加进程中创建一个新线程来发送控制事件,该线程开始在 kernel32.dll 或 kernelbase.dll 中的CtrlRoutine 处执行(未记录)。处理程序不在主线程上执行可能会导致同步问题(例如,在 REPL 中或使用 input)。此外,如果控制事件在等待同步对象或等待同步 I/O 完成时被阻塞,则不会中断主线程。如果主线程应该可以被SIGINT 中断,则需要注意避免在主线程中阻塞。 Python 3 尝试通过使用 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 失败,并且 @ 987654422@ 致电TerminateProcess。奇怪的是,如果您以“系统”进程(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 启动的子进程的简单方法,假设它具有 Windows CTRL_BREAK_EVENT 或 C 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
    

    请注意,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) 将目标函数排队到线程。请参阅文章Inside NT's Asynchronous Procedure Call 对 Windows APC 进行深入分析,特别是阐明内核模式 APC 的作用。您可以通过QueueUserAPC 将用户模式 ​​APC 排队到线程中。它们还被ReadFileExWriteFileEx 排队等待I/O 完成例程。

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

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

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

    【讨论】:

    • 所以基本上:if sys.platform == 'win32': abandon_all_hope_ye_who_enter_here().
    • 使用signal.CTRL_BREAK_EVENT 来停止Python 进程会阻止调用atexit 处理程序,所以我不会称之为正常关闭。但是感谢这个伟大的总结,它真的很有帮助。
    • @schlamar,也许您错过了使用标志CREATE_NEW_PROCESS_GROUP 创建子进程并让子进程为signal.SIGBREAK 设置处理程序的部分。这适用于来自父母的有意信号。不幸的是,它不处理控制台窗口关闭时的情况,因为 Python 的 C 信号处理程序只是设置一个指示信号的标志并立即返回(信号处理程序仅在主线程中调用)。为了处理这种情况,我应该修改这个答案以提供一个设置控制台控制处理程序的 ctypes 示例。
    • @ErykSun 感谢您的详细解释!我尝试将其用于优雅终止,但找不到 Python 信号处理程序实际获得控制的单个组合。对于进程创建,我尝试了:(1)单独的 CMD 终端,每个终端以交互方式运行 python,(2)使用带有 CREATE_NEW_PROCESS_GROUP 的 Popen 创建的目标进程。对于目标进程中的 Python 级信号处理程序,我尝试了 SIGINT 和 SIGBREAK。为了在原始进程中发送信号,我将 os.kill() 与 SIGINT、SIGBREAK、CTRL_C_EVENT 和 CTRL_BREAK_EVENT 一起使用。这是在带有 Python 3.7 的 Win7 上。
    猜你喜欢
    • 1970-01-01
    • 2021-09-22
    • 1970-01-01
    • 2012-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多