1. 什麼是信號#
信號(Signal) 是 OS 中的一種用來進行進程間通信的方法,對於 Linux 系統來說,信號就是軟中斷,用來通知進程發生了某個事件,通常用於中斷進程的正常執行流,以便處理特定事件或者異常情況。
在不同的平台可能信號的定義會存在差異,每個信號對應著不同的值、動作和說明,在 Linux 系統中,我們可以使用 man signal
查看對應的信號介紹。
這裡提供了 POSIX signals 的參考,讀者可以自行查看。一個信號可能對應多個值,這個是因為這些信號值與平台相關。
信號的默認行為中,Term 表明默認動作為終止進程,Ign 表明默認動作為忽略該信號,Core 表明默認動作為終止進程同時輸出 core dump,Stop 表明默認動作為停止進程。
最後,SIGKILL
和 SIGSTOP
這兩個信號既不能被應用程序捕獲,也不能被操作系統阻塞或忽略。
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 中刪除對應的通道,但是直接刪除可能出現 數據競爭 的情況,特別是在信號處理和通道刪除存在並發操作的時候。
為了解決這個問題,它做了以下幾件事:
- 臨時存儲即將停止的信號:當 Stop 方法被調用時,信號處理機制會將即將停止的信號通道存儲在一個臨時的列表([] stopping)中,而不是立即從 map 中刪除。
- 等待信號發送完畢:信號處理機制會保證所有正在進行的信號發送操作完成。
- 最終移除信號通道:一旦所有信號發生操作完成,信號處理機制會從 map 中正式移除通信號通道,確保不會再有信號發送到已經停止的通道中。