Python 信号处理机制的完整解析

1次阅读

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

Python 信号处理机制的完整解析

signal.signal() 为什么收不到 Ctrl+C?

pythonsignal.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

信号处理真正的复杂点不在注册语法,而在于「谁在哪个线程/协程里响应」「阻塞点是否可打断」「不同系统对同一信号的行为差异」。这些细节不写进日志、不报错、也不抛异常,只会在压测或部署时突然失效。

text=ZqhQzanResources