python需显式注册signal.signal()处理sigint和sigterm以实现优雅退出,注意仅主线程生效、避免耗时操作;推荐用threading.event标志位配合主循环检查;asyncio应自定义add_signal_handler并手动执行异步清理。

Python 如何捕获 Ctrl+C 和 kill 信号
Python 默认对 SIGINT(Ctrl+C)会抛出 KeyboardInterrupt,但对 SIGTERM(比如 kill <pid></pid>)直接退出,不触发任何 Python 层逻辑。想“优雅退出”,必须显式注册信号处理器。
用 signal.signal() 绑定处理函数,注意:该函数只在主线程生效;子线程里调用无效,也不能靠它中断阻塞中的 I/O(如 time.sleep() 或 socket.recv())。
-
SIGINT和SIGTERM都要单独注册,不能写成signal.SIGINT | signal.SIGTERM - 处理函数必须接受两个参数:
signum和frame,缺一不可,否则运行时报TypeError - 不要在信号处理器里做耗时操作(如写文件、发 HTTP 请求),它运行在异步上下文,可能被中断或引发死锁
signal.pause() 不适合长期运行的服务
很多人查到 signal.pause() 就以为能“挂起进程等信号”,但它只在 Unix-like 系统有效,Windows 直接报 AttributeError;而且它会让主线程彻底休眠,无法同时做其他事(比如轮询、心跳、接收网络请求)。
真正实用的方式是:用标志位 + 主循环配合 signal.setitimer() 或非阻塞检查。更常见的是把信号处理简化为“设一个全局 flag”,主逻辑定期检查这个 flag 决定是否退出。
立即学习“Python免费学习笔记(深入)”;
- 推荐用
threading.Event替代裸变量(如should_exit = False),避免多线程读写竞争 - 如果主逻辑是
while True:,就在循环开头加if exit_event.is_set(): break -
signal.pause()只适合极简脚本,生产环境基本不用
多线程/多进程下信号只发给主线程
Python 的信号机制本质是 OS 发给进程的,但 CPython 把信号转发逻辑绑定在主线程上——这意味着:子线程里注册的 signal.signal() 会被忽略;子进程(用 multiprocessing 启动的)有自己独立的信号状态,父进程的 handler 不会继承过去。
所以如果你用 ThreadPoolExecutor 或 multiprocessing.Process,别指望在工作线程/进程中靠同一个 handler 做清理。得换思路:
- 主线程捕获信号后,主动调用
executor.shutdown(wait=True)或向子进程发os.kill(pid, signal.SIGTERM) - 子进程启动时,自己注册自己的
SIGTERMhandler,不要依赖父进程 - 避免在子线程里调用
signal.signal(),它不会报错,但也不会生效
asyncio 程序怎么处理退出信号
asyncio 自带信号支持,但默认只响应 SIGINT 和 SIGTERM 并取消所有任务——这看似省事,实则容易遗漏资源释放。比如你开了一个 aiofiles.open() 文件句柄,或者用了 asyncpg.create_pool(),直接取消任务可能导致连接未关闭、文件未 flush。
正确做法是:用 loop.add_signal_handler() 注册自定义 handler,并在里面手动触发 shutdown 流程。
- handler 里别直接
loop.stop(),先await shutdown()(你自己写的清理协程),再loop.stop() - 确保
shutdown()是 awaitable,且内部不包含阻塞调用(如time.sleep()) - Windows 下
add_signal_handler()不可用,得降级用loop.run_in_executor()包一层signal.pause()或轮询
信号处理最常被忽略的一点:清理逻辑本身可能失败(比如网络超时、磁盘满),而信号 handler 里没法 try-except 所有异常——一旦出错,整个进程可能静默崩溃。建议关键清理步骤加日志,用 atexit.register() 做兜底,但注意它不响应 SIGKILL。










