LoginSignup
104
86

Linux の任意のスクリプトをサービス登録し OS 起動時に自動起動させる [init.d/SystemD 編]

Last updated at Posted at 2019-02-26

この記事は、前回の記事のフォローアップ1 記事です。

「お前もデーモン登録してやろうか」〜俺様スクリプトのデーモン化〜

この記事では、init.d もしくは SystemD に「自作スクリプト(アプリ)を登録し、サービスとして自動起動する」までの手順を記載しています。

前回init.d の登録の仕方の記事を読んで、SystemD でも自作スクリプトやアプリをデーモナイズ(デーモン化/サービス化)したいと思われたら参考ください。また、念のため init.d の登録方法についても言及しているため、若干前回と重複します。

🐒   本記事は Dasher の著者から許可をいただいたので、Wiki にある「Running Dasher on a Raspberry Pi at startup」を翻訳する予定でした。ところが、ちんたらしてたら Dasher の開発自体が終了してしまいました。そのため Dasher に特化しない汎用的な記事として新たに書き起こしましたが、本記事は同じ MIT ライセンスとします。また、追記/誤記/誤字などありましたら遠慮なく編集リクエストください。

init.d と SystemD の違い

init.d(もしくは SystemV init)の方が古く、SystemD の方が新しい仕組みです。共通することを先に説明します。

共通点

基本的に init.dinit プログラム、SystemDsystemd プログラムは、どちらも OS が最初に起動するプログラムです。

そして、どちらも「デーモン」として常駐し、サービス(常駐アプリ)となるプログラムやスクリプトの「起動」「終了」「再起動」などを管理したり、他のプロセスを監視する点でも同じです。親となるプロセスの総本山とも言えるものです。

そのため、このプログラム自体が起動できないとカーネル・パニックを起こします

この全ての親ブロセスを管理するプログラムを INIT システムと総称したりもします。そのため init.dinit と間違えやすいので、明確にするため init.dSystemV init とも呼んだりもします。読み違えやすいので注意します。

また、どちらもサービスごとに「設定ファイル」を用意しておき、専用のディレクトリに設置します。その後、それらをサービス登録することでサービスとしての利用が可能になります。

「サービス登録」と言っても、登録コマンドを叩くと設置したファイルのシンボリック・リンクがサービス登録用のディレクトリに作られるだけです。そして init.d もしくは systemd は起動時に、そのシンボリックリンクを起動して行きます。

また、「サービスとしての利用」と言っても、OS 起動後の自動起動や、startstop コマンドなどによる管理ができると言うだけです。

「デーモン」と「サービス」、どちらも同じものです。一度起動すると終了シグナルを受け取るまで、何かしらのサービスを提供しつづけるプロセスです。深く悩まずに、Linux や macOS など UNIX 寄りの表現がデーモン、Windows 寄りの表現がサービス程度の認識でいいと思います。Linux の記事なので、正しくは「デーモン」で統一すべきなのですが、恐らくこの記事が必要なユーザーは Windows や macOS ユーザーが多いと思うので、把握しやすいように「サービス」という言い方をしています。ここで言う「サービス」は、いわゆる「ただの常駐アプリ」ではなく、終了・再起動といった「シグナルも受け取ることを前提とした常駐アプリ」を指します。

initd と systemd の違い

init.dSystemD の一番の違いは、サービスごとに必要な「設定ファイル」の中身です。

init.d の本体となる init プログラム自体は、以下のシンプルな動作しか基本的にできません。

  • OS 起動時に、登録されたサービスの設定ファイルを start の引数を付けて実行する。
  • OS シャットダウン時にはサービスの設定ファイルを stop の引数を付けて実行する。

ここで注目して欲しいのが「実行する」の部分です。

設定ファイルなのに「実行する」と言うのも、実は init.d のサービス設定ファイルは、設定とはいえど「実質的にシェルのスクリプト」です

init.d の設定ファイルの内容を見るとわかりますが、起動・終了などの処理が、普通にシェル・スクリプトで start stop の引数によって俺様アプリの起動と kill を実行していることが確認できます。

例)cron サービスの設定ファイルの場合

以下は RaspberryPi OS の cron サービスの設定ファイルです。

ここでは詳しく見る必要はなく、スクリプト末尾にある case "$1" in のセクションに注目してください。スクリプトの第 1 引数($1)によって実行内容を変えているのがわかります。

/etc/init.d/cron
#!/bin/sh
# Start/stop the cron daemon.
#
### BEGIN INIT INFO
# Provides:          cron
# Required-Start:    $remote_fs $syslog $time
# Required-Stop:     $remote_fs $syslog $time
# Should-Start:      $network $named slapd autofs ypbind nscd nslcd winbind sssd
# Should-Stop:       $network $named slapd autofs ypbind nscd nslcd winbind sssd
# Default-Start:     2 3 4 5
# Default-Stop:
# Short-Description: Regular background program processing daemon
# Description:       cron is a standard UNIX program that runs user-specified 
#                    programs at periodic scheduled times. vixie cron adds a 
#                    number of features to the basic UNIX cron, including better
#                    security and more powerful configuration options.
### END INIT INFO

PATH=/bin:/usr/bin:/sbin:/usr/sbin
DESC="cron daemon"
NAME=cron
DAEMON=/usr/sbin/cron
PIDFILE=/var/run/crond.pid
SCRIPTNAME=/etc/init.d/"$NAME"

test -f $DAEMON || exit 0

. /lib/lsb/init-functions

[ -r /etc/default/cron ] && . /etc/default/cron

# Read the system's locale and set cron's locale. This is only used for
# setting the charset of mails generated by cron. To provide locale
# information to tasks running under cron, see /etc/pam.d/cron.
#
# We read /etc/environment, but warn about locale information in
# there because it should be in /etc/default/locale.
parse_environment () 
{
    for ENV_FILE in /etc/environment /etc/default/locale; do
        [ -r "$ENV_FILE" ] || continue
        [ -s "$ENV_FILE" ] || continue

         for var in LANG LANGUAGE LC_ALL LC_CTYPE; do
             value=`egrep "^${var}=" "$ENV_FILE" | tail -n1 | cut -d= -f2`
             [ -n "$value" ] && eval export $var=$value

             if [ -n "$value" ] && [ "$ENV_FILE" = /etc/environment ]; then
                 log_warning_msg "/etc/environment has been deprecated for locale information; use /etc/default/locale for $var=$value instead"
             fi
         done
     done

# Get the timezone set.
    if [ -z "$TZ" -a -e /etc/timezone ]; then
        TZ=`cat /etc/timezone` 
    fi
}

# Parse the system's environment
if [ "$READ_ENV" = "yes" ] ; then
    parse_environment
fi


case "$1" in
start)	log_daemon_msg "Starting periodic command scheduler" "cron"
        start_daemon -p $PIDFILE $DAEMON $EXTRA_OPTS
        log_end_msg $?
	;;
stop)	log_daemon_msg "Stopping periodic command scheduler" "cron"
        killproc -p $PIDFILE $DAEMON
        RETVAL=$?
        [ $RETVAL -eq 0 ] && [ -e "$PIDFILE" ] && rm -f $PIDFILE
        log_end_msg $RETVAL
        ;;
restart) log_daemon_msg "Restarting periodic command scheduler" "cron" 
        $0 stop
        $0 start
        ;;
reload|force-reload) log_daemon_msg "Reloading configuration files for periodic command scheduler" "cron"
	# cron reloads automatically
        log_end_msg 0
        ;;
status)
        status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
        ;;
*)	log_action_msg "Usage: /etc/init.d/cron {start|stop|status|restart|reload|force-reload}"
        exit 2
        ;;
esac
exit 0

このように、init.d の設定ファイルはスクリプトであるため、挙動を柔軟に制御できる反面、スクリプトに問題があっても気付きにくいなど、いささか複雑です。しかも、下手するとカーネル・パニックを起こします。

対して、SystemD の設定ファイルの場合は、設定のみが記載されたファイルです。

起動(start)・終了(stop)などの処理は、本体である systemd に組み込まれているため、設定ファイルはサービスの起動に必要な情報だけで済みます。

つまり、SystemD のサービス設定ファイルは、本当の意味で「設定だけ・・のファイル」です。スクリプトよりは自由度は低いものの、記載がシンプルなぶん、安定した柔軟性があるという違いがあります。

例)sshd サービスの設定ファイルの場合

以下は RaspberryPi OS の sshd サービスの設定ファイルです。

ここでは何をしているかよりも、パラメーターしか設定していないことに注目です。

/etc/systemd/system/sshd.service
[Unit]
Description=OpenBSD Secure Shell server
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify
RuntimeDirectory=sshd
RuntimeDirectoryMode=0755

[Install]
WantedBy=multi-user.target
Alias=sshd.service

実は、SystemD はプロセス管理以上の処理(プロセス間通信、ログインやネットワークの起動時処理など)も担えます。そのため、ユーザ・メリットが多くデフォルトの INIT(最上位プロセス)システムとして SystemD を採用しているディストロは多いです。
しかし init.dSystemV init)に比べて SystemD は多機能な反面、依存も多いので「1 つのことを行い、またそれをうまくやるプログラム」という Unix 哲学に反するとして init.d 系(SystemV init 系)を好むディストロも多くあります。
特に、使い勝手(多機能性や利便性)より、堅牢性を重視したディストロ(例えば Alpine Linux など)では OpenRC という、シンプル・軽量かつ堅牢な INIT を採用していたりします。そのぶん SystemD が担っていた処理を手前で行う必要があるため、より Linux の知識を必要とします。
とは言え、この違いは「良い」「悪い」ではなくポリシーの問題です。
かく言う Alpine Linux も、BusyBox という cdls などの本来個別である基本コマンドを 1 つのプログラムで動かせるようにしています。同様に「Unix 哲学うんぬん」という人も多いです。
結局のところ、自分の好みにあったディストロが、どの INIT システムを採用しているかを把握して、まずは使ってみるのがベストだと思います。

init.d or SystemD

どちらのデーモンがメインに利用されているか確認したい場合、1番目のプロセス ID を確認するのが簡単です。

PID1をPSコマンドで確認
$ ps auxq 1
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME  COMMAND
root         1  0.0  0.6  28208  6044 ?        Ss    3月31  31:13 /sbin/init

上記の場合、/sbin/init が PID 1 で実行されているため一般的には init.d が使われていることになります。

しかし、OS によって注意する必要があります。RaspberryPi OS(旧 Raspbian)などの Debian/Ubuntu 系の一部の OS では両方が使われていることがあるからです。

RaspberryPi OS で具体的に確認してみてみましょう。

まずは、先の ps auxq 1 コマンドで確認した 1 番プロセス(PID 1)の実行プログラム init の確認です。

init の本体のパスである /sbin/init のファイルの詳細を ls -l で見てみます。

シンボリック・リンクで、本体は別であることに注目です。

$ # PID1をPSコマンドで確認
$ ps auxq 1
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME  COMMAND
root         1  0.0  0.6  28208  6044 ?        Ss    3月31  31:13 /sbin/init

$ # init は systemd のシンボリック・リンクになっている
$ ls -l /sbin/init
lrwxrwxrwx 1 root root 20  7月 22  2019 /sbin/init -> /lib/systemd/systemd

このように、/sbin/init の実体は /lib/systemd/systemd なのがわかります。

つまり「init と言いながら systemd が動いている」と言うことです。この場合、OS は init.d を呼び出す(実行する)も実質的に systemd に処理を任せることになります。

この init の皮をかぶった systemd ですが、まず init.d 向けのサービスを替わりに起動します。次に、SystemD 向けのサービスを起動することになります。そのため、このような systemdinit を兼任して両方が使える OS があるので注意します。

逆に言えば、この仕組み(init.d systemd 併用)の場合は、どちらのサービス設定ファイルを設置しても動くと言うことです。

どちらも使える OS の場合に、どちらの設定ファイルのサービスにしようか悩んだら、「起動するだけで十分」なら SystemD、「起動前後にゴニョゴニョと処理をしたい」なら init.d が良いと思います。


俺様アプリの用意

init.d にせよ SystemD にせよ、まずは対象となる俺様アプリ(実行ファイルもしくはスクリプト)を事前に用意する必要があります。

🐒   本記事は「スクリプトをサービス(デーモン)として常駐させる」ことを目的としています。そのため、起動時に1回だけ実行させたいのであれば crond にスクリプトを登録crontab@reboot /path/to/myScript.py などと記載)したほうが楽だと思います。一回きりの起動であれば、Raspbian/Debian ですが下記記事が参考になります。

他のサービス同様に「start」や「stop」(/etc/init.d/myService start/etc/init.d/myService stop)などで起動・終了できるようにしたい場合は、本記事を参考にしてください。

俺様サービス作成時の注意事項

俺様サービスを Linux で作りたい場合、プログラム言語は問いません。つまり Bash でも PHP でも Go でも Python でも俺様サービスを作ることは可能です。

しかし、最低限以下の点を網羅する必要があります。

  1. コマンドラインから呼び出し/実行できる状態であること。
  2. プログラムは、呼び出されると中断シグナルを受け取るまで常駐(実行)すること。
  3. メモリの割り当てに注意すること。

まずは、1 の「呼び出し/実行できる状態」とは、ターミナルやコマンドラインから叩けば無限ループで実行される状態ということです。

$ python /path/to/myScript.py $ php /path/to/myScript.php $ ./path/to/myGoApp などです。shebang2 を付けて $ /path/to/myScript.rb などと直接起動できるタイプでもかまいません。、コマンドを叩くと起動できるようになっているということです。

そして 2 番目の「呼び出されると常駐する」とは、実行されたら処理を終了させないようにループなどで継続させる必要があるということです。Web サービスのアプリなどがわかりやすいでしょうか。

最後の「メモリの割り当てに注意」とは、使用メモリが無駄に肥大化しないように注意して設計する、ということです。プログラムの組み方やプログラム言語にも依存するのですが。

例えば、ループ内でメモリを新規確保し続けないなどです。メモリがどんどん肥大化していくからです。

また、カウンターなど永遠とカウントアップさせていて、気付いたら(変数が)とんでもない数値になっていた、みたいなことにならないように注意します。

注意点

プロセスを終了させないことで常駐させるには、基本的に while(true){} などの無限ループでスクリプトが終了しないようにすると思います。

この時、注意すべき基本が2つあります。

  1. 終了/中断シグナルの監視を忘れない
  2. sleep を入れることも検討する

通常、無限ループのスクリプトを強制終了させるには Ctrl+C などの SIGINT(2番シグナル)がスクリプトのプロセスに送られます。

しかし、OS の再起動時、シャットダウン時もしくは手動によるサービス停止などの場合に意図しないシグナルが送られることがあるので注意します。

例えば、サービスが停止されると kill <サービスの PID> コマンドの SIGTERM(15番シグナル)や SIGKILL(9番シグナル)が実行中のスクリプトに送信されます

そのため、ループ内での「終了シグナルの検知処理」などにも注意する必要があります。

特に、PHP の register_shutdown_function() や Golang の defer などの終了時に実行される関数は、SIGINTSIGTERM による終了の場合、発火(実行)されません。ループ内でシグナルをチェックする必要があります。

この処理を意識しないと、ちゃんとシャットダウン・コマンドを実行して再起動しているのに、スクリプトからみると「急に PC の電源を引っこ抜かれた」時と同じ状態になります。「ファイルを閉じる」や「別プロセスを終了させる」などの処理が必要な場合は特に注意です。

その場合、例えば PHP の場合はプロセスのシグナルを検知・コントロールする pcntl_signalを使う、などの工夫が必要です。

他にも注意しておく点として、sleep があります。

ループ内で処理が発生しない場合、すぐに continue をせずに sleep などのクッションを入れることも検討してください。

while(true) {
    if (isSomething() === false) {
        continue; // ここで超高速な無限ループが発生しているため、他のプロセスが割り込む余地がない
    }
    doSomething();
}

上記のように、入力や状態監視をする際に、ループが高速すぎるため CPU 負荷が上がってしまうことがあります。

この場合、sleep を一瞬でも入れることで、かなり CPU 負荷が減ります。

  while(true) {
      if (isSomething() === false) {
+         usleep(20000);
          continue;
      }
      doSomething();
  }

init.d で起動時に任意のスクリプトをサービス実行する

RaspberryPi(Raspbian Jessie)などの System V 系の init で登録する場合は、本項をご覧ください。SystemD で登録したい場合は次項で説明しています。

サービス設定ファイル「init スクリプト」

登録したいプログラム/スクリプトを用意した次に必要なのが、そのスクリプトをサービスとして登録するための設定ファイルの作成とインストールです。

init のサービス用設定ファイルと言っても、前述したように実態は sh などの shebang 付きのシェル・スクリプトです。

その設定ファイル(スクリプト)が実行される際に start 引数が付いていたら自分のプログラム(登録したプログラム/スクリプト)を起動し、stop 引数が付いていたらプロセスを探して終了させるだけのスクリプトです。意外に力技な仕組みなのです。

普通のシェル・スクリプトとの違いは、呼び出し元の init プログラムに準拠したヘッダー情報(コメント行)および必須機能を含んでいることです。以降は、このサービス用設定ファイルを init スクリプトと呼びます。

インストールは、必要事項を記載した init スクリプトを /etc/init.d/ に設置し、登録コマンド update-rc.d を実行することで行われます。

「登録コマンド」と言っても、具体的には /etc/rc[0-9S].d/ のディレクトリ内にエイリアス(シンボリック・リンク)を張るだけです。そして、init プログラムが起動すると /etc/rc[0-9S].d/ にあるファイルを順番に実行して行きます。

init スクリプト(サービス設定ファイル)は、他のサービスを参考にしても良いのですが、Felix H. Dahlke 氏GitHub で無償で提供しているテンプレートを使うと便利です。気に入ったら「いいね(「★Star」)」してあげてください。

なお、このテンプレートの使い方の詳細は前述の「スクリプト をサービス登録するテンプレート[init.d編]」@ Qiita をご覧ください。

Dasherinit.d でサービス登録する例

それでは init.d に登録する例として、dasher3 というコマンド・アプリをラズパイの init.d にサービスとして登録したいと思います。

何度か言及していますが、起動したら Ctrl+C の押下もしくはプロセスを kill するまで動き続けるスクリプト or アプリであれば、プログラム言語問わず何でも登録可能です。dasher を自身のスクリプト名に置き換えてお読みください。(詳しくは上記の「スクリプト作成時の注意点」をご覧ください。)

  1. テンプレートの生データを開き全体をコピーする。
  2. ラズパイ上の /etc/init.d/ ディレクトリ内にファイルを作成する。(ファイル名はサービス名と同じにします。今回は dasher
  3. sudo 権限でファイルを開きペーストします。(nano でも vim でも可)
$ # ラズパイに SSH 接続する
$ ssh pi@raspberrypi.local
$ 
$ # sudo 権限でテキスト・エディタを開き dasher ファイルにコピー内容をペースト
$ sudo nano /etc/init.d/dasher 
[クリップボードの内容を貼り付ける]

次に、ファイルの先頭にあるヘッダー情報を変更する必要があります。以下が一例です。

#!/bin/sh
### BEGIN INIT INFO
# Provides:          dasher
# Required-Start:    $network $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start dasher daemon at boot time
# Description:       Enable daemon service
### END INIT INFO

dir="/home/pi/dasher"
cmd="npm run start"
user="root"

dir の項目に注目ください。これは、作業ディレクトリを dir に変更します。ここでは Dasher の本体が /home/pi/dasher/ ディレクトリに設置 or インストール済みであることを前提としています。

cmd は実際の実行コマンドで、user は実行するユーザーです。

セキュリティの問題で root 以外のユーザーに設定することもできますが、そのユーザーの環境変数は読み込まないことに注意します。つまり、実行ユーザーの環境変数に追加したパスなども通っていないので、実行スクリプト内で読み込む必要があります。

さて、今回使ったエディタは nano であるため、保存するには CTRL + o と入力してから Enter を押し、CTRL + x でエディタを終了します。

次に、ファイルのパーミッションを変更してスクリプトを 「インストール」(登録)します。

$ sudo chmod 755 /etc/init.d/dasher
$ sudo update-rc.d dasher defaults

以上でラズパイが再起動したときに実行されるはずです。他にも以下ように手動で起動することもできます。

$ sudo /etc/init.d/dasher start

実行中のログを表示するには、ログやエラーファイルを開いて確認します。ログはファイルの末尾に追加されていくので、最新のログを確認したい場合は tail コマンドで出力ログまたはエラーログを表示します。

$ tail -f /var/log/dasher.log
$ tail -f /var/log/dasher.err

SystemD で起動時に任意のスクリプトをサービス実行する

init.d 同様に、SystemD でサービス登録する場合も登録用の「SystemD Unit ファイル」の作成が必要です。

同じく、例として dasher3 というコマンド・アプリをサービスとして登録したいと思います。

  1. 下記 Unit ファイルのサンプルをコピーする。

    SystemDのサービス登録用のテンプレート(Unitファイル)
    [Unit]
    Description=Dasher
    After=network.target
    
    [Service]
    Type=simple
    # dasher のファイルにアクセスするユーザー
    User=root
    # dasher の本体および作業ディレクトリ
    WorkingDirectory=/home/pi/dasher
    # "which npm"コマンドで NPM の絶対パスを確認します
    ExecStart=/usr/local/bin/npm run start
    
    
    [Install]
    WantedBy=multi-user.target
    
  2. ラズパイ上の /etc/systemd/system/ ディレクトリ内に任意の <サービス名>.service のファイルを作成します。(今回は dasher.service

  3. sudo 権限でファイルを開きペーストします。(nano でも vim でも可)

    ファイルの作成とエディタを開く
    $ # ラズパイに SSH 接続する
    $ ssh pi@raspberrypi.local
    $ 
    $ # sudo 権限でテキスト・エディタを開き dasher.service ファイルにコピー内容をペースト
    $ sudo nano /etc/systemd/system/dasher.service
    [クリップボードの内容を貼り付ける]
    

    今回使ったエディタは nano であるため、保存するには CTRL + o と入力してから Enter を押し、CTRL + x でエディタを終了します。

  4. 登録した Unit の再読み込み、自動起動設定および手動起動を行います。

    $ sudo systemctl --system daemon-reload
    $ sudo systemctl enable dasher
    $ sudo systemctl start dasher
    

以上でサービスは起動し、再起動時にも起動するはずです。

すべての設定が正しく機能しているかどうかを確認するには、次のコマンドを使用します。

サービス起動の確認
$ sudo systemctl status dasher
サービスの実行ログを見る
$ sudo journalctl -f -u dasher
  1. 【フォローアップ(Follow-up)とは】前回の不足分を補うための続報・追跡・追求・補足をすること。

  2. 【shebang(シバンまたはシェバン)とは】UNIX/linux のスクリプトの #! から始まる1行目のこと。スクリプトの実行に必要なインタプリタを指定します。shebang なしの場合の呼び出しは「$ php /path/to/myScript.php」となりインタプリタを指定する必要がありますが、shebang ありの場合(スクリプト内の1行目に#!/usr/bin/env php の記載がある場合)の呼び出しは「$ /path/to/myScript.php」とインタプリタの指定が不要になります。環境変数のパスに指定されたディレクトリに設置されたスクリプトは、スクリプト名の指定だけ(パスの指定なし)で実行するためには必須でもあります。

  3. 【Dasher とは】ネットワークを監視し、特定の MAC アドレス(物理アドレス)からのブロードキャスト信号を検知すると、Web Hook を叩く(任意の URL にアクセスする) Node.js ベースのアプリケーションのこと。市販の Amazon Dash Button(ADB)を合法な状態(物理的なクラッキングをせずに) IoT のトリガー(ボタン)として使えるようにしたパイオニア的アプリケーションです。現在は開発を終了しており、後継ではないものの Python ベースの「Nekmo/amazon-dash」の利用が推奨されています。 2

104
86
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
104
86