銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

ラズパイで自動起動するデーモンを自作する

f:id:takamints:20150607205740p:plain

Raspberry Piで動作するデーモンをC言語で作る方法と、自動起動する設定手順などをまとめました。



はじめに

以下の順に説明します。C言語そのものや、コンパイルとかリンクに関する説明はしていません。

  1. デーモン本体の作成
  2. デーモン起動/停止スクリプトの作成
  3. 実行モジュール・スクリプトの配置と自動起動の設定
  4. 動作の確認
  5. まとめと反省・所感や雑感

説明中で使用しているコードは、Gist「C言語でラズパイのデーモンを作るときの補助関数とスケルトン · GitHub」に置いていますので、実際に動作させる場合はそちらを参考になさってください。


RASPBERRY DREAM
RASPBERRY DREAM
posted with amazlet at 15.06.08
avex trax (2012-09-07)
売り上げランキング: 52,713

1. デーモン本体の作成

デーモンとして動作するプログラムを作成するには、以下の4つの機能仕様を実装します。 2と3は必須条件ではないですがいろんな理由でやっておいたほうがいいと思います。

  1. デーモンプロセスを生成する
  2. ログはsyslogで出力する
  3. プロセスIDをファイルへ保存する
  4. SIGTERMで終了する

1~3については、Gist の daeomonize.cdaemonizeという関数にまとめています。デーモンのmainは同Gistのmydaemon.cを参考にしてください。

1-1. デーモンプロセスを生成する

デーモンは親プロセスを持たないプロセスです。プログラム起動後、子プロセスを生成してから終了することで、子プロセスがデーモンとして動き続けます。 最近のLinuxには、デーモンを生成するための daemon() という関数が用意されており、簡単に生成できるようになっています。

daemon()を呼び出したプロセスは、forkしてから終了します。ですからdaemon() から戻るのは子プロセスだけ。この時点で既にデーモンが生成されて実行している状態になっているんですね。

(Man page of DAEMON より抜粋)
名前
daemon - バックグラウンドで動作させる
書式
#include <unistd.h>
int daemon(int nochdir, int noclose);
説明
nochdir が 0 の場合、 daemon() は呼び出したプロセスの現在の作業ディレクトリをルートディレクトリ (“/”) に変更する。 それ以外の場合、現在の作業ディレクトリは変更されない。
noclose が 0 の場合、 daemon() は標準入力・標準出力・標準エラーを /dev/null にリダイレクトする。 それ以外の場合、これらのファイルディスクリプターは変更されない。
返り値
成功した場合、 daemon() は 0 を返す。 エラーが起こった場合、 daemon() は -1 を返す。

1-2. ログはsyslogで出力する

デーモンの標準入出力ファイルはおそらく閉じられていますので、コンソールへの表示はできません。 不具合が生じた場合に問題特定のため、以下のようにして、syslogでログを出力する準備をしておきます。

#include <syslog.h>

/* /var/log/daemon.log へ プロセスID込みでログ出力 */
openlog("daemon", LOG_PID, LOG_DAEMON));

1-3. プロセスIDをファイルへ保存する

上述の daemon 呼び出し以後に、生成されたデーモンのプロセスID(pid)をファイルに保存します。 これはデーモンの起動/停止スクリプトから利用されます。なくても何とかなりますが、停止やシャットダウン時に必要以上に時間がかかるようになってしまいます。

ファイルは慣習的に /var/run/<デーモン名>.pid とします。つまり、デーモンの実行モジュールの名前が、mydaemon なら /var/run/mydaemon.pid へそのpidが書き込まれます。

FILE* pidfile = fopen("/var/run/mydaemon.pid", "w+");
if (pidfile) {
  int pid = getpid();
  fprintf(pidfile, "%d\n", pid);
  fclose(pidfile);
} else {
  syslog(LOG_ERR,
    "daemonize() : failed to write pid.\n");
}

1-4. SIGTERMで終了する

前項までの処理が行えているなら、あとはデーモン本来の処理を行うのみです。 一般的には、SIGTERMを受け取るまでのループになると思います。

たとえば以下のような実装です。

#include <unistd.h>
#include <signal.h>
#include <syslog.h>

volatile int sigterm = 0;
void handle_sigterm(int sig) { sigterm = 1; }
int mydaemon( void ) {
  syslog(LOG_INFO, "mydaemon started.\n");
  signal(SIGTERM, handle_sigterm);
  while(!sigterm) {
    /* こんな感じ */
    usleep(1000000);
  }
  syslog(LOG_INFO, "mydaemon stopped.\n");
  return 0;
}

このほか、慣習として、SIGHUP(ハングアップ)を受け取ったら設定ファイルなどを読み直して再実行するようにするらしいです。

2. デーモン起動/停止スクリプトの作成

デーモン起動/停止スクリプトは、スケルトン(/etc/init.d/skeleton)をコピーして書き換えます。

pi@raspberrypi~ $ cp /etc/init.d/skeleton /etc/init.d/mydaemon

書き換え前:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          skeleton
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# Author: Foo Bar <foobar@baz.org>
#
# Please remove the "Author" lines above and replace them
# with your own name if you copy and modify this script.

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Description of the service"
NAME=daemonexecutablename
DAEMON=/usr/sbin/$NAME
DAEMON_ARGS="--options args"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

まず書き換えるのは、3行目あたりのコメントで# Provides: skeleton となっているところで、デーモンの名前mydaemonに書き換えます。サービスをあらわす一意な名前として扱われるようで、他のサービスとかぶっているといけないようです。(2015-06-08 追記)

次に書き換えるのが21行目あたりの NAME=daemonexecutablenameとなっているところ。これも自作デーモンの名前 mydaemon に書き換えます。

DAEMONはデーモン本体のパスです。/usr/sbin/$NAME となっていますが、前項では実行モジュールを /usr/local/sbin へ配置しましたので、/usr/local/sbin/$NAME に書き換えます。

次のDAEMON_ARGSは起動時にデーモンへ渡されるコマンドライン引数です。不要なら空にしておいたほうが気持ちよいです。

後は確認です。PIDFILEは、Cのソースに記述したpidを保存するファイルと同じであること、SCRIPTNAMEが、この起動スクリプトのファイル名と一致することを確認します。

書き換え後:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          mydaemon
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# Author: Foo Bar <foobar@baz.org>
#
# Please remove the "Author" lines above and replace them
# with your own name if you copy and modify this script.

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh scriptPATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Description of the service"
NAME=mydaemon
DAEMON=/usr/local/sbin/$NAME
DAEMON_ARGS=""
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

※ ここで PIDFILE のファイル名の設定があるから、てっきりプロセスIDを保存してくれるのだろうと思いこんでしまったです。スクリプトではチェックするだけなんですね。

3. 実行モジュール・スクリプトの配置と自動起動の設定

配置とパーミッション

実行モジュール(デーモン本体)は /usr/local/sbin へ、デーモン起動スクリプトは、/etc/init.d へ配置し、どちらも、所有者、グループ共にrootとして、実行属性を与えます。

自動起動の設定

この時点でデーモンは動作するはずですが、システム起動時に自動起動するには、update-rc.d コマンドで設定しなくてはなりません。(どうやら最近のトレンド(?)としては、insserv コマンドでやるほうが良いらしいですが、何がどう違うのか知りません)

以下は、これらの処理を一括して行うスクリプトの例です。それぞれ個別にやってもかまいませんし、本来はmakeやcmakeでやるべきなんでしょうね。あとinstallコマンドとか使うほうがスマートかと思います。が、まあとりあえず。

#!/bin/sh
NAME=mydaemon
DSCR=/etc/init.d/${NAME}
DEXE=/usr/local/sbin/${NAME}

sudo ${DSCR} stop

sudo cp ./${NAME}.sh ${DSCR}
sudo chown root.root ${DSCR}
sudo chmod 755 ${DSCR}

sudo cp ${NAME}.out ${DEXE}
sudo chown root.root ${DEXE}

sudo update-rc.d ${NAME} defaults

sudo ${DSCR} start

4. 動作確認

以上で、ラズパイを再起動すれば自作デーモンが立ち上がるはずですが、その前に確認してみましょう。

pi@raspberry ~ $ /etc/init.d/mydaemon start # 起動

pi@raspberry ~ $ /etc/init.d/mydaemon stop  # 停止

正しく起動したなら、/var/run/mydaemon.pid にそのプロセスidが保存されているはずです。catやpsで確認してみましょう。

停止に関して、数十秒処理が戻らない場合は、プロセスidが正しく記録されていない、または起動していない可能性があります(処理内容にもよりますが)。

いずれにせよ、/var/log/daemon.log にログが出ているはずですので、そちらを確認してみましょう。

問題なく動いているようなら、sudo shutdown -r now で再起動!

自動起動していると思います。

5. まとめと反省・所感や雑感

ラズパイ買って当初はGPIOで「Lチカ(LEDチカチカさせる)」やって喜んだのですが、その後大して何にもやってなかったんですよね。今回、会社の有志の勉強会でちょっくら使うことになりまして、調べながらがんばってみました。

なので、断定的に書いていますが、それほど詳しいわけではありませんので、変なところや非常識なところがあればコメントください。

実際作ってみるまで、デーモンって、WindowsのサービスやMS-DOSの常駐プログラム(懐かしw)の感覚もあって、かなり難しそうに思っていましたが、意外に簡単でした。

他のLinuxディストリビューションでも同じ方法でOKなように思いますが、とりあえず未確認なのでラズパイ限定としておきます。

さて、これでやっと、シャットダウンスイッチを作れます。

で、作りました↓

takamints.hatenablog.jp