gsignalの翻訳メモ
gsignalについて
$ go version
go version 1.13
signal
パッケージはシグナルハンドラを提供し、Goプログラムの開発者にシグナルの制御をできるようにします。
いきなりsignal
パッケージの内部に飛び込む前に、サンプルコードから始めましょう。
シグナルのサブスクリプション
シグナルのサブスクリプションはチャネルを使っておこないます。
次のサンプルコードは、SIGINT
, SIGTERM
かSIGWINCH
をサブスクリプションするプログラムです。
func main() {
done := make(chan bool, 1)
s1 := make(chan os.Signal, 1)
signal.Notify(s1, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-s1
fmt.Println(`/!\ The program is going to exit...`)
done <- true
}()
s2 := make(chan os.Signal, 1)
signal.Notify(s2, syscall.SIGWINCH)
go func() {
for {
<-s2
fmt.Println(`/!\ The terminal has been resized.`)
}
}()
<-done
}
各os.Signal
チャネルはsignal.Notify
で割り当てられたシグナルをサブスクリプションします。
次の図は、サンプルコードのサブスクリプションのワークフローを示しています。
GoはシグナルのサブスクリプションをやめるAPIsignal.Stop(os.Signal)
やシグナルを無視するAPIsignal.Ignore(...os.Signal)
を提供してくれています。
func main() {
done := make(chan bool, 1)
s1 := make(chan os.Signal, 1)
signal.Notify(s1, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-s1
fmt.Println(`/!\ The program is going to exit...`)
signal.Stop(s1)
// シグナルのサブスクリプションは終了したのでここは永遠にブロックされたまま
<-s1
done <- true
}()
<-done
}
このプログラムはCTRL+C
で中断することはできないので、決して停止しません。
それでは、入ってきた信号を処理するリスナーフェーズとプロセスフェーズがどのようになっているかを見てみましょう。
gsignal
初期化フェーズの間、signal
パッケージは、ループ処理をし続けシグナルを受け取る役割を持つGoroutineを生成します。この例ではsignal.loop
と呼びます。
このループはシグナルが実際に来るまでスリープします。
そして、シグナルがプログラムに送られてきた時、シグナルハンドラは、シグナルの処理をgsignal
という特別なGoroutineに依頼します。
このGoroutineは通常のスタックより大きめの固定長のスタック(32KB)を持っています。 これは違うOSからのシグナルにも対応できるようにこのサイズになっており、決して拡張されません。
このgsignal
Goroutineはちゃんとシグナルに対応できるように、各スレッドM
と結びつけられています。
gsignal
はシグナルが受け入れているシグナルかどうか確認して、そうだとしたらシグナルをキューに送って、眠っていたsignal.loop
Goroutineを起こします。
そして、signal.loop
はループ処理の中でシグナルを処理します。
このシグナルをサブスクリプションしているチャネルをまず見つけ、次にそのチャネルにシグナルをプッシュします。
go tool trace
を使うと、signal.loop
Goroutineがどのように動いているか可視化できます。
gsignal
Goroutineがロックされたりブロックされていたりすると、シグナルのハンドリングに支障をきたすことがあります。
また、gsignal
は固定サイズのためメモリを確保できません。
このため、シグナル処理では、シグナルが到着したらすぐにキューにPUSHするためのgsignal
と、そのキュー上でループ処理によってシグナルを処理するsignal.loop
の2つにgoroutineを分離させることが重要です。
これらを踏まえてサブスクリプションのワークフローの図を更新しましょう。