os/signal 包实现了对输入信号的访问。这个包只有两个重要方法。
信号的转发
Notify 函数让 signal 包将输入信号转发到 c。如果没有列出要传递的信号,会将所有输入信号传递到 c;否则只传递列出的输入信号。
signal 包不会为了向 c 发送信息而阻塞(就是说如果发送时 c 阻塞了,signal 包会直接放弃):调用者应该保证 c 有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的通道,缓存为 1 就足够了。
可以使用同一通道多次调用 Notify:每一次都会扩展该通道接收的信号集。
唯一从信号集去除信号的方法是调用 Stop。
可以使用同一信号和不同通道多次调用 Notify:每一个通道都会独立接收到该信号的一个拷贝。
1 | func Notify(c chan<- os.Signal, sig ...os.Signal) |
Example:
1 | package main |
Output:
1 | $ go run main.go // 注意:运行代码后按`Ctrl +C`发送信号结果为: |
停止转发信号
Stop 函数让 signal 包停止向 c 转发信号。它会取消之前使用 c 调用的所有 Notify 的效果。当 Stop 返回后,会保证 c 不再接收到任何信号。
1 | func Stop(c chan<- os.Signal) |
Example:
1 | package main |
Output:
1 | $ go run main.go |
优雅的退出守护进程
我们先来了解一下 Golang 中的信号类型:
在 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 | 后台程序向终端中写数据时触发 |
在 SUSv2 和 POSIX.1-2001 标准中的信号列表:
信号 | 值 | 动作 | 说明 |
---|---|---|---|
SIGTRAP | 5 | Core | Trap 指令触发 (如断点,在调试器中使用) |
SIGBUS | 0,7,10 | Core | 非法地址 (内存地址对齐错误) |
SIGPOLL | Term | Pollable event (Sys V). Synonym for SIGIO | |
SIGPROF | 27,27,29 | Term | 性能时钟信号 (包含系统调用时间和进程占用 CPU 的时间) |
SIGSYS | 12,31,12 | Core | 无效的系统调用 (SVr4) |
SIGURG | 16,23,21 | Ign | 有紧急数据到达 Socket (4.2BSD) |
SIGVTALRM | 26,26,28 | Term | 虚拟时钟信号 (进程占用 CPU 的时间)(4.2BSD) |
SIGXCPU | 24,24,30 | Core | 超过 CPU 时间资源限制 (4.2BSD) |
SIGXFSZ | 25,25,31 | Core | 超过文件大小资源限制 (4.2BSD) |
注意:需要特别说明的是,SIGKILL 和 SIGSTOP 这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略。
通常我们在 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 进程当前状态或记录日志的动作,完成这些后,主进程退出。
知道了这些,我们看一下下面的例子:
1 | package main |
测试一下:
1 | $ go run main.go |