Go 语言
标准库、反射和 cgo
系统信号的处理

系统信号的处理

系统信号(signal)是一种软件中断,它提供了一种异步的事件处理机制,用于操作系统内核或其他应用进程通知某一应用进程发生了某种事件。

我们来看一个例子:一个终端前台启动的程序,当用户按下中断键(ctrl+c)时,该程序的进程将会收到内核发来的中断信号(SIGINT)。

func main() {
    var wg sync.WaitGroup
    errChan := make(chan error, 1)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, Signal!")
    })
    wg.Add(1)
    go func() {
        defer wg.Done()
        errChan <- http.ListenAndServe(":8080", nil)
    }()
 
    select {
    case <-time.After(2 * time.Second):
        log.Println("Server started")
    case err := <-errChan:
        log.Println(err)
    }
    wg.Wait()
    log.Println("Server stopped")
}

我们在终端里启动上面的代码,并且在终端上按下中断键,但没有如愿出现程序退出提示。这是因为我们没有处理系统信号。

应用程序收到系统信号后,一般有三种处理方式:

  • 执行系统默认处理动作:终止进程;
  • 忽略信号;
  • 捕捉信号并执行自定义处理动作。

对于运行在生产环境下的程序,我们不要忽略系统信号的处理,而应采用捕捉退出信号的方式执行自定义的收尾处理函数。

Go 对信号的支持

信号机制经过多年的演进,已经变得十分复杂和烦琐。Go 语言将信号的复杂性留给了运行时层,为用户提供了体验相当友好的接口:os/signal 包。

在标准库 os/signal 包中,其中最主要的函数是 signal.Notify,它可以让程序接收指定的系统信号。

该函数可以设置捕捉那些应用关注的系统信号,并在 Go 运行时层与 Go 用户层之间用一个 channel 相连。当收到信号时,运行时层会将信号发送到 channel,用户层可以通过 channel 接收到信号。

我们来看一个例子:

package main
 
import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)
 
func main() {
    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)
 
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 
    go func() {
        sig := <-sigs
        fmt.Println(sig)
        done <- true
    }()
 
    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}

Go 将信号分成两大类:一类是同步信号;另一类是异步信号。

  • 同步信号:指那些由程序执行错误引发的信号,包括 SIGBUS(总线错误/硬件异常)、SIGFPE(算术异常)、SIGSEGV(段错误/无效内存引用) 。这些信号意味着应用出现了严重bug,无法继续执行下去。
  • 异步信号:同步信号之外的信号都被 Go 划到异步信号。异步信号不是由程序执行错误引起的,而是由其他程序或操作系统内核发出的。

异步信号中 SIGHUP、SIGINT、SIGTERM 这三个信号会导致程序直接退出;SIGQUIT、SIGILL、SIGTRAP、SIGABRT、SIGSTKFLT、SIGEMT 和 SIGSYS 在导致程序退出的同时,还会将程序退出的栈状态打印出来; SIGPROF 和 SIGVTALRM 信号用于性能分析,SIGXCPU 和 SIGXFSZ 信号用于处理 CPU 和文件大小超限的情况。另外,SIGKILL 和 SIGSTOP 信号是不能被捕捉和处理的。

实现程序的优雅退出

所谓优雅退出是指程序在退出前有机会等待尚未完成的事务处理、释放资源、关闭连接、保留中间状态、持久化数据等等。

与优雅退出对立的是强制退出,也就是我们常用的 kill -9,它会直接杀死进程,不给进程任何机会去处理未完成的事务。

下面我们来看一个 HTTP 服务结合系统信号实现优雅退出的例子:

func main() {
    var wg sync.WaitGroup
 
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, Signal!")
    })
    var srv = http.Server{Addr: ":8080"}
 
    srv.RegisterOnShutdown(func() {
        time.Sleep(5 * time.Second)
        wg.Done()
    })
 
    wg.Add(2)
    gofunc () {
        quit := make(chan os.Signal, 1)
        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
        <-quit
 
        timeoutCtx, cf := context.WithTimeout(context.Background(), 5*time.Second)
        defer cf()
        var done = make(chan struct{}, 1)
        go func() {
            err := srv.Shutdown(timeoutCtx)
            if err != nil {
                log.Println(err)
            }
            done <- struct{}{}
            wg.Done()
        }()
 
        select {
        case <-time.After(5 * time.Second):
            log.Println("Server shutdown timeout")
        case <-done:
            log.Println("Server shutdown")
        }
    }()
 
    err := srv.ListenAndServe()
    if err != nil {
        log.Println(err)
    }
    wg.Wait()
    log.Println("Server stopped")
}