os/signal - 信号

    因为一个具有合适权限的进程可以向另一个进程发送信号,这可以称为进程间的一种同步技术。当然,进程也可以向自身发送信号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下。

    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令(除 0,引用无法访问的内存区域)。
    • 用户键入了能够产生信号的终端特殊字符。如中断字符(通常是 Control-C)、暂停字符(通常是 Control-Z)。
    • 发生了软件事件。如调整了终端窗口大小,定时器到期等。

    针对每个信号,都定义了一个唯一的(小)整数,从 1 开始顺序展开。系统会用相应常量表示。Linux 中,1-31 为标准信号;32-64 为实时信号(通过 可以查看)。

    信号达到后,进程视具体信号执行如下默认操作之一。

    • 忽略信号,也就是内核将信号丢弃,信号对进程不产生任何影响。
    • 终止(杀死)进程。
    • 产生 coredump 文件,同时进程终止。
    • 暂停(Stop)进程的执行。
    • 恢复进程执行。

    当然,对于有些信号,程序是可以改变默认行为的,这也就是 os/signal 包的用途。

    兼容性问题:信号的概念来自于 Unix-like 系统。Windows 下只支持 os.SIGINT 信号。

    程序无法捕获信号 SIGKILL 和 SIGSTOP (终止和暂停进程),因此 os/signal 包对这两个信号无效。

    Go 语言实现了自己的运行时,因此,对信号的默认处理方式和普通的 C 程序不太一样。

    • SIGBUS(总线错误), SIGFPE(算术错误)和 SIGSEGV(段错误)称为同步信号,它们在程序执行错误时触发,而不是通过 os.Process.Kill 之类的触发。通常,Go 程序会将这类信号转为 run-time panic。
    • SIGHUP(挂起), SIGINT(中断)或 SIGTERM(终止)默认会使得程序退出。
    • SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT 或 SIGSYS 默认会使得程序退出,同时生成 stack dump。
    • SIGTSTP, SIGTTIN 或 SIGTTOU,这是 shell 使用的,作业控制的信号,执行系统默认的行为。
    • SIGPROF(性能分析定时器,记录 CPU 时间,包括用户态和内核态), Go 运行时使用该信号实现 runtime.CPUProfile

    信号可以被忽略或通过掩码阻塞(屏蔽字 mask)。忽略信号通过 signal.Ignore,没有导出 API 可以直接修改阻塞掩码,虽然 Go 内部有实现 sigprocmask 等。Go 中的信号被 runtime 控制,在使用时和 C 是不太一样的。

    改变信号的默认行为

    Notify 改变信号处理,可以改变信号的默认行为;Ignore 可以忽略信号;Reset 重置信号为默认行为;Stop 则停止接收信号,但并没有重置为默认行为。

    SIGPIPE

    文档中对这个信号单独进行了说明。如果 Go 程序往一个 broken pipe 写数据,内核会产生一个 SIGPIPE 信号。

    如果 Go 程序没有为 SIGPIPE 信号调用 Notify,对于标准输出或标准错误(文件描述符 1 或 2),该信号会使得程序退出;但其他文件描述符对该信号是啥也不做,当然 write 会返回错误 EPIPE。

    如果 Go 程序为 SIGPIPE 调用了 Notify,不论什么文件描述符,SIGPIPE 信号都会传递给 Notify channel,当然 write 依然会返回 EPIPE。

    也就是说,默认情况下,Go 的命令行程序跟传统的 Unix 命令行程序行为一致;但当往一个关闭的网络连接写数据时,传统 Unix 程序会 crash,但 Go 程序不会。

    如果非 Go 代码使用信号相关功能,需要仔细阅读掌握 os/signal 包中相关文档:Go programs that use cgo or SWIG 和 Non-Go programs that call Go code

    Ignore 函数

    func Ignore(sig ...os.Signal)

    忽略一个、多个或全部(不提供任何信号)信号。如果程序接收到了被忽略的信号,则什么也不做。对一个信号,如果先调用 Notify,再调用 IgnoreNotify 的效果会被取消;如果先调用 Ignore,在调用 Notify,接着调用 Reset/Stop 的话,会回到 Ingore 的效果。注意,如果 Notify 作用于多个 chan,则 Stop 需要对每个 chan 都调用才能起到该作用。

    Notify 函数

    类似于绑定信号处理程序。将输入信号转发到 chan c。如果没有列出要传递的信号,会将所有输入信号传递到 c;否则只传递列出的输入信号。

    channel c 缓存如何决定?因为 signal 包不会为了向 c 发送信息而阻塞(就是说如果发送时 c 阻塞了,signal 包会直接放弃):调用者应该保证 c 有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的 channel,缓存为 1 就足够了。

    相关源码:

    可以使用同一 channel 多次调用 Notify:每一次都会扩展该 channel 接收的信号集。唯一从信号集去除信号的方法是调用 Stop。可以使用同一信号和不同 channel 多次调用 Notify:每一个 channel 都会独立接收到该信号的一个拷贝。

    func Stop(c chan<- os.Signal)

    让 signal 包停止向 c 转发信号。它会取消之前使用 c 调用的所有 Notify 的效果。当 Stop 返回后,会保证 c 不再接收到任何信号。

    Reset 函数

    取消之前使用 Notify 对信号产生的效果;如果没有参数,则所有信号处理都被重置。

    使用示例

    1. package main
    2. import (
    3. "fmt"
    4. "os"
    5. "os/signal"
    6. "syscall"
    7. )
    8. var firstSigusr1 = true
    9. func main() {
    10. // 忽略 Control-C (SIGINT)
    11. // os.Interrupt 和 syscall.SIGINT 是同义词
    12. signal.Ignore(os.Interrupt)
    13. c1 := make(chan os.Signal, 2)
    14. // Notify SIGHUP
    15. signal.Notify(c1, syscall.SIGHUP)
    16. // Notify SIGUSR1
    17. signal.Notify(c1, syscall.SIGUSR1)
    18. go func() {
    19. for {
    20. switch <-c1 {
    21. fmt.Println("sighup, reset sighup")
    22. signal.Reset(syscall.SIGHUP)
    23. if firstSigusr1 {
    24. fmt.Println("first usr1, notify interrupt which had ignore!")
    25. c2 := make(chan os.Signal, 1)
    26. // Notify Interrupt
    27. signal.Notify(c2, os.Interrupt)
    28. go handlerInterrupt(c2)
    29. }
    30. }
    31. }
    32. }()
    33. select {}
    34. }
    35. func handlerInterrupt(c <-chan os.Signal) {
    36. for {
    37. switch <-c {
    38. case os.Interrupt:
    39. fmt.Println("signal interrupt")
    40. }
    41. }
    42. }

    编译后运行,先后给该进程发送如下信号:SIGINT、SIGUSR1、SIGINT、SIGHUP、SIGHUP,看输出是不是和你预期的一样。

    1. 查看 Go 中 Linux/amd64 信号的实现,发现大量使用的是 rt 相关系统调用,这是支持实时信号处理的 API。
    2. C 语言中信号处理涉及到可重入函数和异步信号安全函数问题;Go 中不存在此问题。
    3. Unix 和信号处理相关的很多系统调用,Go 都隐藏起来了,Go 中对信号的处理, 包中的函数基本就能搞定。

    导航