Go とシグナルの検出


今日はとっても簡単なお題なのだけど、ちゃんと理解していないトピックとして、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 -efgo 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!")
    }

}

バリエーションは多くないので、このあたりのプログラムはこれで大体さっと書ける感じです。シグナルをもっと拾いたいときは、その都度シグナルの意味を調べてみましょう。