Open1

gsignalの翻訳メモ

gsignalについて

$ go version
go version 1.13

signalパッケージはシグナルハンドラを提供し、Goプログラムの開発者にシグナルの制御をできるようにします。

いきなりsignalパッケージの内部に飛び込む前に、サンプルコードから始めましょう。

シグナルのサブスクリプション

シグナルのサブスクリプションはチャネルを使っておこないます。

次のサンプルコードは、SIGINT, SIGTERMSIGWINCHをサブスクリプションするプログラムです。

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で割り当てられたシグナルをサブスクリプションします。

次の図は、サンプルコードのサブスクリプションのワークフローを示しています。

subscription0

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と呼びます。

このループはシグナルが実際に来るまでスリープします。

gsignal0

そして、シグナルがプログラムに送られてきた時、シグナルハンドラは、シグナルの処理をgsignalという特別なGoroutineに依頼します。

このGoroutineは通常のスタックより大きめの固定長のスタック(32KB)を持っています。 これは違うOSからのシグナルにも対応できるようにこのサイズになっており、決して拡張されません。

このgsignalGoroutineはちゃんとシグナルに対応できるように、各スレッドMと結びつけられています。

gsignal1

gsignalはシグナルが受け入れているシグナルかどうか確認して、そうだとしたらシグナルをキューに送って、眠っていたsignal.loopGoroutineを起こします。

gsignal2

そして、signal.loopはループ処理の中でシグナルを処理します。

このシグナルをサブスクリプションしているチャネルをまず見つけ、次にそのチャネルにシグナルをプッシュします。

gsignal3

go tool traceを使うと、signal.loopGoroutineがどのように動いているか可視化できます。

gsignal4

gsignalGoroutineがロックされたりブロックされていたりすると、シグナルのハンドリングに支障をきたすことがあります。

また、gsignalは固定サイズのためメモリを確保できません。

このため、シグナル処理では、シグナルが到着したらすぐにキューにPUSHするためのgsignalと、そのキュー上でループ処理によってシグナルを処理するsignal.loopの2つにgoroutineを分離させることが重要です。

これらを踏まえてサブスクリプションのワークフローの図を更新しましょう。

gsignal5

References

ログインするとコメントできます