48
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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)
	}
}
48
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
48
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?