Go编写的Python扩展忽略Ctrl+C的原因及解决办法问询
问题背景与疑问
我在用Go编写的Python驱动连接服务时,遇到了一个问题:当服务实例处于不健康状态时,驱动会陷入停滞,只能通过kill进程终止。经过测试发现两个明显差异:
- 执行Go编写的扩展时,程序会忽略Ctrl+C指令,必须等到Go函数执行完毕、控制权回到Python后才会响应;
- 同样逻辑的C编写扩展,执行时按下Ctrl+C会立刻触发
KeyboardInterrupt异常。
我想搞清楚:
- 为什么会出现这种差异?
- 有没有办法规避这个问题,或者实现类似超时的机制?(之前试过用
threading.Timer抛出异常模拟超时,但异常是在Timer线程里抛出的,无法中断主线程的Go函数执行)
测试环境:CPython 3.9/3.10,Go 1.20.4,C编译器9.4.0
原因分析
本质差异来自CPython和Go runtime的信号处理逻辑冲突:
- CPython的信号处理机制:CPython默认会把Ctrl+C对应的
SIGINT信号转化为KeyboardInterrupt异常,但这个转换是在Python字节码解释器的主循环中完成的。当调用C扩展的阻塞函数时,CPython会保留信号处理逻辑,而像sleep这类标准C函数在收到SIGINT时会直接中断并返回错误,此时CPython会检测到信号并抛出异常。 - Go runtime的信号接管:Go程序有独立的runtime和信号处理系统,当Go编译成共享库被Python加载后,Go的runtime会接管
SIGINT等信号的处理。此时Python的信号处理逻辑被覆盖,Ctrl+C发送的SIGINT会被Go捕获,不会传递给Python解释器,直到Go函数执行完毕。另外,Go的time.Sleep是由runtime调度的,不会被外部信号直接中断,所以就算按了Ctrl+C,Go的sleep也会执行到结束。
解决方案
针对这个问题,有几种可行的解决思路:
1. 在Go扩展中显式处理SIGINT信号
修改Go代码,主动监听SIGINT信号,收到信号时提前退出阻塞操作。这样Python就能在按下Ctrl+C时立刻终止Go函数的执行:
package main import ( "C" "log" "os" "os/signal" "syscall" "time" ) // 必须加//export注释,否则Python无法正确调用该函数 //export helloWorld func helloWorld() { log.Println("Hello World") // 创建信号通道,监听SIGINT sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT) // 用select同时等待sleep完成或信号到来 select { case <-time.After(10 * time.Second): log.Println("Done sleeping") case <-sigChan: log.Println("Received SIGINT, exiting early") signal.Stop(sigChan) return } } func main() {}
编译命令:go build -buildmode=c-shared -o go_library.so
2. 将Go逻辑改为独立进程调用
把Go代码编译成可执行文件,Python通过subprocess模块调用,这样可以直接向子进程发送信号来中断执行,避免主线程被阻塞:
import subprocess import ctypes try: print("Calling golang executable") proc = subprocess.Popen(["./go_executable"]) proc.wait() print("Calling C code") c_library = ctypes.cdll.LoadLibrary("./c_library.so") hello_c = c_library.helloWorld hello_c() except KeyboardInterrupt: print("Ctrl+C issued") if 'proc' in locals(): proc.send_signal(subprocess.signal.SIGINT) finally: print("Done")
Go代码编译为可执行文件:go build -o go_executable
3. 用信号实现超时机制(改进版)
之前的threading.Timer无法中断主线程,是因为Python线程是协作式的。可以用signal模块在主线程设置超时信号,直接触发异常:
import ctypes import signal def timeout_handler(signum, frame): raise TimeoutError("Function execution timed out") go_library = ctypes.cdll.LoadLibrary('./go_library.so') hello_Go = go_library.helloWorld c_library = ctypes.cdll.LoadLibrary("./c_library.so") hello_c = c_library.helloWorld try: # 设置5秒超时 signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(5) print("Calling golang code") hello_Go() signal.alarm(0) # 执行完成,取消超时 print("Calling C code") hello_c() except (KeyboardInterrupt, TimeoutError) as e: print(f"Interrupt/Timeout occurred: {e}") finally: signal.alarm(0) # 确保超时被取消 print("Done")
注意:这种方法需要Go扩展配合处理信号,否则超时后Go函数可能还在后台运行,会导致资源泄漏。
测试用POC代码
Python调用代码
import ctypes go_library = ctypes.cdll.LoadLibrary('./go_library.so') hello_Go = go_library.helloWorld c_library = ctypes.cdll.LoadLibrary("./c_library.so") hello_c = c_library.helloWorld try: print("Calling golang code") hello_Go() print("Calling C code") hello_c() except KeyboardInterrupt: print("Ctrl+C issued:") finally: print("Done")
Go扩展原代码
package main import ( "C" "log" "time" ) func helloWorld(){ log.Println("Hello World") time.Sleep(10 * time.Second) log.Println("Done sleeping") } func main(){ }
C扩展原代码
#include <stdio.h> #include <unistd.h> // sleep函数需要的头文件 int helloWorld() { printf("Hello from C\n"); sleep(10); printf("Done sleeping from C\n"); return 0; }
编译C代码命令:gcc -shared -fPIC -o c_library.so c_library.c
内容的提问来源于stack exchange,提问作者puerkito66
相关产品推荐
相关产品推荐

