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

我正在尝试粘贴在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在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的决定对跨平台代码来说是不幸的。 当传递一个不是进程组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不会在系统级别执行信号[*]。 微软的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 3,您可以改为使用faulthandler模块。 或者通过ctypes调用CRT的signal函数来设置一个ctypes回调作为处理程序。

CRT通过为相应的Windows异常设置Windows 结构化异常处理程序来实现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注册一个控制台控制事件处理程序来实现这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不是进程组标识时,控制台执行什么操作是不确定的,不应该依赖它。 如果进程及其父进程都连接到控制台,那么发送一个控制事件基本上就像目标是组0一样。如果父进程没有连接到当前控制台,那么GenerateConsoleCtrlEvent失败,并且os.kill调用TerminateProcess 奇怪的是,如果你的目标是“系统”进程(PID 4)及其子进程smss.exe(会话管理器),调用会成功,但除了目标被错误地添加到附加进程列表(即GetConsoleProcessList )之外,什么都不会发生。 这可能是因为父进程是“空闲”进程,因为它是PID 0,所以被隐含地接受为广播PGID。 父进程规则也适用于非控制台进程。 定位非控制台子进程什么也不做 – 除了通过添加未连接的进程错误地损坏控制台进程列表。 我希望很明显,您只应将控件事件发送到组0或通过CREATE_NEW_PROCESS_GROUP创建的已知进程组。

不要依赖于能够将CTRL_C_EVENT发送给除0组CTRL_C_EVENT的任何其他组件,因为它最初在新的进程组中被禁用。 将这个事件发送到一个新组并不是不可能的,但是目标进程首先必须通过调用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因为我没有定义一个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有异步过程调用 (APC)将目标函数排队到一个线程。 请参阅文章Inside NT的异步过程调用深入分析Windows APC,特别是阐明内核模式APC的作用。 您可以通过QueueUserAPC将用户模式APC排队到一个线程。 它们也被ReadFileExWriteFileEx排队等待I / O完成例程。

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

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

  • 结构化异常处理 ,例如__try__finally __leaveRaiseExceptionAddVectoredExceptionHandler __leaveRaiseExceptionAddVectoredExceptionHandler
  • 内核调度程序对象 (即同步对象 ),例如SetEventSetWaitableTimer

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

窗口消息可以发送并发布到所有共享调用线程的桌面并且处于相同或较低的完整性级别的线程 。 当一个线程调用PeekMessage或者GetMessage时,发送一个窗口消息把它放入系统队列中调用窗口过程。 发布消息将其添加到线程的消息队列中,该消息队列具有10,000条消息的默认配额。 带有消息队列的线程应该有一个消息循环来通过GetMessageDispatchMessage处理队列。 仅在控制台进程中的线程通常没有消息队列。 但是,控制台主机进程conhost.exe显然确实如此。 当单击关闭按钮时,或者通过任务管理器或taskkill.exe终止控制台的主进程时,将WM_CLOSE消息发布到控制台窗口线程的消息队列中。 控制台轮流发送一个CTRL_CLOSE_EVENT给所有附加的进程。 如果一个进程处理这个事件,它会被强制终止5秒钟才能正常退出。