banner
Rick Sanchez

Rick Sanchez

OS && DB 爱好者,深度学习炼丹师,蒟蒻退役Acmer,二刺螈。

Golang中的信號處理

1. 什麼是信號#

信號(Signal) 是 OS 中的一種用來進行進程間通信的方法,對於 Linux 系統來說,信號就是軟中斷,用來通知進程發生了某個事件,通常用於中斷進程的正常執行流,以便處理特定事件或者異常情況。

在不同的平台可能信號的定義會存在差異,每個信號對應著不同的值、動作和說明,在 Linux 系統中,我們可以使用 man signal查看對應的信號介紹。

這裡提供了 POSIX signals 的參考,讀者可以自行查看。一個信號可能對應多個值,這個是因為這些信號值與平台相關。

信號的默認行為中,Term 表明默認動作為終止進程,Ign 表明默認動作為忽略該信號,Core 表明默認動作為終止進程同時輸出 core dump,Stop 表明默認動作為停止進程。

最後,SIGKILLSIGSTOP 這兩個信號既不能被應用程序捕獲,也不能被操作系統阻塞或忽略。

2. Golang 中的信號處理#

在 Golang 中提供了對應的信號處理包: os/signal,主要使用下面 2 個方法:

  • signal.Notify () 方法用來監聽信號
  • signal.Stop () 方法用來取消監聽

2.1 signal.Notify 方法#

函數簽名: func Notify(c chan <- os.Signal, sig ... os.Signal)

這個方法的第二個參數是一個變長列表,可以指定多個監聽信號,這些信號會被轉發至第一個參數中傳入的通道中。如果未指定任何信號,則所有信號都會被轉發。

這個通道 c 應該是非阻塞的,signal 包不會為了向 c 發送信息而阻塞(如果阻塞了,singal 包會直接放棄),一般如果使用單一信號通知,容量設置為 1 即可。

下面我們使用一個簡單的例子來看看如何捕獲信號:

package main

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

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		ch := make(chan os.Signal, 1)
		signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP)
		for {
			s := <-ch
			switch s {
			case syscall.SIGINT:
				fmt.Println("\nSIGINT!")
				return
			case syscall.SIGQUIT:
				fmt.Println("\nSIGQUIT!")
				return
			case syscall.SIGTERM:
				fmt.Println("\nSIGTERM: Elegant exit")
				return
			case syscall.SIGHUP:
				fmt.Println("\nSIGHUP: Terminal connection disconnected")
				return
			default:
				fmt.Println("\nUnknown Signal!")
				return
			}
		}
	}()
	wg.Wait()
}

當運行了這個程序後,我們可以使用鍵盤快捷鍵或者使用 kill 命令向這個進程發送對應的信號。

一個使用的例子比如我們可以監聽終止信號完成優雅退出:

package main

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

// 檢測信號
func listenSignal(file *os.File) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	for {
		select {
		case sig := <-c:
			fmt.Printf("接收到信號 %s, 將退出\n", sig)
			file.Close()
			os.Exit(0)
		}
	}
}

func main() {
	file, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("無法打開文件:", err)
		return
	}
	defer file.Close()

	go listenSignal(file)

	fmt.Println("程序正在運行,按下 control+c 退出")
	select {} // 阻塞主線程
}

2.2 signal.Stop 方法#

函數簽名: func Stop(c chan<- os.Signal)

signal.Stop 方法會取消 channel 對信號的監聽行為:

  • 調用 signal.Stop (c) 後,會停止將任何信號轉發到通道 c
  • 參數 c 是一個只能發送 (send-only) 的信號通道
  • 被 Stop 的通道可以再次調用 Notify 方法重新監聽

下面的例子演示了一個 channel 調用了 Stop 方法停止監聽後再重新監聽的過程:

package main

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

func main() {
	c := make(chan os.Signal, 1)

	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)

	// 啟動一個 goroutine 來處理信號
	go func() {
		for sig := range c {
			fmt.Printf("接收到信號: %s\n", sig)
		}
	}()

	// 模擬程序運行一段時間
	fmt.Println("正在監聽信號...")
	time.Sleep(5 * time.Second)

	// 停止信號通知
	signal.Stop(c)
	fmt.Println("信號通知已停止")

	// 模擬程序運行一段時間,期間不會收到信號
	time.Sleep(5 * time.Second)

	// 重新註冊信號通知
	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
	fmt.Println("重新開始監聽信號...")

	// 再次模擬程序運行一段時間
	time.Sleep(10 * time.Second)
}

另外,為了避免數據競爭和確保信號處理的正確性, signal.Stop 方法在內部維護一個 map,將信號類型映射到通道列表,當信號來的時候會將信號發送到所有的對應通道,當刪除時,需要從這個 map 中刪除對應的通道,但是直接刪除可能出現 數據競爭 的情況,特別是在信號處理和通道刪除存在並發操作的時候。

為了解決這個問題,它做了以下幾件事:

  1. 臨時存儲即將停止的信號:當 Stop 方法被調用時,信號處理機制會將即將停止的信號通道存儲在一個臨時的列表([] stopping)中,而不是立即從 map 中刪除。
  2. 等待信號發送完畢:信號處理機制會保證所有正在進行的信號發送操作完成。
  3. 最終移除信號通道:一旦所有信號發生操作完成,信號處理機制會從 map 中正式移除通信號通道,確保不會再有信號發送到已經停止的通道中。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。