Go标准库 os/signal 使用详解

本文介绍了Go语言中的os/signal包,用于处理进程信号。通过Notify函数可以监听和转发信号,Stop函数用于停止转发。优雅退出通常通过捕获SIGTERM信号,设置信号处理函数并在主循环中检查退出标志来实现。示例展示了如何创建一个能够优雅退出的守护进程。
摘要由CSDN通过智能技术生成

 

os/signal包实现了对输入信号的访问。这个包只有两个重要方法,这里向大家介绍一下,希望对你有帮助。

1. 信号的转发

Notify函数让signal包将输入信号转发到c。如果没有列出要传递的信号,会将所有输入信号传递到c;否则只传递列出的输入信号。

signal包不会为了向c发送信息而阻塞(就是说如果发送时c阻塞了,signal包会直接放弃):调用者应该保证c有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的通道,缓存为1就足够了。

可以使用同一通道多次调用Notify:每一次都会扩展该通道接收的信号集。唯一从信号集去除信号的方法是调用Stop。可以使用同一信号和不同通道多次调用Notify:每一个通道都会独立接收到该信号的一个拷贝。

 

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


举例:
package main

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

func main() {
	// 初始化一个os.Signal类型的channel
	// 我们必须使用缓冲通道,否则在信号发送时如果还没有准备好接收信号,就有丢失信号的风险。
	c := make(chan os.Signal, 1)
	// notify用于监听信号
	// 参数1表示接收信号的channel
	// 参数2及后面的表示要监听的信号
	// os.Interrupt 表示中断
	// os.Kill 杀死退出进程
	signal.Notify(c, os.Interrupt, os.Kill)
	// 阻塞直到接收到信息
	s := <-c
	fmt.Println("Got signal:", s)
}


输出:
$ go run main.go   // 注意:运行代码后按`Ctrl +C`发送信号结果为:
Got signal: interrupt

 

2. 停止转发信号 

Stop函数让signal包停止向c转发信号。它会取消之前使用c调用的所有Notify的效果。当Stop返回后,会保证c不再接收到任何信号。

func Stop(c chan<- os.Signal)

举例:
package main

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

func main() {
	// 初始化一个os.Signal类型的channel
	// 我们必须使用缓冲通道,否则在信号发送时如果还没有准备好接收信号,就有丢失信号的风险。
	ch := make(chan os.Signal)
	// notify用于监听信号 默认是所有信号
	signal.Notify(ch)

	//停止向ch转发信号,ch将不再收到任何信号
	signal.Stop(ch)
	fmt.Println("signal.Stop")
	//ch将一直阻塞在这里,因为它将收不到任何信号
	//所以下面的exit输出也无法执行
	s := <-ch
	fmt.Println("Got signal:", s)
}

输出:
$ go run main.go
signal.Stop
exit status 2

 

3. 优雅的退出守护进程

我们先来了解一下Golang中的信号类型:

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

信号值动作说明
SIGHUP1Term终端控制进程结束(终端连接断开)
SIGINT2Term用户发送INTR字符(Ctrl+C)触发
SIGQUIT3Core用户发送QUIT字符(Ctrl+/)触发
SIGILL4Core非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT6Core调用abort函数触发
SIGFPE8Core算术运行错误(浮点运算错误、除数为零等)
SIGKILL9Term无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV11Core无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE13Term消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM14Term时钟定时信号
SIGTERM15Term结束程序(可以被捕获、阻塞或忽略)
SIGUSR130,10,16Term用户保留
SIGUSR231,12,17Term用户保留
SIGCHLD20,17,18Ign子进程结束(由父进程接收)
SIGCONT19,18,25Cont继续执行已经停止的进程(不能被阻塞)
SIGSTOP17,19,23Stop停止进程(不能被捕获、阻塞或忽略)
SIGTSTP18,20,24Stop停止进程(可以被捕获、阻塞或忽略)
SIGTTIN21,21,26Stop后台程序从终端中读取数据时触发
SIGTTOU22,22,27Stop后台程序向终端中写数据时触发

 

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

信号动作说明
SIGTRAP5CoreTrap指令触发(如断点,在调试器中使用)
SIGBUS0,7,10Core非法地址(内存地址对齐错误)
SIGPOLL TermPollable event (Sys V). Synonym for SIGIO
SIGPROF27,27,29Term性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS12,31,12Core无效的系统调用(SVr4)
SIGURG16,23,21Ign有紧急数据到达Socket(4.2BSD)
SIGVTALRM26,26,28Term虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU24,24,30Core超过CPU时间资源限制(4.2BSD)
SIGXFSZ25,25,31Core

超过文件大小资源限制(4.2BSD)

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

 

kill命令的原理

通常我们在Linux系统中会使用kill命令来杀死进程,那其中的原理是什么呢?

kill pid 方式

kill pid的作用是向进程号为pid的进程发送SIGTERM(这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。

kill -9 pid 方式

kill -9 pid则是向进程号为pid的进程发送SIGKILL(该信号的编号为9),从本文上面的说明可知,SIGKILL既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法“感知”SIGKILL信号,它在完全无准备的情况下,就被收到SIGKILL信号的操作系统给干掉了,显然,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。在某些情况下(如进程已经hang死,无响应正常信号),就可以使用kill -9来结束进程。

 

从上面的介绍不难看出,优雅退出可以通过捕获SIGTERM来实现。具体来讲,通常只需要两步动作:

  • 注册SIGTERM信号的处理函数并在处理函数中做一些进程退出的准备。信号处理函数的注册可以通过signal()或sigaction()来实现,其中,推荐使用后者来实现信号响应函数的设置。信号处理函数的逻辑越简单越好,通常的做法是在该函数中设置一个bool型的flag变量以表明进程收到了SIGTERM信号,准备退出。
  • 在主进程的main()中,通过类似于while(!bQuit)的逻辑来检测那个flag变量,一旦bQuit在signal handler function中被置为true,则主进程退出while()循环,接下来就是一些释放资源或dump进程当前状态或记录日志的动作,完成这些后,主进程退出。

 

知道了这些,我们看一下下面的例子:

package main

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

// 优雅退出go守护进程
func main()  {
	//创建监听退出chan
	c := make(chan os.Signal)
	//监听指定信号 ctrl+c kill
	signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	go func() {
		for s := range c {
			switch s {
			case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
				fmt.Println("退出", s)
				ExitFunc()
			default:
				fmt.Println("other", s)
			}
		}
	}()

	fmt.Println("进程启动...")
	sum := 0
	for {
		sum++
		fmt.Println("sum:", sum)
		time.Sleep(time.Second)
	}
}

func ExitFunc()  {
	fmt.Println("开始退出...")
	fmt.Println("执行清理...")
	fmt.Println("结束退出...")
	os.Exit(0)
}

测试一下: 

$ go run main.go
进程启动...
sum: 1
sum: 2
sum: 3
退出 interrupt // Ctrl + C 
开始退出...
执行清理...
结束退出...

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值