signal.signal() 必须在主线程注册,子线程调用会抛 valueerror;注册需在任何线程启动前完成,handler 函数必须接受 signum 和 frame 两参数,且自定义后不再触发 keyboardinterrupt 异常。

signal.signal() 必须在主线程中注册
Python 的 signal 模块只支持在主线程中设置信号处理器,子线程调用 signal.signal() 会直接抛出 ValueError: signal only works in main thread。如果你的应用启用了多线程(比如用 threading.Thread 或 concurrent.futures),信号注册必须放在 if __name__ == '__main__': 块开头,或确保在任何线程启动前完成。
常见错误是把信号注册写在某个工作函数里,或者在 ThreadPoolExecutor 的 submit 回调中尝试注册——这必然失败。
- 正确做法:主线程启动后立即注册,例如
signal.signal(signal.SIGINT, handle_exit) - 若主逻辑被封装成函数,注册需在其调用前,不能在函数内部
- 使用
asyncio时注意:loop.run_forever()会接管 SIGINT,此时手动注册可能被覆盖,应优先用loop.add_signal_handler()
handle_exit 函数必须接受 signum 和 frame 两个参数
信号处理器函数签名是硬性要求:def handle_exit(signum, frame):。少一个参数、参数名不匹配(比如写成 s 或 sig)、或加了默认值都会导致调用时报 TypeError。
这个函数本身不会自动终止程序——它只是响应信号。你需要显式退出,否则 Ctrl+C 后程序继续运行。
- 推荐用
sys.exit(0)或os._exit(0)(后者更彻底,跳过清理) - 避免在 handler 中做耗时操作(如网络请求、文件写入),因为信号是异步的,可能中断正在执行的系统调用
- 如果需要清理资源,建议设个全局标志(如
running = True),让主循环检测并退出,而不是在 handler 里直接清理
捕获 SIGINT 后无法再触发 KeyboardInterrupt 异常
一旦你用 signal.signal(signal.SIGINT, ...) 自定义了处理器,Python 就不再自动抛出 KeyboardInterrupt 异常。这意味着原来靠 try/except KeyboardInterrupt: 捕获的逻辑会失效。
这是很多老代码迁移时踩坑的地方:以为注册了 handler 就“增强”了 Ctrl+C 处理,结果反而丢失了原有的异常流程。
- 若原有逻辑依赖
KeyboardInterrupt(比如某些库内部行为),不要覆盖 SIGINT,而是保留默认行为 - 想兼顾两者?可以在 handler 里手动触发异常:
raise KeyboardInterrupt,但要注意这仍需在主线程且不能在信号上下文中做太多事 - 测试时记得用
python -c "import os; os.kill(os.getpid(), 2)"模拟 SIGINT,避免依赖终端环境
Windows 下 SIGINT 行为与 Unix 不一致
Windows 对 SIGINT 的支持是模拟的,仅在交互式终端中有效;如果程序通过 IDE 运行、重定向了 stdin/stdout,或打包成 exe,Ctrl+C 可能根本不会触发你的 handler。
更麻烦的是,Windows 默认将 SIGINT 映射为 CTRL_C_EVENT,而 signal.pause() 在 Windows 上不可用,signal.sigwait() 也不支持 SIGINT。
- 跨平台方案:优先检查
sys.platform,Windows 下可考虑轮询msvcrt.kbhit()+msvcrt.getch()检测 Ctrl+C(但仅限控制台) - 打包工具(如 PyInstaller)默认禁用控制台信号,需加
--console参数才能让 Ctrl+C 生效 - 容器或 systemd 环境下,SIGINT 可能被父进程截获,Python 进程收不到,这时得靠外部信号转发或健康检查超时机制
KeyboardInterrupt。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











