如何避免在覆盖 go 的默认信号处理程序时发生竞争?
How can you avoid races in overriding go's default signal handlers?
tl;dr 如果一个信号可以在任何时候被 go 运行time 处理,我们如何安全地使用 signal.Ignore
来忽略 SIGINT在安装默认信号处理程序时与我们在 main()
运行s
中的指令之间的竞争方式
pkg/signal 的 go 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() 中的同步代码执行完成之前,默认信号处理程序不会 运行。
就算你动它,我们好像也在赛跑
- go 运行time 注册其默认信号处理程序的时间点,并且
- 我们的代码可以运行的最早点(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如包文档所述,故意捕获信号将绕过 nohup
或 trap "" 1 2 15
中的任何 "pre-ignored" 状态 shell。如果您希望对此进行检查,请使用 signal.Ignored
函数。
2如果 signal_unix.go
导出了 dieFromSignal
函数,您也许可以直接使用它,但它不能。最好有一个 OS 不可知的包装器来至少干净地尝试这种自杀。这甚至可以在 OS 级别使用 sigprocmask
使自杀尽可能无种族歧视。
tl;dr 如果一个信号可以在任何时候被 go 运行time 处理,我们如何安全地使用 signal.Ignore
来忽略 SIGINT在安装默认信号处理程序时与我们在 main()
运行s
pkg/signal 的 go 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() 中的同步代码执行完成之前,默认信号处理程序不会 运行。
就算你动它,我们好像也在赛跑
- go 运行time 注册其默认信号处理程序的时间点,并且
- 我们的代码可以运行的最早点(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如包文档所述,故意捕获信号将绕过 nohup
或 trap "" 1 2 15
中的任何 "pre-ignored" 状态 shell。如果您希望对此进行检查,请使用 signal.Ignored
函数。
2如果 signal_unix.go
导出了 dieFromSignal
函数,您也许可以直接使用它,但它不能。最好有一个 OS 不可知的包装器来至少干净地尝试这种自杀。这甚至可以在 OS 级别使用 sigprocmask
使自杀尽可能无种族歧视。