目录
- 信号
- 信号的种类
- 分类
- 信号种类
- Go Signal
- handlers
- handler
- 信号的存储
- 信号持有状态的获取
- 信号持有状态的清空
- Notify
- Stop
- Ignore & Reset
- NotifyContext
- 如何优雅的关闭web app
- 优雅地重启
信号
信号(Signal)是Linux, 类Unix和其它POSIX兼容的操作系统中用来进程间通讯的一种方式。对于Linux系统来说,信号就是软中断,用来通知进程发生了异步事件。
当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。
例:
- 输入命令,在Shell下启动一个前台进程。
- 用户按下Ctrl-C,键盘输入产生一个硬件中断。
- 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行, CPU从用户态切换到内核态处理硬件中断。
- 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。
- 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。
Go 语言提供了对信号处理的包(os/signal)。
Go 中对信号的处理主要使用os/signal包中的两个方法:一个是notify方法用来监听收到的信号;一个是 stop方法用来取消监听。
Go信号通知机制可以通过往一个channel中发送os.Signal实现。
信号的种类
使用命令查看:kill -l
分类
- 非可靠信号:1~31号信号,信号可能会丢失
- 可靠信号:34~64号信号,信号不可能丢失
信号种类
信号 | 值 | 动作 | 说明 |
---|---|---|---|
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 | 后台程序向终端中写数据时触发 |
Go Signal
- Go中关于信号的处理主要集中于os/signal package中。
- os/signal中涉及的function主要有:Notify、Stop、Ignore、Reset、NotifyContext。
- os/signal中信号的存储在handlers。
handlers
var handlers struct {
sync.Mutex
m map[chan<- os.Signal]*handler
ref [numSig]int64
stopping []stopping
}
type stopping struct {
c chan<- os.Signal
h *handler
}
type handler struct {
// numSig==65 一共65种信号
// mask [3]uint32
mask [(numSig + 31) / 32]uint32
}
- handlers包含了锁、具体存储channel及对应信号的map、信号接收map(用于确认信号是否需要开启/关闭)、待stop的channel
- Mutex锁用于handlers内的数据竞争管理。
- m中保存了信号需要发送的handler
- ref记录了每个信号的接收量
- stopping: 当信号被stop时映射channel到信号,不采用map是因为入口保留的只是很简洁的数据。需要1个独立的存储是因为需要m在任何时刻来对应ref,而且也需要保持对要被stop的channel的*handler值的追踪。
handler
-
当前所有系统的信号总数为65个,需要记录每个信号需要的状况,直接使用二进制可以极大的减少内存占用空间。
-
因此可以选用2个64位或3个32的数用于存储信号需要情况,多余的空间还可以用于以后的扩展。
-
3个32位数字相对于2个64位占用空间更小,因此采用3个32位数字用于存储信号需要状况。
type handler struct {
mask [(numSig + 31) / 32]uint32
}
func (h *handler) want(sig int) bool {
return (h.mask[sig/32]>>uint(sig&31))&1 != 0
}
func (h *handler) set(sig int) {
h.mask[sig/32] |= 1 << uint(sig&31)
}
func (h *handler) clear(sig int) {
h.mask[sig/32] &^= 1 << uint(sig&31)
}
信号的存储
(1)根据sig/32确定uint32在数组中的位置
(2)通过sig&31可以获取在uint32中的位置,左移相应位并设置对应位为1(或操作)
信号持有状态的获取
(1)根据sig/32确定uint32在数组中的位置
(2)通过sig&31可以获取在uint32中的位置,右移相应位,判断对应位是否为1(与操作)
信号持有状态的清空
(1)根据sig/32确定uint32在数组中的位置
(2)通过sig&31可以获取在uint32中的位置,右移相应位,通过异或清零(异或操作)
handler是1个[3]uint32数组,uint32的每位都可以可以存储对应的信号,意味着一个channel至多可以存储96个信号,当前所有系统的信号总数为65个,因此handler足以存储所有的信号。
因为信号要存储在数组中,因此通过sig/32确定uint32的位置,然后再根据设置/获取对应位的数据完成数据的存取。
Notify
- Notify用于将信号注册至对应channel,系统收到对应信号时会发送至此channel。
- channel一定要有足够的缓存接收信号,否则会因阻塞而导致信号发送失败。
Notify过程:
-
获取锁
-
检查当前channel是否已存入m,若没有,则创建新的handler存入。
-
若未指定信号,则依次将所有信号添加到handler中;否则将指定的信号依次添加到handler中
-
具体的添加过程如下:
- 检查信号,无效直接退出
- 检查信号是否已加入handler,没有的话将添加信号,若是全局第一次添加则启动信号监听循环
- 将对应信号的待接收数加1(handlers.ref[n]++)
-
释放锁
Stop
Stop用于取消channel对信号的监听。
Stop过程:
- 获取锁
- 若m中没当前channel,则退出并释放锁。
- m中存在当前channel的handler,先从m中移除channel。
- 遍历所有信号,若信号已在handler中注册,将待接收数减1,当接收数为0时,说明当前信号已不被Notify,关闭信号
- 存入stopping(此处为避免Stop时处于发送与关的竞争,选择发送后移除)
- 释放锁
- 等待stopping完成信号的发送
- 将当前channel的stopping移除
- 释放锁
Ignore & Reset
- Ignore用于全局忽略信号,忽略的信号,不会再被接收到
- Reset用于全局重置信号,重置后,可以接收到原已忽略的此信号
NotifyContext
NotifyContext相对Notify,多了context用以传递上下文,返回的stop可用以释放相关资源,内部调用的仍是Notify。
如何优雅的关闭web app
// +build go1.8
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// 开启一个goroutine启动服务
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
// kill 默认会发送 syscall.SIGTERM 信号
// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
<-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
log.Println("Shutdown Server ...")
// 创建一个5秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown: ", err)
}
log.Println("Server exiting")
}
按下Ctrl+C时会发送syscall.SIGINT来通知程序优雅关机
优雅地重启
package main
import (
"log"
"net/http"
"time"
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "hello gin!")
})
// 默认endless服务器会监听下列信号:
// syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
// 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)
// 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机
// 接收到 SIGUSR2 信号将触发HammerTime
// SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数
if err := endless.ListenAndServe(":8080", router); err!=nil{
log.Fatalf("listen: %s\n", err)
}
log.Println("Server exiting")
}
通过执行kill -1 pid命令发送syscall.SIGINT来通知程序优雅重启,具体做法如下:
- 打开终端,go build -o graceful_restart编译并执行./graceful_restart,终端输出当前pid(假设为43682)
- 将代码中处理请求函数返回的hello gin!修改为hello gin2i!,再次编译go build -o graceful_restart
- 打开一个浏览器,访问127.0.0.1:8080/,此时浏览器白屏等待服务端返回响应。
- 在终端迅速执行kill -1 43682命令给程序发送syscall.SIGHUP信号
- 等第3步浏览器收到响应信息hello gin!后再次访问127.0.0.1:8080/会收到hello gin2i!的响应。
- 在不影响当前未处理完请求的同时完成了程序代码的替换,实现了优雅重启。
但是需要注意的是,此时程序的PID变化了,因为endless 是通过fork子进程处理新请求,待原进程处理完当前请求后再退出的方式实现优雅重启的。所以当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。
无论是优雅关机还是优雅重启归根结底都是通过监听特定系统信号,然后执行一定的逻辑处理保障当前系统正在处理的请求被正常处理后再关闭当前进程。使用优雅关机还是使用优雅重启以及怎么实现,这就需要根据项目实际情况来决定了。