Terminate Process Gracefully
Linux#
Linux下一切都很简单,可以向一个进程发送信号,Ctrl+C对应的是SIGINT信号,kill命令默认发送的是SIGTERM信号。
程序可以通过监听这两个信号来执行对应的清理程序,从而做到优雅停服:
import signal
import sys
import time
def signal_handler(sig, frame):
_logger.info(f"Receive {sig}, Performing graceful shutdown...")
time.sleep(2)
# Perform any necessary cleanup here
sys.exit(0)
# Register the signal handler for SIGINT
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
符合POSIX标准的操作系统都可以用同样的方法,干净,整洁,世界真美好。
Windows#
-
Windows没有SIGINT和SIGTERM等信号,对于控制台程序,可以发送和接收CTRL_C_EVENT和CTRL_BREAK_EVENT两种事件。
-
在python代码中,可以使用和Linux下一样的方法监听SIGINT信号和SIGBREAK信号,分别对应CTRL_C_EVENT和CTRL_BREAK_EVENT,python执行器会进行转换,所以代码可以保持不变。
-
然而,在Windows下给一个程序发送CTRL_C_EVENT并非易事…
- 首先,接收程序必须是控制台程序
- 其次,发送程序必须不是控制台程序,或者与接收程序在同一个控制台中
也就是说,如果两个控制台程序,但是这两个程序不在一个控制台中,是不能相互发送CTRL_C_EVENT的。
在Windows下可以用pythonw.exe启动脚本,则脚本运行在非控制台中,可以用下面的代码给另外一个程序发送事件:
import ctypes from ctypes import wintypes kernel32 = ctypes.windll.kernel32 CTRL_C_EVENT = 0 pid = xxx # Attach to the target process console if not kernel32.AttachConsole(pid): raise Exception(f"Failed to attach console to process with PID {pid}") # Send the CTRL_CLOSE_EVENT signal if not kernel32.GenerateConsoleCtrlEvent(1, pid): error_code = kernel32.GetLastError() raise Exception(f"Failed to send CTRL_CLOSE_EVENT Error:{error_code}") # Detach from the target process console kernel32.FreeConsole()
代码中需要先Attach到目标进程中的控制台上,然后再发送事件。
GenerateConsoleCtrlEvent() 函数说明文档:https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent
注意 GenerateConsoleCtrlEvent() 函数第二个参数指定一个进程ID,若该进程不是process group的leader,则会失败,可以通过传 0 对该 console 上的所有进程发送事件。
-
但是 如果目标进程为非控制台程序,则无法对其发送CTRL_C_EVENT,`taskkill` 程序结束进程时默认是发送WM_CLOSE事件,如果一个程序只是控制台程序,没有任何窗口,则无法接收该事件…
有一个方法可以让控制台程序响应WM_CLOSE事件,通过下面的方法启动控制台程序(实测在cmd中有效,powershell貌似用法不同):
start "" python.exe test.py other_args
该方法会启动一个控制台窗口,绑定python程序,当使用 taskkill /pid xxx 来结束进程时,控制台窗口会收到WM_CLOSE事件关闭窗口,而python程序会收到CTRL_CLOSE_EVENT。
通过下面的代码可以处理CTRL_CLOSE_EVENT事件从而进行清理工作:
import ctypes from ctypes import wintypes _ctrl_handler = None _HandlerRoutine = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) def ctrl_handler(ctrl_type): if ctrl_type == 2: # 2 is the value for CTRL_CLOSE_EVENT _logger.info(f"Received {ctrl_type}. Performing graceful shutdown...") time.sleep(2) # Perform any necessary cleanup here sys.exit(0) return True return False # Register the signal handler for CTRL_CLOSE_EVENT _ctrl_handler = _HandlerRoutine(ctrl_handler) kernel32 = ctypes.windll.kernel32 if not kernel32.SetConsoleCtrlHandler(_ctrl_handler, True): raise ctypes.WinError(ctypes.get_last_error())
据说 _HandlerRoutine() 的返回值一定要用变量存起来避免垃圾回收,否则当事件发生时会导致程序崩溃: https://stackoverflow.com/a/19907688
相关的Windows函数文档:
-
如果控制台程序进程被创建时使用了 DETACHED_PROCESS 或者 CREATE_NO_WINDOW ,该进程没有控制台对应,则进程无法收到WM_CLOSE和CTRL_C_EVENT
process = subprocess.Popen(["someprocess"], creationflags=subprocess.DETACHED_PROCESS)
Windows下进程创建的标志位说明:https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
-
如果控制台程序进程被创建时使用了 CREATE_NEW_CONSOLE 标志,程序会启动一个新的控制台,但无法被attach,也就无法发送 CTRL_C_EVENT 和 CTRL_BREAK_EVENT,只能向窗口发送WM_CLOSE事件
atexit#
Python的 `atexit` 模块可以注册一个函数并在程序结束的时候调用该函数, 然而 `atexit` 函数不会在 SIGTERM 时被调用…
也不会在Windows下收到CTRL_BREAK_EVENT和CTRL_CLOSE_EVENT时被调用…
父进程退出时通知子进程#
Linux下通过调用prctl(PR_SET_PDEATHSIG, SIGNAL)系统函数可以设置当父进程退出时,子进程收到设置的信号。(https://stackoverflow.com/a/19448096)
Windows下可以通过Job Object来实现:https://stackoverflow.com/a/23587108
MacOS下可以通过kqueue实现:https://stackoverflow.com/a/6484903