如何避免在覆盖 go 的默认信号处理程序时发生竞争?

How can you avoid races in overriding go's default signal handlers?

tl;dr 如果一个信号可以在任何时候被 go 运行time 处理,我们如何安全地使用 signal.Ignore 来忽略 SIGINT在安装默认信号处理程序时与我们在 main() 运行s

中的指令之间的竞争方式

pkg/signalgo docs 说明了信号的默认行为

A SIGHUP, SIGINT, or SIGTERM signal causes the program to exit.

因此,编写一个旋转 CPU 的 golang 二进制文件,按 ctrl+c 发送 SIGINT,程序将退出。

现在,假设您想覆盖该行为。一种方法是忽略 signal.Ignore(syscall.SIGINT)

的信号

但现在考虑以下

package main

import (
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    fmt.Println("Looping, SIGINT will have default behavior")
    for start := time.Now(); time.Now().Before(start.Add(time.Second * 5)); {

    }
    signal.Ignore(syscall.SIGINT)
    fmt.Println("OK")

    for start := time.Now(); time.Now().Before(start.Add(time.Second * 5)); {

    }
}

这里我们有一个循环 5 秒的简单 golang 二进制文件。这是一个繁忙的循环,所以我们知道我们不会通过在 OS 线程上让出时间来参与协作式多任务处理。然后注册忽略SIGINT。

如果您尝试这样做,您会注意到如果在前 5 秒内输入 ctrl+c,程序将退出。这似乎是有道理的——我们正在获得默认的 golang 运行 时间信号处理行为,因为我们还没有通过调用 signal.Ignore.

来覆盖它

现在也许我们可以通过将 signal.Ignore 移动到 main() 中的第一件事来解决这个问题,但是这个程序证明了 运行time 并不能保证在 main() 中的同步代码执行完成之前,默认信号处理程序不会 运行。

就算你动它,我们好像也在赛跑

  1. go 运行time 注册其默认信号处理程序的时间点,并且
  2. 我们的代码可以运行的最早点(main中的第一个(或者第二个,如果你需要创建一个通道)指令)

我找不到这方面的文档。 go 运行time 提供什么保证让我绝对确定信号不会在第 1 阶段和第 2 阶段之间到达?

TL;DR:尽早捕捉,例如,就在 main 的顶部。


正如评论所说,这似乎不是将此描述为问题的好方法,因为它在所有编程语言中都非常通用:如果您没有为 [=11= 设置信号处理程序],默认情况下,您会在 SIGINT 被杀死,一旦被杀死,就不会了。的确如此,但是 任何 程序都可以在启动过程中终止,在它有机会开始捕获信号之前。无论编程语言如何,都是如此。它对 C 和 C++ 程序的影响与对 Go 程序的影响一样大。

那么,一般来说,要可靠地且尽可能无竞争地 捕获或丢弃 SIGINT 信号 所需要做的就是使用 signal.Notify(ch, os.Interrupt) 很早,靠近 main 的顶部,其中 ch 是您为此创建的频道。1 然后您可以编写自己的比赛-通过 goroutines 和通道处理这个问题的免费代码:

  • 让您自己的 goroutine 读取通道以查看if/when 信号已传送。
  • 让它读取另一个通道,或者使用您自己的互斥锁管理的一些共享内存区域,以查看在传送信号时如何处理信号。
  • 发出信号后,如果您应该退出,请调用 os.Exit(或者——可能更好——在 syscall.SIGINT 上使用 os.Reset,然后给自己一个 syscall.SIGINT生成正确的 OS 级退出状态,"killed by signal 1"2)。如果应该忽略该信号,只需删除频道通知即可。

有点相关:最近有一个针对 syscall.SIGPIPE 处理的修复。特别是,调用 signal.Ignore(syscall.SIGPIPE) 应该忽略 SIGPIPE,但没有。 This seems to be fixed in Go 1.14.


1如包文档所述,故意捕获信号将绕过 nohuptrap "" 1 2 15 中的任何 "pre-ignored" 状态 shell。如果您希望对此进行检查,请使用 signal.Ignored 函数。

2如果 signal_unix.go 导出了 dieFromSignal 函数,您也许可以直接使用它,但它不能。最好有一个 OS 不可知的包装器来至少干净地尝试这种自杀。这甚至可以在 OS 级别使用 sigprocmask 使自杀尽可能无种族歧视。