今日はとっても簡単なお題なのだけど、ちゃんと理解していないトピックとして、Go 言語でのシグナルのハンドリングについて書いてみたい。
お題
タイマーと、go routine と、シグナルの割り込みのいづれか一番早く発生した内容表示して、プログラムを終了する。シグナルの場合はシグナルの内容を表示する。
シグナルのハンドリング
次のサンプルがほぼすべてになります。
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
s := <-sig
fmt.Printf("Signal received: %s \n", s.String())
os.Signal
のチャネルを作成します。その後、signal.Notify
で監視すべきシグナルを列挙します。すると、そこに列挙されたシグナルが発生すると、作成したチャネルにメッセージが通知されます。os.Signal
の Struct は Value()
と、String()
を持っていますが、Value()
は戻りが無いので、String()
を使います。
シグナルの種類
理解があいまいなポイントとして、シグナルの理解があります。全部理解するのは大変なので、ざっくりと代表的なものだけでも理解しておきましょう。
シグナル | 解説 | 発生のさせ方 |
---|---|---|
SIGHUP | ターミナルの終了 | terminal を終わらせる, kill -1 process_id |
SIGINT | プロセスの停止 | ctr+c |
SIGQUIT | プロセスを停止させてコアダンプをはく | ctr+\ , kill -3 process_id |
SIGKILL | プロセスの強制終了 | kill -9 process_id |
SIGTERM | デフォルトシグナル。通常はプロセスの停止を表す。 | kill process_id or kill -15 process_id |
SIGHUP は元々は、HANGUP
つまり電話を切ることで、昔はモデルとかで通信していたのでその名残で、現在は、ターミナル殺したときに発生します。
サンプルプログラム
次のような簡単なプログラムで実行してみます。プログラムを停止させる時に、いきなり落とすのではなく、終了処理をしてからGracefulに終わってほしいと思う場面は多いでしょう。そういう場面を想定して書いています。go routine でシグナルを待っていますが、30秒たったら終わるようにしています。Shutdown gracefully
の箇所にそういう処理を入れ込めばいい感じです。
main.go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
fmt.Println("Start process....")
go func() {
trap := make(chan os.Signal, 1)
signal.Notify(trap, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT)
s := <-trap
fmt.Printf("Received shutdown signal %s\n", s)
fmt.Printf("Shutdown gracefully....\n")
os.Exit(0)
}()
fmt.Println("Waiting for the signal....")
time.Sleep(30 * time.Second)
}
実行
$ go build main.go -o signal
$ ./signal > aaa.txt
ここで、ターミナルを閉じる
$ cat aaa.txt
Start process....
Waiting for the signal....
Received shutdown signal hangup
Shutdown gracefully....
同様の結果は、
$ ./signal
別ターミナル
$ ps -ef | grep signal
ushio 29566 31751 0 19:00 pts/13 00:00:00 ./signal
ushio 29635 29030 0 19:00 pts/14 00:00:00 grep --color=auto signal
ushio@DESKTOP-KIUTRHV:~/Code/Project/keda/v2/samples/spike/iteration6$ kill -1 29566
最初のターミナル
$ ./signal
Start process....
Waiting for the signal....
Received shutdown signal hangup
Shutdown gracefully....
go run main.go にご注意
このようなシグナルのプログラムをテストしている時に一つだけ注意事項があります。
$ go run main.go
Start process....
Waiting for the signal....
上記のような形で、プログラムをランして、SIG_TERM
を発生させます。
$ ps -ef | grep go
ushio 186 46 0 Feb05 pts/0 00:03:15 /home/ushio/go/bin/gopls -mode=stdio
ushio 547 32362 0 15:35 pts/0 00:00:11 /home/ushio/go/bin/gopls -mode=stdio
ushio 3932 31751 0 19:15 pts/13 00:00:00 go run main.go
ushio 4042 3932 0 19:15 pts/13 00:00:00 /tmp/go-build777782526/b001/exe/main
ushio 4158 29030 0 19:15 pts/14 00:00:00 grep --color=auto go
ushio 20332 20134 0 15:08 pts/0 00:00:33 /home/ushio/go/bin/gopls -mode=stdio
ushio 25461 25267 0 15:22 pts/0 00:00:20 /home/ushio/go/bin/gopls -mode=stdio
ushio 28569 28174 0 15:28 pts/0 00:00:45 /home/ushio/go/bin/gopls -mode=stdio
ushio@DESKTOP-KIUTRHV:~/Code/Project/keda/v2/samples/spike/iteration6$ kill 3932
あれ、単に終了して、graceful shutdown が走りません。
$ go run main.go
Start process....
Waiting for the signal....
Terminated
理由は簡単で、go run
のコマンドの実体は、go build -o /tmp/something && ./tmp/something
に相当します。上記の ps -ef
の go run main.go
のほかに、/tmp/go-build777782526/b001/exe/main
が動いています。go run main.go
の方は、単なる go のSDKであり、実態の方は、/tmp/..../main
の方なので、そちらの方にシグナルを送らないといけないので、間違えないでください。/tmp/.../main
の方にシグナルを送るとうまくいきます。
$ ps -ef | grep go
ushio 186 46 0 Feb05 pts/0 00:03:16 /home/ushio/go/bin/gopls -mode=stdio
ushio 547 32362 0 15:35 pts/0 00:00:12 /home/ushio/go/bin/gopls -mode=stdio
ushio 6559 31751 1 19:20 pts/13 00:00:00 go run main.go
ushio 6663 6559 0 19:20 pts/13 00:00:00 /tmp/go-build604160077/b001/exe/main
ushio 6741 29030 0 19:20 pts/14 00:00:00 grep --color=auto go
ushio 20332 20134 0 15:08 pts/0 00:00:33 /home/ushio/go/bin/gopls -mode=stdio
ushio 25461 25267 0 15:22 pts/0 00:00:20 /home/ushio/go/bin/gopls -mode=stdio
ushio 28569 28174 0 15:28 pts/0 00:00:46 /home/ushio/go/bin/gopls -mode=stdio
ushio@DESKTOP-KIUTRHV:~/Code/Project/keda/v2/samples/spike/iteration6$ kill 6663
別ターミナル
$ go run main.go
Start process....
Waiting for the signal....
Received shutdown signal terminated
Shutdown gracefully....
お題のプログラミングの解答
こんなものを作ってみました。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
timer := time.NewTimer(20 * time.Second)
finished := make(chan bool)
sigterm := make(chan os.Signal, 1)
signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGTSTP, syscall.SIGKILL)
go func() {
time.Sleep(21 * time.Second)
finished <- true
}()
select {
case <-timer.C:
fmt.Println("Timer wins!")
case signal := <-sigterm:
fmt.Printf("Signal wins! Signal %s\n", signal.String())
case <-finished:
fmt.Println("Go routine wins!")
}
}
バリエーションは多くないので、このあたりのプログラムはこれで大体さっと書ける感じです。シグナルをもっと拾いたいときは、その都度シグナルの意味を調べてみましょう。