banner
Rick Sanchez

Rick Sanchez

OS && DB 爱好者,深度学习炼丹师,蒟蒻退役Acmer,二刺螈。

Golang中的信号处理

1. 什么是信号#

信号(Signal) 是 OS 中的一种用来进行进程间通信的方法,对于 Linux 系统来说,信号就是软中断,用来通知进程发生了某个事件,通常用于中断进程的正常执行流,以便处理特定事件或者异常情况。

在不同的平台可能信号的定义会存在差异,每个信号对应着不同的值、动作和说明,在 Linux 系统中,我们可以使用 man signal查看对应的信号介绍。

这里提供了 POSIX signals 的参考,读者可以自行查看。一个信号可能对应多个值,这个是因为这些信号值与平台相关。

信号的默认行为中,Term 表明默认动作为终止进程,Ign 表明默认动作为忽略该信号,Core 表明默认动作为终止进程同时输出 core dump,Stop 表明默认动作为停止进程。

最后,SIGKILLSIGSTOP 这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略。

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 中删除对应的通道,但是直接删除可能出现 数据竞争 的情况,特别是在信号处理和通道删除存在并发操作的时候。

为了解决这个问题,它做了以下几件事:

  1. 临时存储即将停止的信号:当 Stop 方法被调用时,信号处理机制会将即将停止的信号通道存储在一个临时的列表([] stopping)中,而不是立即从 map 中删除。
  2. 等待信号发送完毕:信号处理机制会保证所有正在进行的信号发送操作完成。
  3. 最终移除信号通道:一旦所有信号发生操作完成,信号处理机制会从 map 中正式移除通信号通道,确保不会再有信号发送到已经停止的通道中。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。