Goで学ぶLinux Signal

Goまみれの生活を送っているinouesです。
今回はGoを使ってLinux Signalを学んでいこうと思います💪

対象読者は「Linux Signal全然知らないよ〜」という方から「Linux Signalは知ってるけどGoでの扱い方なんて知らないよ〜」という方までになっておりますので、予めご了承下さい🙇

Linux Signalってなんぞ?

シグナル(英: signal)とは、Unix系(POSIX標準に類似の)OSにおける、限定的なプロセス間通信の一形態。プロセスに対し、非同期で、イベントの発生を伝える機構である。 (Wikipediaより)

ということで、何かプロセスにシグナルというやつを渡すってことはわかりましたね!

それでは実際にどのようにシグナルを渡すことができるのでしょうか?🤔

簡単かつ有名なもので言うと、 Ctrl + C を押下することで実行中のプログラムを止めることができますが、まさしくこれがSIGINTと呼ばれるシグナルを送っていたのです!

他のシグナルも送ってみたい!という方は、killコマンドを使ってやってみるといいかもしれません。
killコマンドという名前で勘違いされるかもしれませんが、実はこのコマンドはシグナルを送るためのコマンドなのです。

kill -l を実行すると、どのようなシグナルを送信できるか一覧が表示されます。
また、 kill -[シグナル番号] [プロセス番号] という形で実行すると、実際に該当するプロセス番号に対してシグナルを送信できます。

試してみたい方は、 Ctrl + C と同じシグナルを送信する kill -2 [プロセス番号] でやってみるといいかもしれません!👺

GoでLinux Signalを扱う

とりあえず作ってみよう

さて、それでは実際にGoでLinux Signalを扱ってみましょう!
基本的に何をするかというと、「実行しているGoプログラムに対して、特定のシグナルが来た時に◯◯をする」ということを実現していきます。

では、とりあえず遊び心を持って「Ctrl + Cをやっても止まらないプログラム」を作ってみましょう!

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    signalCh := make(chan os.Signal)
    signal.Notify(signalCh, syscall.SIGINT)

    for {
        select {
        case <-signalCh:
            fmt.Println("止まらないよ!")
        }
    }
}

さて、これを実際に動かしてみると・・・

できましたね!🙌
これではいつまで経ってもプログラムが止まらないので、 ps ax | grep “go run” で該当するプロセスの番号を確認して、 kill -9 [プロセス番号] で止めましょう。
(※動画ではキャプチャの関係で途中で終了するようになっています)

コード詳解

それでは、コードを詳しく見ていきます。
今回大事な要素は3つです。

  • シグナルを受信するためのchannelを作る
  • どのシグナルをどのchannelを使って受信するか設定する
  • シグナルを受信するまで処理をブロッキングする

シグナルを受信するためのchannelを作る

これはコードの

signalCh := make(chan os.Signal)

でやっています。
受信するシグナルの型は os.Signal になるので、それをchannelとして作成しているだけで簡単ですね。

どのシグナルをどのchannelを使って受信するか設定する

signal.Notify(signalCh, syscall.SIGINT)

でやっていて、今回では signalCh でシグナルを受信し、 syscall.SIGINT というシグナルを受信するよ、という感じの指定になっています。

シグナルを受信するまで処理をブロッキングする

最後に、

for {
    select {
    case <-signalCh:
        fmt.Println("止まらないよ!")
    }
}

で、シグナルを受信するまでブロッキングし、受信後も再度ブロッキングするという感じのコードになっています。

基本的に、シグナルを処理する時はこの3つが必ず出てきますので、頭の片隅に置いておいてください。

発展させてみる

それでは、この例を元に他のシグナルを使って色々と発展させていきましょう!

今回の目的は「Goを使ってLinux Signalを学ぶ」ですので、普段プログラミングをする方でも、あまり馴染みのないSignalを触っていきましょう。

SIGHUP

SIGHUP はHung upを示していて、ザックリ説明すると端末がクローズした時に送信されるシグナルです。

例えば、プログラムが動いている状態なのに、ターミナルを終了した時に、動いていたプログラムに対して送信されます。

それでは、このシグナルを使って「ターミナルを間違って終了したら、その旨をlogファイルに書き込む」というプログラムを書いてみました。

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
)

var (
    logDir  = "log"
    logFile = "log/output.log"
)

func main() {
    signalCh := make(chan os.Signal)
    signal.Notify(signalCh, syscall.SIGHUP)

    select {
    case <-signalCh:
        logging()
        os.Exit(1)
    }
}

func logging() {
    // ログディレクトリの存在チェック
    _, err := os.Stat(logDir)
    if err != nil {
        err = os.Mkdir(logDir, 0777)
        if err != nil {
            log.Fatal(err)
        }
    }

    // ログファイルの存在チェック
    _, err = os.Stat(logFile)
    if err != nil {
        _, err = os.Create(logFile)
        if err != nil {
            log.Fatal(err)
        }
    }

    // ログファイルに書き込み
    f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    _, err = f.WriteString("Terminal hung up!\n")
    if err != nil {
        log.Fatal(err)
    }
}

3つ要素で最初の例と違うのは、受信ブロッキングのところでしょうか。
今回は端末を落とすことを想定しているので、一度SIGHUPを受け取ったらそのままプログラムを終了させるために、forを敢えて使っていません。

あと、端末を落とした後にプログラムを終了させる手段が、プロセス番号に向けてkillするしかなくなるので、SIGHUPを受け取ったと同時にプログラムを終了させるよう os.Exit(1) を実行しています。

これを実行させると、こんな感じに・・・

ちょっと速くて見にくいですが書き込まれましたね!✨

SIGXCPU

最後に SIGXCPU を扱ってみます。

これはプロセスの実行時間を監視して、設定した時間を超えると送られるシグナルになります。

では、これを使って「設定した実行時間を過ぎると、終了メッセージを出力してから終了する」というのを作っていきます🙌

package main

import (
    "fmt"
    "syscall"

    "os"
    "os/signal"
)

func main() {
    rlimit := &syscall.Rlimit{
        Cur: 5,
        Max: 5,
    }
    syscall.Setrlimit(syscall.RLIMIT_CPU, rlimit)

    signalCh := make(chan os.Signal)
    signal.Notify(signalCh, syscall.SIGXCPU)

    go func(signalCh chan os.Signal) {
        select {
        case <-signalCh:
            fmt.Println("Finish!")
            os.Exit(0)
        }
    }(signalCh)

    for {
        // noop
    }
}

結構元の形から変わっちゃいましたね🤔
一番変わっているのが受信ブロッキングしているところですが、これはSIGXCPUの仕様のためです。

SIGXCPUはスリープやスケジューリングの待ち時間を実行時間として見做さないので、受信ブロッキングの部分をgoroutineにして、メインスレッドはforループを回して、さも動いているように見せかけています。

これで実行すると・・・

こんな感じになりました!

まとめ

如何でしたでしょうか?
このようにLinux Signalを理解して、Goを書けると何か面白いものを作れるような気がしませんか?😃

かく言う私も、Linux Signalを簡単に扱えるものが欲しいな〜と思って、こういうライブラリを作ったりしました。

みなさんも是非何か作ってみてください🙇

皆でJPYマイニングしていこうな!