(今さら)Linuxでサービスを登録する

基盤チームのスミッコメンバとして、プロセスの起動/停止スクリプトまわりの調査タスクがふってきた。とあるサーバプロセスについて、OSを起動した際には自動で起動されるのだが、OSを停止した際に、うまくサービスが停止してくれなかったのだ(OSはRHEL)。ちょっとずつ切り分け、対象を絞っていって、最終的にはロックファイルの生成に行き着いた。この調査の途中で、テスト用のサービス(内容は空)を登録しようとしたのだが、存外はまったのでメモしておく。


やりたいことは「新しい(空の)サービス“ashi(あし)”を登録し、OSの起動/停止の際に、自動で起動/停止するようにする」だけ。


こんなの楽々でしょ、と。まずは、/etc/init.d(これは /etc/rc.d/init.d のへシンボリックリンクになっている)以下に、次のスクリプト ashi を保存する(もちろん実行権限を付与して)。この /etc/init.d/ashi は、何もしない(実行自体をログに出力するだけの)起動/停止スクリプトだ。

#!/bin/bash
#
# ashi test
#
# chkconfig: 2345 70 30
# description: ashi test
# processname: ashi
#
. /etc/rc.d/init.d/functions

name="ashi"
log_file="/var/log/${name}/${name}.log"

# for debug
echo "`date` ${0} ${1}" >> ${log_file}


次に、chkconfig コマンドで、ashi を追加し、ランレベル2、3、4、5でオンにする。これは、次のようにコマンドを実行するだけ。

# chkconfig --add ashi
# chkconfig --level 2345 ashi on
# 
# chkconfig --list ashi
ashi          0:off   1:off   2:on    3:on    4:on    5:on    6:off


この時、上記のスクリプト /etc/init.d/ashi の5行目“chkconfig: 2345 70 30”が参照され、次の6ファイルが作成される。ここで、“2345”は前述したランレベル、“70”は起動順序(数が小さいものから起動される)、“30”は停止順序(数が小さいものから停止される)を表している。これら6ファイルは、全て /etc/init.d/ashi へのシンボリックリンクである(ちなみに、/etc/rc[0-6].d は、/etc/rc.d/rc[0-6].d へのシンボリックリンク)。

  /etc/rc0.d/K30ashi
  /etc/rc1.d/K30ashi
  /etc/rc2.d/S70ashi
  /etc/rc3.d/S70ashi
  /etc/rc4.d/S70ashi
  /etc/rc5.d/S70ashi
  /etc/rc6.d/K30ashi

これでお終い。これで、ランレベルが 3 や 5 に変更される時(つまり、普通に起動する時)には、“S70ashi start”が実行され、ashi が起動する(今回は空だから何も起きないが)。また、ランレベルが 0 に変更される時(シャットダウン時)や 6 に変更される時(リブート時)には、“K30ashi stop”が実行され、ashi が停止する。

はずだったのだが、、なんと“K30ashi stop”が実行されないのだ。/var/log/ashi/ashi.log には、“/etc/rc3.d/S70ashi start”の記録しかない。
もちろん、手動で実行した記録は全て残っている。例えば、“/sbin/service ashi stop”を実行すると、“/etc/init.d/ashi stop”の記録が残る。serviceスクリプトは、/etc/init.d 以下のスクリプトの簡単なラップなので、まぁ当然だ。


仕方なく、スタート地点から再確認していく。
ランレベルが変わる時に何がされるかは、/etc/inittab を見ればよい。で、次のようにある。

l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6

つまり /etc/rc.d/rc の引数にランレベルを渡して実行し、終わるのを待つ感じだ。


さて、/etc/rc.d/rc の停止部分は、次のようになっている。指定されたランレベルが0であれば、“/etc/rc0.d/K〜”のスクリプトを順々に、stop オプションをつけて実行しているのがわかる(変数 subsys には、サービス名 sshdsendmail や ashi などが入る*1)。ちなみに、“action "停止メッセージ" $i stop”は、かっこよい停止コメント(“[ OK ]”みたいなやつ)を出力して停止するだけで、結局“$i stop”が実行される。

# First, run the KILL scripts.
for i in /etc/rc$runlevel.d/K* ; do
        check_runlevel "$i" || continue

        # Check if the subsystem is already up.
        subsys=${i#/etc/rc$runlevel.d/K??}
        [ -f /var/lock/subsys/$subsys -o -f /var/lock/subsys/$subsys.init ] \
                || continue

        # Bring the subsystem down.
        if egrep -q "(killproc |action )" $i ; then
                $i stop
        else
                action $"Stopping $subsys: " $i stop
        fi
done

ん?
なんか、/var/lock/subsys 以下をチェックしている。ここでか?このフォルダ配下に格納するロックファイルって、絶対に使わないといけないの?
例えば今回の“ashi”の場合であれば、/var/lock/subsys/ashi も /var/lock/subsys/ashi.init も存在しない場合には、停止スクリプトが実行されない*2。なるほど、これでか。


てなこって、起動/停止スクリプトは、最低でも、次のようにロックファイルの生成/削除を含めて実装しなければいけない。

#!/bin/bash
#
# ashi test
#
# chkconfig: 2345 70 30
# description: ashi test
# processname: ashi
#
. /etc/rc.d/init.d/functions

name="ashi"
lock_file="/var/lock/subsys/${name}"

start(){
    touch ${lock_file}
}
stop(){
    rm -f ${lock_file}
}

case "${1}" in
    start)
            start
            ;;
    stop)
            stop
            ;;
    restart)
            stop
            start
            ;;
    *)
            echo "Usage: ${0} {start|stop|restart}"
            ;;
esac


いやぁ、よい勉強になった(素人丸出しでみっともない)。

*1:こんな“#”の使い方は知らなかった。例えば ad3=aaabbbcccddd とする。echo ${ad3#aaa} とすると、前方一致した aaa が削られて、結果は bbbcccddd となる。echo ${ad3#abc} とすると、前方一致せず、結果は元々の値 aaabbbcccddd となる。変数展開 $ における演算子の一つとして、「詳解 シェルスクリプト」にばっちり解説されていた。

*2:ちなみに、/var/lock/subsys 配下は、シャットダウンすると強制的に消されるっぽい。ゴミが残らなくて、整合性が確保できる。