为什么取这个名字,而不是直接写优雅退出呢?
实际上我一直觉得优雅退出这个名字并不直观,而优雅退出的本质,其实就是监听一些操作系统信号。在监听到退出信号的时候,可以做一些资源回收的操作,而不至于什么都不管直接退出,等到操作系统自己去处理回收这些资源。
操作系统中的信号
关于信号的介绍,可以用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 | 后台程序向终端中写数据时触发 |
第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列为对信号作用的注释性说明
需要特别说明的是,SIGKILL和SIGSTOP这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略。
实际上我们用的比较多的也是上面的其中几个信号,如 SIGHUP、SIGINT、SIGQUIT、SIGKILL、SIGTERM 这几个
kill 和 kill -9 的区别
既然是监听退出信号,我们就不得不提一下 kill 这个命令。通过上面的表格,我们可以知道,SIGKILL 和 SIGTERM 这两个信号对应的就是 kill 命令
kill pid 就是向这个 pid 的进程发送 SIGTERM 信号,这个信号可以被捕获接收、
kill -9 pid 就是向这个 pid 的进程发送 SIGKILL 信号,这个信号既不能被应用程序捕获,也不能被阻塞或忽略,相当于我们平时说的强制退出,在这种情况下应用想反应都来不及(SIGKILL 信号是直接发给 init 进程的,由 init 进程负责终止 pid 指定的进程)
USR1 和 USR2
我们继续看上面的表格,USR1 和 USR2 也是我们平常可能用到的,是为用户保留使用的两个信号,我们可以用来做一些额外的自定义功能
Go 优雅退出
要做到优雅退出,其实就是两件事:
在程序运行中监听信号
当收到了信号后,做资源关闭回收、打印日志、保留服务状态等操作
Notify
在 go 语言中,监听信号主要是用到了 os/signal
包中的notify
方法,也有Stop
停止监听信号的方法以及Reset
取消信号调用notify
的效果的方法等,感兴趣的可以去看一下源码实现,这里主要是提到notify
方法
基本用法
- 监听所有信号的操作
1 | c := make(chan os.Signal) |
- 监听指定信号的操作
1 | c := make(chan os.Signal) |
需要注意的是,代码里的os.Interrupt
等于syscall.SIGINT
,os.Kill
等于syscall.SIGKILL
我们可以进一步实践一下触发信号的效果:
1 | c := make(chan os.Signal) |
源码阅读
我们主要还是看notify
方法:
1 | var handlers struct { |
在官方注释说明中提到,需要传入一个带有缓冲区的 channel
,因为监听的信号是不阻塞地发到 ch
中的,如果 sig
当前没有被 recv
,则直接丢弃,造成了 sig
可能丢失的情况产生
1 | // signal_unix.go |
项目中的实践
1 | // 优雅退出go守护进程 |
这里面有几个关键点:
syscall.Getpid()
:可以直接获取进程启动的 pid,一般我们之间会持久化到一个文件中,可以方便直接查看,而不需要再去ps -ef
我们可以起一个 goroutine 去监听信号,当收到
kill
信号后对资源连接进行关闭,并打印关键日志,用于后续日志检索等在做完这些事情后,记得主动调用
os.Exit(0)
进行退出,因为我们已经捕获到kill
信号,进入了自定义的处理逻辑,如果调用exit
方法,则程序就不会进入结束环节了(kill -9
还是可以直接kill
掉的)