操作系统信号 (signal) 是 IPC (InterProcess Communication 进程间通信) 中唯一一种异步的通信方法,它的本质是用软件来模拟硬件的中断机制。用来通知某个进程有某个事件发生了。
在 linux 中我们可以用kill
命令来查看当前系统所支持的信号,如下图:
可以看到,linux支持的信号有62种 (注意,没有编号32和33的信号)。其中编号从 1 到 31 的信号属于标准信号 (也称不靠谱信号),而编号从 34 到 64 的信号属于实时信号 (也称靠谱信号)。对于同一进程来说,每种标准信号只会被记录并处理一次。并且如果发送给某个进程的标准信号的种类有多个,那么它们的处理顺序也是完全不确定的。而实时信号解决了标准信号的两个问题,即多个同种类的实时信号都可以记录在案,并且他们可以按照信号的发送顺序被处理。虽然实时信号在功能上更为强大,但是已成为事实标准信号也无法被替换掉。因此,这两大类信号一直共存着。
而 Go 中信号要从 os.Signal
讲起,该类型的声明如下:
type Signal interface {
String() string
Signal() // to distinguish from other Stringers
}
从 so.Signal 接口的声明可知,其中 Signal 方法的声明并没有实际意义。它只是作为 os.Signal
接口类型的一个标识。因此,在 Go 标准库中,所有实现它的类型的 Signal 方法都是空方法。所以实现此接口类型的值都可以表示一个操作系统信号。
在 Go 标准库中,已经包含了与不同操作系统的信号相对应的程序实体。具体来说,标准库代码包 syscall
中有与不同操作系统所支持的每一个标准信号对应的同名常量 (简称信号常量
)。这些信号常量的类型都是 syscall.Signal
。syscall.Signal
是 os.Signal
接口的一个实现类型,同时也是一个 int
类型的别名类型。
另外,如果查看 syscall.Signal
类型的 String 方法的源码,还会发现一个包级私有的、名为 signals 的变量:
func (s Signal) String() string {
if 0 <= s && int(s) < len(signals) {
str := signals[s]
if str != "" {
return str
}
}
return "signal " + itoa.Itoa(int(s))
}
系统对应的文件,比如我的电脑系统:/usr/local/go/src/syscall/zerrors_darwin_arm64.go
// Signal table
var signals = [...]string{
1: "hangup",
2: "interrupt",
3: "quit",
4: "illegal instruction",
5: "trace/BPT trap",
6: "abort trap",
7: "EMT trap",
...
在这个数组类型的变量中,每个索引值都代表一个标准信号的编号,而对应的元素则是该信号的一个简短描述。
有了这些前提,就来看一下 os.signal
中的 Notify
函数,
func Notify(c chan<- os.Signal, sig ... os.Signal)
第一个参数是只能接受 os.Signal
类型值的通道类型,可以简称为 signal 的接收通道。这样改函数的调用方就可以从 signal 的接收通道中按顺序获取操作系统发来的信号并进行相应的处理了。
第二个参数是一个可变长的 os.Signal 类型参数,即任意多个 os.Signal 类型的参数。参数 sig 代表的参数值包含了我们希望自行处理的所有信号。接收到需要自行处理的信号后,os/signal
包中的程序,会吧它封装成syscall.Signal 类型的值并放入到 signal 接收通道中。举个例子:
sigRecv := make(chan os.Signal, 1)
sigs := []os.Signal{syscall.SIGINI, syscall.SIGQUIT}
signal.Notify(sigRecv, sigs)
for sig := range sigRecv {
fmt.Printf("signal: %s\n", sig)
}
注意,这个例子这样做比较危险,因为其中忽略了当前进程本该处理的信号(即默认信号)。这样它会无法通过ctrl+c来打断程序。
其实就是自定义处理信号会覆盖系统默认信号的处理方法,未定义的处理信号,还是会执行系统的默认操作。
不过,还好在类 Unix 操作系统下有两种信号既不能自行处理,也不会被忽略,他们就是 SIGKILL 和 SIGSTOP,对他们的响应只能是执行系统的默认操作。
对于其他信号,除了能够自行处理他们外,还可以在之后的任意时刻恢复他们的系统默认操作。这就要用到 os/signal 包中的 Stop 函数,声明如下:
func Stop(c chan<- os.Signal)
注意:调用完 signal.Stop 函数之后,作为参数的 signal 接收通道将不会再被发送任何信号。这里还会存在一个副作用,即在之前示例中那条用于从 signal 接收的通道信号的 for 语句将会一直阻塞。为了消除这种副作用,可以在调用 signal.Shop 函数之后,使用 close 函数关闭 signal 接收通道。
signal.Stop(sigRecv)
close(sigRecv)