signal.signal() 在子线程中无效,必须在主线程注册并保持其存活才能捕获 ctrl+c(sigint);多线程需用共享 flag 或 queue 响应,asyncio 中须用 loop.add_signal_handler() 且不可与 signal.signal() 混用。

signal.signal() 为什么收不到 Ctrl+C?
python 的 signal.signal() 默认只在主线程生效,子线程里调用它无效,且无法捕获 SIGINT(即 Ctrl+C)——除非你在主线程注册并保持其存活。
常见错误现象:KeyboardInterrupt 仍抛出,自定义 handler 完全没触发;或者多线程程序里 signal 注册了但按 Ctrl+C 没反应。
- 必须在主线程调用
signal.signal(signal.SIGINT, handler),且主线程不能退出(比如不能跑完就 exit) - 如果用了
Threading,别在子线程里注册 signal;子线程需通过共享 flag 或queue等方式响应主线程的中断信号 - 使用
asyncio时,signal.signal()依然要注册在主线程,但 handler 内部需用asyncio.create_task()或loop.call_soon_threadsafe()安全调度协程
如何安全退出长循环或阻塞 I/O?
直接靠 signal.signal() 设置全局 flag 不够,因为循环可能卡在 time.sleep()、socket.recv() 或 queue.get() 这类阻塞调用上,根本没机会检查 flag。
使用场景:后台服务、爬虫主循环、日志轮转守护进程等需要优雅终止的长期运行脚本。
立即学习“Python免费学习笔记(深入)”;
- 对可中断的系统调用(如
sleep()、recv()),linux/macos 下会自动被SIGINT中断并抛InterruptedError,需显式捕获并退出 - windows 对某些阻塞调用不触发中断,建议改用带 timeout 的版本,例如
sock.recv(1024, socket.MSG_DONTWAIT)或queue.get(timeout=1) - 避免用
while True:死循环,改用while not shutdown_flag.is_set():,并在 signal handler 里调用shutdown_flag.set()
signal.pause() 和 signal.sigwait() 的区别与适用场景
signal.pause() 是最简等待信号的方式,但它会挂起整个进程、只响应同步信号,且不可重入;signal.sigwait() 更可控,但仅支持 POSIX 系统(Linux/macOS),且要求提前用 signal.pthread_sigmask() 屏蔽对应信号。
性能 / 兼容性影响:Windows 下 sigwait() 不可用;pause() 在容器或 systemd 环境中可能被意外唤醒(如子进程退出产生 SIGCHLD)。
- 想写跨平台最小可行信号监听?老实用
signal.signal()+ 主循环 + timeout sleep - Linux 专用高性能服务?用
pthread_sigmask()屏蔽SIGTERM/SIGINT,再开独立线程调用sigwait()处理,避免干扰主逻辑 - 别在 signal handler 里做复杂操作(如打印日志、写文件),它运行在异步上下文,仅保证调用 async-signal-safe 函数(如
os.write())
asyncio 程序里怎么接 SIGTERM?
asyncio 自带信号处理机制,但默认只监听 SIGINT(Ctrl+C),SIGTERM 需手动注册;而且一旦用了 loop.add_signal_handler(),就不能再用 signal.signal(),否则冲突。
常见错误现象:docker stop 或 kill -15 没反应;或者注册后报错 ValueError: signal only works in main thread。
- 必须在事件循环启动前注册,例如在
asyncio.run()之前,或用loop = asyncio.get_event_loop()后立即调用loop.add_signal_handler(signal.SIGTERM, ...) - handler 函数不能是 async,得是普通函数;如需触发协程,用
loop.create_task()或loop.call_soon() - 若用
asyncio.run(),推荐改用asyncio.get_running_loop().add_signal_handler(...),并在 handler 中调用loop.stop()后 await cleanup
信号处理真正的复杂点不在注册语法,而在于「谁在哪个线程/协程里响应」「阻塞点是否可打断」「不同系统对同一信号的行为差异」。这些细节不写进日志、不报错、也不抛异常,只会在压测或部署时突然失效。