Python 的 os.kill 在 Windows 上封装了两个不相关的 API。当sig 参数为CTRL_C_EVENT 或CTRL_BREAK_EVENT 时,它调用GenerateConsoleCtrlEvent。在这种情况下,pid 参数是进程组 ID。如果后一个调用失败,并且对于所有其他sig 值,它将调用OpenProcess,然后调用TerminateProcess。在这种情况下,pid 参数是一个进程 ID,sig 值作为退出代码传递。终止 Windows 进程类似于将SIGKILL 发送到 POSIX 进程。通常应该避免这种情况,因为它不允许进程干净地退出。
请注意,os.kill 的文档错误地声称“kill() 还需要进程句柄被杀死”,这从来都不是真的。它调用OpenProcess 来获取进程句柄。
决定使用 WinAPI CTRL_C_EVENT 和 CTRL_BREAK_EVENT,而不是 SIGINT 和 SIGBREAK,对于跨平台代码来说是不幸的。它也没有定义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 所需的六个信号:SIGINT、SIGABRT、SIGTERM、SIGSEGV、SIGILL 和 SIGFPE。
SIGABRT 和 SIGTERM 仅针对当前进程实现。您可以通过 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 来实现 SIGSEGV、SIGILL 和 SIGFPE:
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_EVENT 或CTRL_BREAK_EVENT 时,CRT 的处理程序分别调用注册的SIGINT 或SIGBREAK 处理程序。 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 排队到线程中。它们还被ReadFileEx 和WriteFileEx 排队等待I/O 完成例程。
当线程进入可警报等待时(例如WaitForSingleObjectEx 或SleepEx 与bAlertable 为TRUE),用户模式APC 将执行。另一方面,内核模式 APC 会立即分派(当 IRQL 低于 APC_LEVEL 时)。 I/O 管理器通常使用它们在发出请求的线程上下文中完成异步 I/O 请求包(例如,将数据从 IRP 复制到用户模式缓冲区)。请参阅Waits and APCs 获取显示 APC 如何影响可警报和不可警报等待的表格。请注意,内核模式 APC 不会中断等待,而是由等待例程在内部执行。
Windows 可以使用 APC 实现类似 POSIX 的信号,但实际上它使用其他方式来实现相同目的。例如:
可以将窗口消息发送和发布到共享调用thread's desktop 并且处于相同或更低完整性级别的所有线程。当线程调用PeekMessage 或GetMessage 时,发送窗口消息将其放入系统队列以调用窗口过程。发布消息会将其添加到线程的消息队列中,该队列的默认配额为 10,000 条消息。带有消息队列的线程应该有一个消息循环来通过GetMessage 和DispatchMessage 处理队列。仅控制台进程中的线程通常没有消息队列。但是,控制台主机进程 conhost.exe 显然可以。当单击关闭按钮时,或者当控制台的主进程通过任务管理器或taskkill.exe 被终止时,WM_CLOSE 消息将发布到控制台窗口线程的消息队列中。控制台依次向其所有附加进程发送CTRL_CLOSE_EVENT。如果一个进程处理了这个事件,它会在被强制终止之前有 5 秒的时间优雅地退出。