1. 信号とは#
信号(Signal) は OS におけるプロセス間通信の方法の一つであり、Linux システムにおいては、信号はソフトウェア割り込みであり、プロセスに特定のイベントが発生したことを通知するために使用されます。通常、特定のイベントや異常な状況を処理するために、プロセスの正常な実行フローを中断するために使用されます。
異なるプラットフォームでは信号の定義に違いがある場合があります。各信号は異なる値、動作、および説明に対応しています。Linux システムでは、man signal
を使用して対応する信号の説明を確認できます。
ここでは POSIX signals の参考を提供していますので、読者は自由に確認できます。1 つの信号は複数の値に対応する場合がありますが、これはこれらの信号値がプラットフォームに関連しているためです。
信号のデフォルト動作では、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)に保存し、即座にマップから削除しません。
- 信号送信が完了するのを待つ:信号処理メカニズムは、すべての進行中の信号送信操作が完了することを保証します。
- 信号チャネルを最終的に削除:すべての信号発生操作が完了した後、信号処理メカニズムはマップから通信号チャネルを正式に削除し、停止したチャネルに信号が送信されないことを保証します。