Signal信号处理

前言

信号(Signal)是Linux, 类Unix和其它POSIX兼容的操作系统中用来进程间通讯的一种方式。

对于Linux系统来说,信号就是软中断,用来通知进程发生了异步事件。

当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。

有时候我们想在Go程序中处理Signal信号,比如收到SIGTERM(15)信号后优雅的关闭程序,以及 goroutine结束通知等。

Go 语言提供了对信号处理的包(os/signal)。

Go 中对信号的处理主要使用os/signal包中的两个方法:

  • notify方法用来监听收到的信号;
  • stop方法用来取消监听。

Go信号通知机制可以通过往一个channel中发送os.Signal实现。

信号类型

每个个平台的信号定义或许有些不同。

下面列出了POSIX中定义的信号。

Linux 使用34-64信号用作实时系统中。

命令 man signal 提供了官方的信号介绍。

在POSIX.1-1990标准中定义的信号列表

信号 动作 说明
SIGHUP 1 Term 终端控制进程结束(终端连接断开)
SIGINT 2 Term 用户发送INTR字符(Ctrl+C)触发
SIGQUIT 3 Core 用户发送QUIT字符(Ctrl+/)触发
SIGILL 4 Core 非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT 6 Core 调用abort函数触发
SIGFPE 8 Core 算术运行错误(浮点运算错误、除数为零等)
SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV 11 Core 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE 13 Term 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM 14 Term 时钟定时信号
SIGTERM 15 Term 结束程序(可以被捕获、阻塞或忽略)
SIGUSR1 30,10,16 Term 用户保留
SIGUSR2 31,12,17 Term 用户保留
SIGCHLD 20,17,18 Ign 子进程结束(由父进程接收)
SIGCONT 19,18,25 Cont 继续执行已经停止的进程(不能被阻塞)
SIGSTOP 17,19,23 Stop 停止进程(不能被捕获、阻塞或忽略)
SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略)
SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发
SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

在SUSv2和POSIX.1-2001标准中的信号列表

信号 动作 说明
SIGTRAP 5 Core Trap指令触发(如断点,在调试器中使用)
SIGBUS 0,7,10 Core 非法地址(内存地址对齐错误)
SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF 27,27,29 Term 性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS 12,31,12 Core 无效的系统调用(SVr4)
SIGURG 16,23,21 Ign 有紧急数据到达Socket(4.2BSD)
SIGVTALRM 26,26,28 Term 虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU 24,24,30 Core 超过CPU时间资源限制(4.2BSD)
SIGXFSZ 25,25,31 Core 超过文件大小资源限制(4.2BSD)

第1列为信号名;

第2列为对应的信号值,需要注意的是,有些信号名对应着3个信号值,这是因为这些信号值与平台相关

将man手册中对3个信号值的说明摘出如下,the first one is usually valid for alpha and sparc, the middle one for i386, ppc and sh, and the last one for mips.

第3列为操作系统收到信号后的动作,Term表明默认动作为终止进程,Ign表明默认动作为忽略该信号,Core表明默认动作为终止进程同时输出core dump,Stop表明默认动作为停止进程。

第4列为对信号作用的注释性说明。

需要特别说明的是,SIGKILLSIGSTOP这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略

注册信号Notify

Notify用于将信号注册至对应channel,系统收到对应信号时会发送至此channel。

注意:channel一定要有足够的缓存接收信号,否则会因阻塞而导致信号发送失败。

func Notify(c chan<- os.Signal, sig ...os.Signal)

Notify使包信号将输入信号转发至channel c.
如果未指定信号,则所有输入信号都会被转发至c;否则仅转发指定的信号。
包信号发送至c时不会被阻塞:调用者必须确认c拥有足够的缓存空间来跟上预期的信号速率。
对于用于一个信号通知的channel,缓存大小为1是足够的。
对于同一channel允许多次调用Notify:
每次调用都会扩展发送到channel的信号集
调用Stop将信号移除信号集的唯一方式。
允许多次调用Notify,即使是不同的channel和同样的信号
每个channel独立接收输入信号的副本

例子

package main

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

func Ma() {
	go func() {
		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("Ctrl+C触发")
				return
			case syscall.SIGQUIT:
				fmt.Println("Ctrl+/触发")
				return
			case syscall.SIGTERM:
				fmt.Println("15 优雅的退出程序")
				return
			case syscall.SIGHUP:
				fmt.Println("终端控制进程结束(终端连接断开)")
			default:
				fmt.Println("default")
				return
			}
		}
	}()
	time.Sleep(1 * time.Hour)
}

实用例子

收到终止的信号做一些收尾的工作

// 检测信号
func listenSignal(file *os.File) {
	c := make(chan os.Signal, 1)
	//监听指定信号 SIGINT和SIGTERM。按下control+c向进程发送SIGINT信号,syscall.SIGTERM
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	for {
		select {
		case sig := <-c: //接收到终止信息
			fmt.Println("接收到信号 %d, 将退出\n", sig)
			file.Close()
			os.Exit(0)
		}
	}
}

取消监听stop

Stop用于取消channel对信号的监听。

Stop时,会将channel对应的数据从m中删除,为了避免数据竞争,对于即将stopping的数据会存储在[]stopping中,然后等待信号发送完毕,最后才移除stop的信号。

例子

package main

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

func Ma() {
	go func() {
		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("Ctrl+C触发")
				signal.Stop(ch)//取消监听
				return
			case syscall.SIGQUIT:
				fmt.Println("Ctrl+/触发")
				signal.Stop(ch)
				return
			case syscall.SIGTERM:
				fmt.Println("15 优雅的退出程序")
				signal.Stop(ch)
				return
			case syscall.SIGHUP:
				fmt.Println("终端控制进程结束(终端连接断开)")
				signal.Stop(ch)
				return
			default:
				fmt.Println("default")
			}
		}
	}()
	time.Sleep(1 * time.Hour)
}

上例中,如果不取消监听,当匹配到一次信号后,就会退出死循环,因为没有取消监听,信号还是会送入管道,但是因为退出了处理的死循环,就会无人无人处理信号,出现阻塞的状态。

posted @ 2022-11-15 10:58  厚礼蝎  阅读(216)  评论(0编辑  收藏  举报