信号处理与Go程序的优雅退出

信号处理与Go程序的优雅退出

学过计算机系统的人,应该知道异常控制流(ECF)。异常控制流发生在计算机系统的各个层次。比如,在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序。在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程。在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序。

图片

什么是信号

本文讨论的就是在应用层次的异常,也被称为信号。我们知道,在正常情况下,低层次的硬件异常是由内核异常处理程序处理的,对用户进程而来是不可见的。而信号提供了一种机制,通知用户进程发生了这些异常。

简单来说,一个信号就是一条事件消息,它通知进程系统中发生了一个某种类型的事件。

举几个常见的例子(以linux为例)。SIGINT,它就是通过我们在终端输入的Ctrl+C进行触发;SIGKILL,即终端输入kill -9;SIGUSR1和SIGUSR2,用户自定义信号。

可以通过kill -l查看本机操作系统提供了哪些信号,以下是小菜刀mac上支持的信号列表。

1
2
3
4
5
6
7
8
9
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGEMT 8) SIGFPE
9) SIGKILL 10) SIGBUS 11) SIGSEGV 12) SIGSYS
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGURG
17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD
21) SIGTTIN 22) SIGTTOU 23) SIGIO 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGINFO 30) SIGUSR1 31) SIGUSR2

如果你想知道每种信号,代表什么含义,可通过man signal命令查看信号注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ man signal
No Name Default Action Description
1 SIGHUP terminate process terminal line hangup
2 SIGINT terminate process interrupt program
3 SIGQUIT create core image quit program
4 SIGILL create core image illegal instruction
5 SIGTRAP create core image trace trap
6 SIGABRT create core image abort program (formerly SIGIOT)
7 SIGEMT create core image emulate instruction executed
8 SIGFPE create core image floating-point exception
9 SIGKILL terminate process kill program
10 SIGBUS create core image bus error
11 SIGSEGV create core image segmentation violation
12 SIGSYS create core image non-existent system call invoked
13 SIGPIPE terminate process write on a pipe with no reader
14 SIGALRM terminate process real-time timer expired
15 SIGTERM terminate process software termination signal
16 SIGURG discard signal urgent condition present on socket
17 SIGSTOP stop process stop (cannot be caught or ignored)
18 SIGTSTP stop process stop signal generated from keyboard
19 SIGCONT discard signal continue after stop
20 SIGCHLD discard signal child status has changed
21 SIGTTIN stop process background read attempted from control terminal
22 SIGTTOU stop process background write attempted to control terminal
23 SIGIO discard signal I/O is possible on a descriptor (see fcntl(2))
24 SIGXCPU terminate process cpu time limit exceeded (see setrlimit(2))
25 SIGXFSZ terminate process file size limit exceeded (see setrlimit(2))
26 SIGVTALRM terminate process virtual time alarm (see setitimer(2))
27 SIGPROF terminate process profiling timer alarm (see setitimer(2))
28 SIGWINCH discard signal Window size change
29 SIGINFO discard signal status request from keyboard
30 SIGUSR1 terminate process User defined signal 1
31 SIGUSR2 terminate process User defined signal 2

注意:SIGKILL和SIGSTOP这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略,这意味着当你在使用kill -9的时候一定要十分的慎重!

信号监听

Go中对信号的监听主要通过os/sgnal包下的四个方法

  • Notify:监听信号

  • Stop:取消监听

  • Reset:重置监听

  • Ignore/Ignored:忽略信号

监听示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func signalHandler() {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan) // 监听所有信号。注意,Notify接收可变参数,可以指定监听信号。

go func() {
for {
sig := <-signalChan // 监听到信号
log.Printf("got signal to exit [signal = %v]", sig)
}
}()
}

func main() {
signalHandler()
select {}
}

在goland中执行代码输入示例

1
2
3
4
5
6
2020/05/31 11:28:31 got signal to exit [signal = window size changes]
2020/05/31 11:28:31 got signal to exit [signal = urgent I/O condition]
2020/05/31 11:28:31 got signal to exit [signal = window size changes]
2020/05/31 11:28:32 got signal to exit [signal = interrupt]

Process finished with exit code 9

注意:在该代码中,你再也不能通过Ctrl+C关闭程序了(上述输出第四行代表的就是接收到SIGINT信号),你只能采用kill -9的方式关闭(上述最后一行代表的就是接收到SIGKILL信号)。

当然,你也可以指定监听特定信号。

1
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)

需要注意的是,不同系统平台的信号定义会有所差别,可在syscall包下zerrors_xxx_yyy.go中找到对应信号(其中,xxx代表操作系统,yyy代表硬件体系。例如小菜刀的机器上对应的就是zerrors_darwin_amd64.go文件中的信号定义)。

Go程序优雅退出

我们都知道,应用程序在部署上线后,都会遇到升级维护的问题,一般需要停掉应用程序,再更新代码,启动程序。但是,我们在停掉线上运行着的应用程序时,通常不能简单粗暴的直接kill,需要做一些退出前处理(例如关掉数据库连接,持久化日志,清理应用垃圾等),而退出处理的触发就是通过信号接收处理。

因此,程序的优雅退出即是,在程序退出之前,相关资源得到妥善地处理。以下为优雅退出示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func SetupSignalHandler(shutdownFunc func(bool)) {
closeSignalChan := make(chan os.Signal, 1)
// 监听四种关闭信号
signal.Notify(closeSignalChan,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)

go func() {
sig := <-closeSignalChan
log.Printf("got signal to exit [signal = %v]", sig)
//判断关闭信号是否为SIGQUIT(用户发送Ctrl+/即可触发)
shutdownFunc(sig == syscall.SIGQUIT)
}()
}

func shutdown(isgraceful bool) {
if isgraceful {
//当满足 sig == syscall.SIGQUIT,做相应退出处理
}
// 不是syscall.SIGQUIT的退出信号时,做相应退出处理
}

func main() {
SetupSignalHandler(shutdown) // 注册监听信号,绑定信号处理机制
select {} // 模拟应用程序一直保持在线运行
}

参考文章

  1. 《深入理解计算机系统 第三版》

  2. https://golang.google.cn/pkg/os/signal/

图片

以上内容转载自机器铃砍菜刀

-------------本文结束 感谢阅读-------------