目录


1.1. 基本概念

信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间。

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

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

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

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

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

1.2. Go 对信号的处理

os/signal

1.2.1. Go 程序对信号的默认行为

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

os.Process.Killruntime.CPUProfile

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

1.2.2. 改变信号的默认行为

os/signal
NotifyIgnoreResetStop

1.2.3. 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 程序不会。

1.2.4. cgo 注意事项

os/signal

1.3. signal 中 API 详解

1.3.1. Ignore 函数

func Ignore(sig ...os.Signal)
NotifyIgnoreNotifyIgnoreNotifyReset/Stop

1.3.2. Notify 函数

func Notify(c chan<- os.Signal, sig ...os.Signal)

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

signal

相关源码:

// src/os/signal/signal.go process 函数
for c, h := range handlers.m {
    if h.want(n) {
        // send but do not block for it
        select {
        case c <- sig:
        default:    // 保证不会阻塞,直接丢弃
        }
    }
}
NotifyStopNotify

1.3.3. Stop 函数

func Stop(c chan<- os.Signal)
NotifyStop

1.3.4. Reset 函数

func Reset(sig ...os.Signal)
Notify

1.3.5. 使用示例

注:syscall 包中定义了所有的信号常量

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

var firstSigusr1 = true

func main() {
    // 忽略 Control-C (SIGINT)
    // os.Interrupt 和 syscall.SIGINT 是同义词
    signal.Ignore(os.Interrupt)

    c1 := make(chan os.Signal, 2)
    // Notify SIGHUP
    signal.Notify(c1, syscall.SIGHUP)
    // Notify SIGUSR1
    signal.Notify(c1, syscall.SIGUSR1)
    go func() {
        for {
            switch <-c1 {
            case syscall.SIGHUP:
                fmt.Println("sighup, reset sighup")
                signal.Reset(syscall.SIGHUP)
            case syscall.SIGUSR1:
                if firstSigusr1 {
                    fmt.Println("first usr1, notify interrupt which had ignore!")
                    c2 := make(chan os.Signal, 1)
                    // Notify Interrupt
                    signal.Notify(c2, os.Interrupt)
                    go handlerInterrupt(c2)
                }
            }
        }
    }()

    select {}
}

func handlerInterrupt(c <-chan os.Signal) {
    for {
        switch <-c {
        case os.Interrupt:
            fmt.Println("signal interrupt")
        }
    }
}

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

1.3.6. 关于信号的额外说明

signal