LoginSignup
49
45

More than 5 years have passed since last update.

Goでデーモンを作るにはどうするのが良い?

Last updated at Posted at 2015-12-27

少し前にAdvent Calendarのネタswiftfsと言うツールを作ったのですが、Goでデーモンプログラムをどう書けば良いのかよくわからなかった。以下の記事を参考にしたけど、そもそもfork()しちゃいかんとなると、なかなか難しいですね。

Goでデーモンを実装する

で、swiftfsは以下のような実装にしました。syscall使うとWindowsとかで動かなくなってしまうとか、そもそも長いとか、あまり良い実装ではないように思います。Go的にもっとい良い方法があったら教えて下さい。コード全文は末尾とGistにあります。

親プロセス、子プロセス(デーモン本体)共通

func main() {
    var child *bool = flag.Bool("child", false, "Run as a child process")
    flag.Parse()

fork()できないので、親プロセス、子プロセスの判別をflagで行います。コマンドに--childが付いていれば子プロセスと判断。

親プロセス

    // 子プロセスとのパイプを作っておく
    r, w, err := os.Pipe()
    if err != nil {
        return err
    }

    cmd := exec.Command(os.Args[0], args...)
    cmd.ExtraFiles = []*os.File{w}
    cmd.Stderr = os.Stderr
    cmd.Stdout = os.Stdout

パイプを使って親プロセスと子プロセスの間でプロセス間通信をします。cmd.ExtraFilesに適当なos.File(この場合はw)を渡しておくと、実行したプロセス側のファイルディスクリプタに割り当てられます。割り当てられるのは3番からです。

子プロセスの起動完了を親側で待ったり、初期化処理でエラーになったことを親側で知るためにこうしました。親側では適切な終了コードとともにプログラムを終了できます。そして

    if err = cmd.Start(); err != nil {
        return err
    }

cmd.Start()で子プロセスを起動します。argsに--childをセットしてあるので、起動されたプロセスはchildMain()が実行されます。


    // パイプから子プロセスの起動状態を取得する
    var status int = DAEMON_START
    go func() {
        buf := make([]byte, 1)
        r.Read(buf)

        if int(buf[0]) == DAEMON_SUCCESS {
            status = int(buf[0])
        } else {
            status = DAEMON_FAIL
        }
    }()

    // 子プロセスの起動を30秒待つ
    i := 0
    for i < 60 {
        if status != DAEMON_START {
            break
        }
        time.Sleep(500 * time.Millisecond)
        i++
    }

    // 親プロセス終了
    if status == DAEMON_SUCCESS {
        return nil
    } else {
        return fmt.Errorf("Child failed to start")
    }

パイプの読み込み側(変数r)には子プロセス側から何かしら状態が返ってきます。それを待つgoroutineを作り、ステータスの変更を待ちます。もし子プロセスがシグナルなどを受け取って異常終了しても、タイムアウトして適切に(エラーで)終了できるようにしてます。

子プロセス


func childMain() {
    // 初期化処理があればここに
    var err error

    // 子プロセスの起動状態を親プロセスに通知する
    pipe := os.NewFile(uintptr(3), "pipe")
    if pipe != nil {
        defer pipe.Close()
        if err == nil {
            pipe.Write([]byte{DAEMON_SUCCESS})
        } else {
            pipe.Write([]byte{DAEMON_FAIL})
        }
    }

これはサンプルなので初期化処理は省いています。初期化が終わった後、子プロセス側はファイルディスクリプタ3番に自身の起動状態を書き込みます。ここは単純にint型で状態を表しています。

コード全文

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

const (
    DAEMON_START = 1 + iota
    DAEMON_SUCCESS
    DAEMON_FAIL
)

func main() {
    var child *bool = flag.Bool("child", false, "Run as a child process")
    flag.Parse()

    if !*child {
        // parent
        if err := parentMain(); err != nil {
            log.Fatalf("Error occurred [%v]", err)
            os.Exit(1)
        } else {
            os.Exit(0)
        }

    } else {
        // child
        childMain()
    }
}

func parentMain() (err error) {
    args := []string{"--child"}
    args = append(args, os.Args[1:]...)

    // 子プロセスとのパイプを作っておく
    r, w, err := os.Pipe()
    if err != nil {
        return err
    }

    cmd := exec.Command(os.Args[0], args...)
    cmd.ExtraFiles = []*os.File{w}
    cmd.Stderr = os.Stderr
    cmd.Stdout = os.Stdout
    if err = cmd.Start(); err != nil {
        return err
    }

    // パイプから子プロセスの起動状態を取得する
    var status int = DAEMON_START
    go func() {
        buf := make([]byte, 1)
        r.Read(buf)

        if int(buf[0]) == DAEMON_SUCCESS {
            status = int(buf[0])
        } else {
            status = DAEMON_FAIL
        }
    }()

    // 子プロセスの起動を30秒待つ
    i := 0
    for i < 60 {
        if status != DAEMON_START {
            break
        }
        time.Sleep(500 * time.Millisecond)
        i++
    }

    // 親プロセス終了
    if status == DAEMON_SUCCESS {
        return nil
    } else {
        return fmt.Errorf("Child failed to start")
    }
}

func childMain() {
    // 初期化処理があればここに
    var err error

    // 子プロセスの起動状態を親プロセスに通知する
    pipe := os.NewFile(uintptr(3), "pipe")
    if pipe != nil {
        defer pipe.Close()
        if err == nil {
            pipe.Write([]byte{DAEMON_SUCCESS})
        } else {
            pipe.Write([]byte{DAEMON_FAIL})
        }
    }

    // SIGCHILDを無視する
    signal.Ignore(syscall.SIGCHLD)

    // STDOUT, STDIN, STDERRをクローズ
    syscall.Close(0)
    syscall.Close(1)
    syscall.Close(2)

    // プロセスグループリーダーになる
    syscall.Setsid()

    // Umaskをクリア
    syscall.Umask(022)

    // / にchdirする
    syscall.Chdir("/")

    // main loop
    for {
        time.Sleep(1000 * time.Millisecond)
    }
}
49
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
45