什么是信号?
信号(signal)是进程间通讯的一种方式,用来提醒进程某个事件已经发生。它属于一种异步通知进制。一个进程不必通过任何操作来等待信号的到达,事实上进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
信号接受与处理流程
在Linux系统中,我们可以通过kill -l
查看系统支持的信号。如果应用程序注册了某个信号处理的函数,那么当信号达到时候,则该函数会被调用,否则缺省的动作(action)被调用。
实际上信号的接收不是由用户进程来完成的,而是由内核代理处理。当一个进程P2向另一个进程P1发送信号后,,内核接受到信号,并将其放在P1的信号队列当中。当P1再次陷入内核态时(比如系统调用、中断或异常),会检查信号队列,并根据相应的信号调取相应的信号处理函数。
注意:
-
进程从用户态进入内核态是需要在内核态保存一份用户态堆栈的副本的,目的是为了当进程从内核态退出时候,能够还原之前的用户态调用上下文
-
信号处理程序执行完毕之后,进程会主动调用sigreturn()系统调用再次回到内核,是为了继续查看有没有其他信号需要处理,如果没有此时内核进行内核态栈帧的平衡恢复工作
信号处理的默认动作
信号分为非实时信号(不可靠信号)和实时信号(可靠信号)两种类型,分别对应于 Linux 的信号值为 1~31 和 3264。131信号也称为常规信号(regular signal),常规信号不具有信号缓存特性,当一个信号在处理过程中再来一个同样的信号,新来的信号会丢失,不会被缓存到队列,所以是不可靠信号。32~64作为实时信号(real-time signal),用户可以自定义信号处理程序,它具有信号缓存特性,所以称为可靠信号。
我们可以通过man 7 signal
命令查看信号默认处置动作,如何发送信号、以及信号列表等信息。
当应用程序收到信号时候,进程会根据信号类型来做出相应的处置动作来进行响应。信号处置动作有以下几种:
动作 | 功能 |
---|---|
Term | Default action is to terminate the process |
Ign | Default action is to ignore the signal |
Core | Default action is to terminate the process and dump core |
Stop | Default action is to stop the process |
Cont | Default action is to continue the process if it is currently stopped |
如何发送信号
我们可以通过系统调用或库函数来发送信号:
系统调用/库函数 | 功能 |
---|---|
raise | Sends a signal to the calling thread. |
kill | Sends a signal to a specified process, to all members of a specified process group, or to all processes on the system. |
killpg | Sends a signal to all of the members of a specified process group. |
pthread_kill | Sends a signal to a specified POSIX thread in the same process as the caller |
我们也可以直接通过kill
或pkill
命令发送信号给某个进程:
1 | kill process_id // 默认发送SIGTERM信号,用来终止进程 |
当进程运作在终端时候,我们可以通过特定组合键发送信号给该进程:
- Ctrl-C 发送 INT signal (SIGINT),通常导致进程结束
- Ctrl-Z 发送 TSTP signal (SIGTSTP); 通常导致进程挂起(suspend)
- Ctrl-\ 发送 QUIT signal (SIGQUIT); 通常导致进程结束 和 dump core.
- Ctrl-T (不是所有的UNIX都支持) 发送INFO signal (SIGINFO); 导致操作系统显示此运行命令的信息
信号类型
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 | 用户定义信号1 |
SIGUSR2 | 31,12,17 | Term | 用户定义信号2 |
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 | 后台程序向终端中写数据时触发 |
注意:
SIGKILL
和SIGSTOP
信号是不能被捕获,阻塞和忽略的- Window系统是不支持
SIGUSR1
和SIGUSR2
信号的
通过信号接收和处理,Nginx服务器能够完成配置重新加载,优雅退出等功能。在程序中我们也可以根据Nginx信号设计机制来完成我们的功能。下面列出Nginx(Master进程)处理的信号,以及对应的功能。
- ERM/INT 快速退出,当前的请求不执⾏完成就退出
- QUIT 优雅退出,执⾏完当前的请求后退出
- HUP 重新加载配置⽂件,⽤新的配置⽂件启动新worker进程,并优雅的关闭旧的worker进
程 - USR1 重新打开⽇志⽂件
- USR2 平滑的升级nginx⼆进制⽂件
Golang中的信号
Go程序对信号的默认行为
Go 语言实现了自己的运行时,对信号的默认处理方式会与标准Unix C应用有一些不太一样:
- SIGBUS(总线错误), SIGFPE(算术错误)和 SIGSEGV(段错误)称为同步信号,它们在程序执行错误时触发,而不是通过 os.Process.Kill 之类的触发。当捕获到此类信号时候,Go程序会产生runtime panic
- SIGHUP(挂起), SIGINT(中断)或 SIGTERM(终止)默认会使得程序终止退出
- SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT, SIGSYS 默认会使程序退出,并打印出每个Goroutine的栈跟踪(stack trace)信息
- SIGTSTP, SIGTTIN 或 SIGTTOU,这是 shell 使用的作业控制的信号,会执行系统默认的行为
- SIGPROF Go运行时使用该信号实现 runtime.CPUProfile(性能分析定时器,记录 CPU 时间,包括用户态和内核态)
对于SIGPIPE
信号,如果 Go 程序往一个 broken pipe
写数据,内核会产生一个SIGPIPE
信号。如果Go 程序没有为SIGPIPE
信号调用Notify
,对于写入对象是标准输出或标准错误,该信号会使得程序退出;但其他文件描述符(比如网络连接)对该信号是啥也不做,write会返回错误 EPIPE
。
如果 Go 程序为SIGPIPE
调用了Notify
,不论什么文件描述符,SIGPIPE
信号都会传递给 Notify channel,write 依然会返回 EPIPE。这也就是说Go的命令行程序跟传统的 Unix 命令行程序行为一致;但当往一个关闭的网络连接写数据时,传统 Unix 程序会crash,但 Go 程序不会。
signal包中的API
Golang中os/signal
包实现了信号发送、接收、忽略等功能。os/signal
包中API有以下几个:
Ignore 函数
用来忽略一个、多个或全部(不提供任何信号)信号。函数签名如下:
func Ignore(sig …os.Signal)
对一个信号,如果先调用 Notify,再调用 Ignore,Notify 的效果会被取消;如果先调用 Ignore,在调用 Notify,接着调用 Reset/Stop 的话,会起到Ingore 的效果
Notify 函数
通过通道实现类似给信号绑定信号处理函数的功能。
func Notify(c chan<- os.Signal, sig …os.Signal)
将输入信号转发到 chan c,若sig为空,则会把所有输入信号都传递到c。如果c阻塞了,siganl
包会直接放弃该信号,所有调用者应该保证c有足够的缓存的空间。对于使用单一信号通知的channel,缓存为1就足够了。
Stop 函数
用来让signal
包停止向通道转发信号。
func Stop(c chan<- os.Signal)
它会取消之前使用 c 调用的所有 Notify 的效果。当 Stop 返回后,会保证 c 不再接收到任何信号。
Reset 函数
用来重置信号的处理程序;若sig为空, 则所有信号处理都被重置。
func Reset(sig …os.Signal)
使用示例
监听所有信号
1 | func main() { |
注意:在实际使用中,一定要指定使用的信号,不要监听所有信号。在实际项目中,就遇到胡乱使用信号导致的问题:
1 | srv := &http.Server{ |
上面http服务程序想当然的认为信号都是由人为发送的(比如手动退出程序时候,kill命令),其实当server向已断开的客户端写入数据时候,系统会产生SIGPIPE信号。或者客户端向Go Http应用发送带外数据时候,系统内核会传递SIGURG信号给Go应用。这两种情况都会导致Go应用非常退出。
守护进程优雅退出
通过监听SIGQUIT
, SIGINT
等信号,我们可以实现http服务优雅退出功能:
1 | WaitGracefulExit(srv *http.Server) { |
打印stack trace信息
根据上面Go程序对信号的默认行为中的描述,Go应用程序在收到SIGQUIT
、SIGABRT
等信号时候会打印出所有Goroutine的栈跟踪信息,但会退出应用。如果我们想实现不退出也能打印出栈信息,可以监听信息SIGUSER1
信号并打印stack trace信息。
1 | c := make(chan os.Signal, 1) |
注意:window系统是不支持SIGUSER1
信号,如果要支持window系统,我们可以换成SIGHUP
信号。