1. 什么是信号#
信号(Signal) 是 OS 中的一种用来进行进程间通信的方法,对于 Linux 系统来说,信号就是软中断,用来通知进程发生了某个事件,通常用于中断进程的正常执行流,以便处理特定事件或者异常情况。
在不同的平台可能信号的定义会存在差异,每个信号对应着不同的值、动作和说明,在 Linux 系统中,我们可以使用 man signal
查看对应的信号介绍。
这里提供了 POSIX signals 的参考,读者可以自行查看。一个信号可能对应多个值,这个是因为这些信号值与平台相关。
信号的默认行为中,Term 表明默认动作为终止进程,Ign 表明默认动作为忽略该信号,Core 表明默认动作为终止进程同时输出 core dump,Stop 表明默认动作为停止进程。
最后,SIGKILL
和 SIGSTOP
这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略。
2. Golang 中的信号处理#
在 Golang 中提供了对应的信号处理包: os/signal
,主要使用下面 2 个方法:
- signal.Notify () 方法用来监听信号
- signal.Stop () 方法用来取消监听
2.1 signal.Notify 方法#
函数签名: func Notify(c chan <- os.Signal, sig ... os.Signal)
这个方法的第二个参数是一个变长列表,可以指定多个监听信号,这些信号会被转发至第一个参数中传入的通道中。如果未指定任何信号,则所有信号都会被转发。
这个通道 c 应该是非阻塞的,signal 包不会为了向 c 发送信息而阻塞(如果阻塞了,singal 包会直接放弃),一般如果使用单一信号通知,容量设置为 1 即可。
下面我们使用一个简单的例子来看看如何捕获信号:
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP)
for {
s := <-ch
switch s {
case syscall.SIGINT:
fmt.Println("\nSIGINT!")
return
case syscall.SIGQUIT:
fmt.Println("\nSIGQUIT!")
return
case syscall.SIGTERM:
fmt.Println("\nSIGTERM: Elegant exit")
return
case syscall.SIGHUP:
fmt.Println("\nSIGHUP: Terminal connection disconnected")
return
default:
fmt.Println("\nUnknown Signal!")
return
}
}
}()
wg.Wait()
}
当运行了这个程序后,我们可以使用键盘快捷键或者使用 kill
命令向这个进程发送对应的信号。
一个使用的例子比如我们可以监听终止信号完成优雅退出:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
// 检测信号
func listenSignal(file *os.File) {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case sig := <-c:
fmt.Printf("接收到信号 %s, 将退出\n", sig)
file.Close()
os.Exit(0)
}
}
}
func main() {
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("无法打开文件:", err)
return
}
defer file.Close()
go listenSignal(file)
fmt.Println("程序正在运行,按下 control+c 退出")
select {} // 阻塞主线程
}
2.2 signal.Stop 方法#
函数签名: func Stop(c chan<- os.Signal)
signal.Stop
方法会取消 channel 对信号的监听行为:
- 调用 signal.Stop (c) 后,会停止将任何信号转发到通道 c
- 参数 c 是一个只能发送 (send-only) 的信号通道
- 被 Stop 的通道可以再次调用 Notify 方法重新监听
下面的例子演示了一个 channel 调用了 Stop 方法停止监听后再重新监听的过程:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
// 启动一个 goroutine 来处理信号
go func() {
for sig := range c {
fmt.Printf("接收到信号: %s\n", sig)
}
}()
// 模拟程序运行一段时间
fmt.Println("正在监听信号...")
time.Sleep(5 * time.Second)
// 停止信号通知
signal.Stop(c)
fmt.Println("信号通知已停止")
// 模拟程序运行一段时间,期间不会收到信号
time.Sleep(5 * time.Second)
// 重新注册信号通知
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("重新开始监听信号...")
// 再次模拟程序运行一段时间
time.Sleep(10 * time.Second)
}
另外,为了避免数据竞争和确保信号处理的正确性, signal.Stop
方法在内部维护一个 map,将信号类型映射到通道列表,当信号来的时候会将信号发送到所有的对应通道,当删除时,需要从这个 map 中删除对应的通道,但是直接删除可能出现 数据竞争 的情况,特别是在信号处理和通道删除存在并发操作的时候。
为了解决这个问题,它做了以下几件事:
- 临时存储即将停止的信号:当 Stop 方法被调用时,信号处理机制会将即将停止的信号通道存储在一个临时的列表([] stopping)中,而不是立即从 map 中删除。
- 等待信号发送完毕:信号处理机制会保证所有正在进行的信号发送操作完成。
- 最终移除信号通道:一旦所有信号发生操作完成,信号处理机制会从 map 中正式移除通信号通道,确保不会再有信号发送到已经停止的通道中。