OpenBSD下Go实现SIGINT处理器并支持SIGTSTP返回shell
解决OpenBSD下Go程序SIGTSTP无法挂起的问题
问题场景
在OpenBSD/amd64环境(go1.20.1)下,当Go程序通过signal.Notify监听SIGINT和SIGTERM信号时:
- 按下Ctrl-C(发送SIGINT),程序能正常以返回码1终止
- 按下Ctrl-Z(发送SIGTSTP),程序无法被挂起,无法返回shell
但未添加信号处理的程序,按下Ctrl-Z能正常挂起并返回shell。
异常代码示例
package main import ( "os" "os/signal" "syscall" "time" ) func main() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) go func() { <-c os.Exit(1) }() for { time.Sleep(time.Second) } }
正常对比代码
package main import ( "time" ) func main() { for { time.Sleep(time.Second) } }
运行正常代码按下Ctrl-Z的输出:
^Z[1] + Suspended ./test me@mymachine:~$
原因分析
当调用signal.Notify注册特定信号时,Go runtime会接管进程的信号处理机制。在OpenBSD环境下,这种接管会导致未注册的SIGTSTP信号的默认挂起动作被阻断,无法触发进程挂起。
解决方案
方案1:显式处理SIGTSTP,恢复默认挂起动作
监听SIGTSTP信号,收到时先恢复该信号的默认处理逻辑,再重新发送信号触发挂起,挂起恢复后重新监听:
package main import ( "os" "os/signal" "syscall" "time" ) func main() { c := make(chan os.Signal, 1) // 同时监听SIGINT、SIGTERM和SIGTSTP signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTSTP) go func() { for sig := range c { switch sig { case syscall.SIGINT, syscall.SIGTERM: os.Exit(1) case syscall.SIGTSTP: // 停止监听SIGTSTP,恢复系统默认处理 signal.Stop(c, syscall.SIGTSTP) // 给自己发送SIGTSTP,触发挂起 syscall.Kill(syscall.Getpid(), syscall.SIGTSTP) // 进程恢复后,重新监听SIGTSTP signal.Notify(c, syscall.SIGTSTP) } } }() for { time.Sleep(time.Second) } }
方案2:使用signal.NotifyContext处理终止信号
signal.NotifyContext仅会处理指定的终止信号,不会干扰其他信号的默认处理逻辑,SIGTSTP的挂起动作会正常生效:
package main import ( "context" "os" "os/signal" "syscall" "time" ) func main() { // 创建监听SIGINT、SIGTERM的上下文 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // 监听上下文取消信号,触发退出 go func() { <-ctx.Done() os.Exit(1) }() // 主循环,同时监听上下文取消 for { select { case <-ctx.Done(): return default: time.Sleep(time.Second) } } }
验证效果
运行修改后的程序:
- 按下Ctrl-Z,程序会被挂起,返回shell,输出类似正常代码的挂起提示
- 输入
fg命令可恢复程序运行 - 按下Ctrl-C,程序仍能正常以返回码1终止
内容的提问来源于stack exchange,提问作者user92403
相关产品推荐
相关产品推荐

