【问题标题】:Is it possible to capture a Ctrl+C signal (SIGINT) and run a cleanup function, in a "defer" fashion?是否可以以“延迟”方式捕获 Ctrl+C 信号并运行清理功能?
【发布时间】:2012-07-01 09:46:57
【问题描述】:

我想捕获从控制台发送的 Ctrl+C (SIGINT) 信号并打印出部分运行总数。

这在 Golang 中可行吗?

注意:当我第一次发布问题时,我对 Ctrl+CSIGTERM 而不是 SIGINT 感到困惑。

【问题讨论】:

    标签: signals go sigterm


    【解决方案1】:

    您可以使用os/signal 包来处理传入信号。 Ctrl+CSIGINT,所以你可以用它来捕获os.Interrupt

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    go func(){
        for sig := range c {
            // sig is a ^C, handle it
        }
    }()
    

    您导致程序终止和打印信息的方式完全取决于您。

    【讨论】:

    • 我看到你只是调用 Notify() 而不是 signal.Notify()。是一样的吗?
    • 除了for sig := range g {,您还可以使用<-sigchan,如上一个答案:stackoverflow.com/questions/8403862/…
    • @dystroy:当然,如果您实际上要终止程序以响应第一个信号。如果您碰巧决定终止程序,则使用循环可以捕获所有信号。
    • 注意:您必须实际构建程序才能使其工作。如果您在控制台中通过go run 运行程序并通过^C 发送SIGTERM,则信号会写入通道并且程序会响应,但似乎会意外退出循环。这是因为 SIGRERM 也转到 go run! (这让我很困惑!)
    • 请注意,为了让 goroutine 获得处理器时间来处理信号,主 goroutine 必须调用阻塞操作或在适当的位置调用 runtime.Gosched(在程序的主循环中,如果有一个)
    【解决方案2】:

    这行得通:

    package main
    
    import (
        "fmt"
        "os"
        "os/signal"
        "syscall"
        "time" // or "runtime"
    )
    
    func cleanup() {
        fmt.Println("cleanup")
    }
    
    func main() {
        c := make(chan os.Signal)
        signal.Notify(c, os.Interrupt, syscall.SIGTERM)
        go func() {
            <-c
            cleanup()
            os.Exit(1)
        }()
    
        for {
            fmt.Println("sleeping...")
            time.Sleep(10 * time.Second) // or runtime.Gosched() or similar per @misterbee
        }
    }
    

    【讨论】:

    • 对于其他读者:查看@adamonduty 的答案以了解为什么要捕获 os.Interrupt 和 syscall.SIGTERM 将他的解释包含在此答案中会很好,尤其是因为他比你早几个月发帖。
    • 为什么要使用非阻塞通道?有必要吗?
    • @Barry 为什么缓冲区大小是 2 而不是 1?
    • 这是documentation 的摘录。 “包信号不会阻塞发送到 c:调用者必须确保 c 有足够的缓冲区空间来跟上预期的信号速率。对于仅用于通知一个信号值的通道,大小为 1 的缓冲区就足够了。”
    • 此代码有效,但我有一个问题。如果我把os.Exit(1)改成os.Exit(0),然后运行echo $?,退出码是1而不是0。
    【解决方案3】:

    稍微补充一下其他答案,如果您真的想捕获 SIGTERM(kill 命令发送的默认信号),您可以使用syscall.SIGTERM 代替 os.Interrupt。请注意,系统调用接口是特定于系统的,可能无法在任何地方工作(例如在 Windows 上)。但它可以很好地抓住两者:

    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    ....
    

    【讨论】:

    • os.Kill 怎么样?
    • @Eclipse 好问题! os.Killcorrespondssyscall.Kill,这是一个可以发送但不能被捕获的信号。它相当于命令kill -9 &lt;pid&gt;。如果你想捕捉kill &lt;pid&gt;并优雅关机,你必须使用syscall.SIGTERM
    【解决方案4】:

    在上面接受的答案中有(在发布时)一两个小错字,所以这里是清理后的版本。在此示例中,我在收到 Ctrl+C 时停止 CPU 分析器。

    // capture ctrl+c and stop CPU profiler                            
    c := make(chan os.Signal, 1)                                       
    signal.Notify(c, os.Interrupt)                                     
    go func() {                                                        
      for sig := range c {                                             
        log.Printf("captured %v, stopping profiler and exiting..", sig)
        pprof.StopCPUProfile()                                         
        os.Exit(1)                                                     
      }                                                                
    }()    
    

    【讨论】:

    • 请注意,为了让 goroutine 获得处理器时间来处理信号,主 goroutine 必须调用阻塞操作或在适当的位置调用 runtime.Gosched(在程序的主循环中,如果有一个)
    【解决方案5】:

    上述所有方法在拼接时似乎都可以工作,但gobyexample's signals page 有一个非常干净和完整的信号捕获示例。值得添加到此列表中。

    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()
            fmt.Println(sig)
            done <- true
        }()
    
        fmt.Println("awaiting signal")
        <-done
        fmt.Println("exiting")
    }
    

    来源:gobyexample.com/signals

    【讨论】:

      【解决方案6】:

      look at the example

      当我们运行这个程序时,它会阻塞等待信号。通过输入 ctrl-C(终端显示为 ^C)我们可以发送一个 SIGINT 信号,导致程序打印中断然后退出。

      信号。 Notify 注册给定的通道以接收指定信号的通知。

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

      检测HTTP请求取消

      
      
      package main
      
      import (
          "fmt"
          "net/http"
          "time"
      )
      
      func main() {
      
          mux := http.NewServeMux()
          mux.HandleFunc("/path", func(writer http.ResponseWriter, request *http.Request) {
      
              time.Sleep(time.Second * 5)
      
              select {
              case <-time.After(time.Millisecond * 10):
      
                  fmt.Println("started")
                  return
              case <-request.Context().Done():
                  fmt.Println("canceled")
              }
          })
      
          http.ListenAndServe(":8000", mux)
      
      }
      

      【讨论】:

        【解决方案7】:

        您可以使用不同的 goroutine 来检测 syscall.SIGINT 和 syscall.SIGTERM 信号,并使用 signal.Notify 将它们中继到通道。您可以使用通道向该 goroutine 发送钩子并将其保存在函数切片中。当在通道上检测到关闭信号时,您可以在切片中执行这些功能。这可用于清理资源、等待运行的 goroutine 完成、持久化数据或打印部分运行总计。

        我编写了一个小而简单的实用程序来在关机时添加和运行挂钩。希望它能有所帮助。

        https://github.com/ankit-arora/go-utils/blob/master/go-shutdown-hook/shutdown-hook.go

        您可以以“延迟”方式执行此操作。

        优雅关闭服务器的示例:

        srv := &http.Server{}
        
        go_shutdown_hook.ADD(func() {
            log.Println("shutting down server")
            srv.Shutdown(nil)
            log.Println("shutting down server-done")
        })
        
        l, err := net.Listen("tcp", ":3090")
        
        log.Println(srv.Serve(l))
        
        go_shutdown_hook.Wait()
        

        【讨论】:

          【解决方案8】:

          这是另一个版本,可以在您有一些清理任务时使用。代码将在他们的方法中留下清理过程。

          package main
          
          import (
              "fmt"
              "os"
              "os/signal"
              "syscall"
          
          )
          
          
          
          func main() {
          
              _,done1:=doSomething1()
              _,done2:=doSomething2()
          
              //do main thread
          
          
              println("wait for finish")
              <-done1
              <-done2
              fmt.Print("clean up done, can exit safely")
          
          }
          
          func doSomething1() (error, chan bool) {
              //do something
              done:=make(chan bool)
              c := make(chan os.Signal, 2)
              signal.Notify(c, os.Interrupt, syscall.SIGTERM)
              go func() {
                  <-c
                  //cleanup of something1
                  done<-true
              }()
              return nil,done
          }
          
          
          func doSomething2() (error, chan bool) {
              //do something
              done:=make(chan bool)
              c := make(chan os.Signal, 2)
              signal.Notify(c, os.Interrupt, syscall.SIGTERM)
              go func() {
                  <-c
                  //cleanup of something2
                  done<-true
              }()
              return nil,done
          }
          

          如果您需要清理主函数,您还需要使用 go func() 在主线程中捕获信号。

          【讨论】:

            【解决方案9】:

            Death 是一个简单的库,它使用通道和等待组来等待关闭信号。收到信号后,它将在您要清理的所有结构上调用 close 方法。

            【讨论】:

            • 这么多行代码和一个外部库依赖来做四行代码可以做什么? (根据接受的答案)
            • 它允许您并行清理所有结构,如果结构具有标准关闭接口,则自动关闭它们。
            【解决方案10】:

            如果有人需要一种在 Windows 上处理信号的方法,仅供记录。

            我需要通过 os/exec 从程序 A 调用程序 B 进行处理,但程序 B 永远无法正常终止,因为 Windows 不支持通过 cmd.Process.Signal(syscall.SIGTERM) 发送信号或其他信号。

            我通过创建一个临时文件作为信号来解决这个问题。例如,程序 A 创建文件 .signal.term 并且程序 B 需要检查该文件是否存在于间隔基础上。如果文件存在,它将退出程序并在需要时进行清理。

            我敢肯定还有其他方法,但这确实有效。

            【讨论】:

              猜你喜欢
              • 2017-07-15
              • 1970-01-01
              • 1970-01-01
              • 2017-07-31
              • 1970-01-01
              • 2016-05-29
              • 1970-01-01
              • 2017-03-01
              • 1970-01-01
              相关资源
              最近更新 更多