1. 信号とは#
信号(Signal) は OS におけるプロセス間通信の方法の一つであり、Linux システムにおいては、信号はソフトウェア割り込みであり、プロセスに特定のイベントが発生したことを通知するために使用されます。通常、特定のイベントや異常な状況を処理するために、プロセスの正常な実行フローを中断するために使用されます。
異なるプラットフォームでは信号の定義に違いがある場合があります。各信号は異なる値、動作、および説明に対応しており、Linux システムでは man signal
を使用して対応する信号の説明を確認できます。
ここでは POSIX signals の参考を提供していますので、読者は自分で確認できます。信号は複数の値に対応する場合がありますが、これはこれらの信号値がプラットフォームに依存しているためです。
信号のデフォルト動作では、Term はデフォルト動作がプロセスを終了することを示し、Ign はデフォルト動作がその信号を無視することを示し、Core はデフォルト動作がプロセスを終了し同時にコアダンプを出力することを示し、Stop はデフォルト動作がプロセスを停止することを示します。
最後に、SIGKILL
と SIGSTOP
のこの 2 つの信号は、アプリケーションによって捕捉されることも、オペレーティングシステムによってブロックまたは無視されることもありません。
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 に情報を送信するためにブロックしません(ブロックされた場合、signal パッケージは直接放棄します)。一般的に、単一の信号通知を使用する場合、容量は 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: 優雅な終了")
return
case syscall.SIGHUP:
fmt.Println("\nSIGHUP: ターミナル接続が切断されました")
return
default:
fmt.Println("\n未知の信号!")
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
メソッドはチャネルの信号リッスンをキャンセルします:
- signal.Stop (c) を呼び出すと、チャネル c への信号の転送が停止します
- パラメータ c は送信専用の信号チャネルです
- Stop されたチャネルは再度 Notify メソッドを呼び出してリッスンを再開できます
以下の例は、チャネルが 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
メソッドは内部でマップを維持し、信号タイプをチャネルリストにマッピングします。信号が来ると、信号はすべての対応するチャネルに送信され、削除時にはそのマップから対応するチャネルを削除する必要がありますが、直接削除すると データ競合 が発生する可能性があります。特に、信号処理とチャネル削除が並行して行われる場合です。
この問題を解決するために、以下のことが行われます:
- 停止する信号の一時保存:Stop メソッドが呼び出されると、信号処理メカニズムは即座にマップから削除するのではなく、停止する信号チャネルを一時リスト([] stopping)に保存します。
- 信号送信の完了を待つ:信号処理メカニズムは、すべての進行中の信号送信操作が完了することを保証します。
- 信号チャネルの最終削除:すべての信号発生操作が完了した後、信号処理メカニズムはマップから通信号チャネルを正式に削除し、停止されたチャネルに信号が送信されないことを確保します。