UbuntuのイベントベースinitデーモンUpstart

 伝統的なSystem Vのinitデーモン(SysVinit)はホットプラグデバイス、USBハードドライブやUSBメモリ、ネットワーク経由でマウントされたファイルシステムなど、最新のハードウェアをうまく扱えないため、Ubuntuでは Upstart というinitデーモンで置き換えられている。

本稿は、最近出版された書籍A Practical Guide to Ubuntu Linuxの抜粋である。

 SysVinitに代わるものはほかにもいくつかある。特に有名なものとして挙げられるのがinitngであり、DebianやUbuntuで動作する。また、SolarisではSMF(Service Management Facility)、Mac OSではlaunchdが使われている。そのうちにUbuntuでは、これら各システムの機能がUpstartに取り込まれることになるだろう。

 実行レベル(runlevel、ランレベル)ベースのSysVinitデーモン(sysvinitパッケージ)では、各実行レベル(シングルユーザモード、マルチユーザモードなど)と、/etc/rc?.dディレクトリから/etc/init.d内のinitスクリプトへのリンクを利用して、システムサービスの起動および停止を行っている。一方、イベントベースinitデーモンであるUpstart(upstartパッケージ)はイベントを利用して、システムサービスの起動と停止を行う。UbuntuではFeistyリリースでinitデーモンがUpstartに切り替えられ、SystemVの設定からUpstartの設定への移行が開始された。本稿では、Upstartに加えてSysVinitの残存部分(/etc/rc?.dおよび/etc/init.dの各ディレクトリと実行レベルの概念)についても解説する。

 Upstartのinitデーモンはイベントベースであり、システム上のなんらかの変化を捉えて指定されたプログラムを実行する。サービスの起動と停止を行うこれらのプログラムのほとんどは、スクリプトである。こうした設定は、システムが実行レベルに入る際にリンクを用いてinitスクリプトを呼び出すというSysVinitの考え方によく似ている。ただし、Upstartのほうが柔軟性に優れている。実行レベルの変化時に限ってサービスを起動/停止するわけではなく、システムの変化を表す情報を受け取った時点でサービスを起動/停止できる。そのような変化をイベントと呼んでいる。たとえば、Upstartは実行中のシステムに対してファイルシステム、プリンタといったデバイスの追加または削除が行われたという通知をudevから受け取ったときにアクションを起こすことができる。また、システムの起動、シャットダウン、ジョブの状態変化によってもサービスの起動/停止が可能だ。

今後のUpstart
 SysVinitからUpstartへの移行には、Linuxシステムの多くの部分が関係する。切り換えを円滑なものにすると共にエラーの発生を極力抑えるために、Upstartチームは何回かのリリースに分けて移行を行うことを選んだ。

 UbuntuにおけるUpstartデーモンの利用はFeisty(6.10)から始まっている。FeistyからGutsy(7.10)+2リリースの間に、UbuntuはSysVinitの環境から離れ、より簡潔で柔軟なUpstartの環境に移行する。Upstartの管理下に置かれるシステムサービスが増えるにつれ、/etc/init.dおよび/etc/rc?.dディレクトリの中身は/etc/event.dディレクトリのエントリで置き換えられていくことになる。実行レベルはもはやUbuntuの正式な機能ではなくなるが、サードパーティ製ソフトウェアとの互換性のために残される。最終的にはcrondも置き換えられるだろう。

 Upstartシステムは5つのパッケージで構成され、すべてがデフォルトでインストールされている。

  • upstart ― Upstartのinitデーモンおよびinitctlユーティリティ

  • upstart-logd ― logdデーモン、およびlogdサービス用のジョブ定義ファイル

  • upstart-compat-sysv ― SysVinitとの互換性を維持するためのreboot、runlevel、shutdown、telinitの各ユーティリティおよびrcタスク用のジョブ定義ファイル

  • startup-tasks ― システム起動タスク用のジョブ定義ファイル

  • system-services ― ttyサービス用のジョブ定義ファイル

定義

 ここでは、initの説明をわかりやすくするために、いくつかの用語を定義する。

イベントとはinitに通知できる状態の変化である。イベントは、それがシステム内部の状態であれ外部のものであれ、ほぼどんな状態の変化によっても発生させることができる。たとえば、ブートローダはstartupイベントを発生させ、システムの実行レベル2への移行はrunlevel 2というイベントを、ファイルシステムのマウントはpath-mountedイベントを発生させる。ホットプラグデバイスやUSBデバイス(プリンタなど)の取り外しやインストールでイベントが発生することもある。また、initctlのemitコマンドを使えば手動でイベントを発生させることも可能だ。

ジョブとはinitが読み込む一連の命令である。通常、こうした命令にはプログラム(バイナリファイルまたはシェルスクリプト)とイベントの名前が含まれている。イベントが発生すると、対応するプログラムがUpstartのinitデーモンによって実行されるわけだ。ジョブの実行と停止は、それぞれinitctlのstartおよびstopコマンドを使って手動で行うことができる。各ジョブはタスクやサービスごとに分けられている。

タスクとはその作業を実行したうえで待ち状態に戻るジョブである。

サービスとはそれ自身では正常に終了しないジョブである。たとえば、logdデーモンやgettyはサービスとして実装されている。initデーモンは各サービスを監視していて、障害が発生したものを再起動し、イベントや手動操作によって停止したものを抹消する。

 /etc/event.dディレクトリには、ジョブ定義ファイル(Upstartのinitデーモンが実行するジョブを定義したファイル)が収められる。最初は、このディレクトリにUpstartのソフトウェアパッケージ群が入っている。Feisty以降のUbuntuリリースでは、なんらかのサービスをインストールすると、/etc/rc?.dおよび/etc/init.dの各ディレクトリに置かれていた管理用ファイルに代わって、そのサービスの新たな管理用ファイルがこのディレクトリに追加されるようになっている。

 基本的に、Upstartのinitデーモンはステートマシンである。各ジョブの状態を記憶していて、イベント発生に伴うジョブの状態変化を追跡する。initは、あるジョブの状態の変化を捉えてそのコマンドを実行したりジョブを終了させることができる。

 System Vのinitデーモンでは、実行レベルの変化を利用してプロセスの起動/終了を行うタイミングが決められていた。一方、Upstartのinitデーモンを使用するUbuntuシステムには、実行レベルの概念がない。実行レベルベースのシステムからイベントベースのシステムへの移行を容易にし、ほかのディストリビューション向けに作られたソフトウェアとの互換性を維持するために、UbuntuではUpstartを使って実行レベルのエミュレーションを行っている。

 /etc/event.d/rc?ファイル群によって定義されるrc?ジョブは、/etc/init.d/rcというスクリプトを実行する。このスクリプトは/etc/rc?.dディレクトリ内のリンクをたどって/etc/init.d内の対応するinitスクリプトを実行することで、SysVinit環境におけるこれらのリンクの機能をエミュレートしている。rc?の各ジョブはシステムがある実行レベルに移る際にこれらのスクリプトを実行する。システムがある実行レベルから抜ける際には一切動作しない。Upstartでは、SysVinitシステムとの互換性を考えてrunlevelおよびtelinitの各ユーティリティが実装されている。

 initctlユーティリティ(init controlの意)を使えば、システム管理者はroot権限でUpstartのinitデーモンと通信できる。このユーティリティではジョブの開始、終了、レポート表示が行える。たとえば、次の「initctl list」コマンドはジョブとその状態を一覧表示してくれる。

$ sudo initctl list 
logd (stop) waiting
rc-default (stop) waiting
rc0 (stop) waiting
...
tty5 (start) running, process 4720
tty6 (start) running, process 4727

 なお、このセクションで紹介している例の詳細については、initctlのmanページを参照してもらいたい。initctlで使える各コマンドのリストを表示するには「initctl help」(helpの前にハイフンは不要)というコマンドが使える。また、「initctl list --help」とすればlistコマンドの詳細が表示され、「list」の部分をinitctlのほかのコマンドに置き換えればそのコマンドのヘルプ情報が表示される。なお、start、stop、statusの各ユーティリティは同名のinitctl用コマンドを実行するinitctlへのリンクになっている。

ジョブ

 /etc/event.dディレクトリ内の各ファイルは1つのジョブを定義したもので、その定義には通常、イベントとコマンドが最低1つずつ含まれている。イベントが発生するとinitによってそのコマンドが実行される。ここでは、管理者が定義したジョブとUpstartパッケージに付随するジョブの双方を例にとって解説を行う。

 管理者によって定義された次のジョブは、execキーワードを使ってシェルコマンドを実行している。なお、このキーワードは、ファイル内に記述されたシェルスクリプトや実行可能なバイナリファイルの実行にも使える。

$ cat /etc/event.d/mudat 
start on runlevel 2
exec echo "Entering multiuser mode on " $(date) > /tmp/mudat.out

 このファイルには、システムがマルチユーザモード(runlevel 2)に入ったときにシェルコマンドechoを実行するというタスクが定義されている。このコマンドにより、その時点の日時を含むメッセージが/tmp/mudat.outファイルに書き込まれる。シェル側は、substitutionコマンドを使ってdateユーティリティを実行する。このジョブの実行が終わると、mudatタスクは停止し、待ち状態に入る。

 次の例は、catユーティリティによって/tmp/mudat.outファイルの内容を表示し、「initctl list」コマンドによってこのタスクに関するレポートを表示するものだ(同様の情報はstatusユーティリティでも表示される)。

$ cat /tmp/mudat.out 
Entering multiuser mode on Tue Jul 10 17:34:39 PDT 2007

$ sudo initctl list mudat
mudat (stop) waiting

 execのコマンド行にシェルの特殊文字が含まれている場合、initは/bin/sh(dashにリンクされている)を実行したうえでコマンド行をこのシェルに渡す。その他の場合は、execによってコマンド行が直接実行される。複数のシェルコマンドを実行するには、ファイルに記述されたシェルスクリプトをexecを使って実行するか、「script...end script」(後述)を使用する。

 Upstartのinitデーモンで監視できるのは、execを使ってプログラムを実行するジョブ(サービス)だけである。「script...end script」を使って実行されているジョブは監視できない。つまり、サービスではexecを使う必要があるが、タスクではどちらの方法でもプログラムを実行できるわけだ。

myjobサンプル

 イベントを定義し、そのイベントによってトリガされるジョブを設定することもできる。次のmyjobというジョブ定義ファイルは、hithereイベントによってトリガされるジョブを定義したものだ。

$ cat /etc/event.d/myjob 
start on hithere
script
	echo "Hi there, here I am!" > /tmp/myjob.out
 	date >> /tmp/myjob.out
	end script

 myjobファイルは、複数のコマンドを実行するもう1つの方法を示している。この例では「script」と「end script」の両キーワードの間に2つのコマンドが記されている。これらのキーワードを使うと、initは必ず/bin/shを実行する。その間に記されているコマンドは、メッセージと日付を/tmp/myjob.outというファイルに書き込むものだ。このジョブをトリガするには、initctlのemitコマンドが使える。次のinitctlは、このジョブをトリガした際にmyjobが通過する各段階を表示している。

$ sudo initctl emit hithere
hithere
myjob (start) waiting
myjob (start) starting
myjob (start) pre-start
myjob (start) spawned, process 6064
myjob (start) post-start, (main) process 6064
myjob (start) running, process 6064
myjob (stop) running
myjob (stop) stopping
myjob (stop) killed
myjob (stop) post-stop
myjob (stop) waiting

$ cat /tmp/myjob.out 
Hi there, here I am!
Sat Jul 7 20:19:13 PDT 2007

$ sudo initctl list myjob 
myjob (stop) waiting

 この例では、catがmyjobの生成する情報を出力し、initctlがそのジョブの状態を表示している。同じジョブは「initctl start myjob」(または単に「start myjob」)というコマンドでも実行できる。initctlの「start」コマンドは、イベントを発生させずにジョブを実行したい場合に役立つ。たとえば「initctl start mudat」とすれば、runlevel 2イベントを発生させることなく先ほどのmudatジョブを実行できるわけだ。

引数によるイベント指定

 telinitおよびshutdownユーティリティは、引数を含むrunlevel イベントを発生させる。たとえば、shutdownはrunlevel 0というイベントを、telinitはrunlevel 2というイベントを発生させる。これらのイベントは、次の構文を使ってジョブ定義の内部でマッチングを行うことができる。

start | stop on event [arg]

 ここで「event」は「runlevel」のようなイベント、「arg」は省略可能な引数である。システムが実行レベル2に入ったときにジョブを停止させるには、「stop on runlevel 2」とする。また「runlevel [235]」とすれば2、3、5の各実行レベルにマッチさせることができ、「runlevel [!2]」は2を除くすべての実行レベルにマッチする。

 Upstartではイベント指定時に引数を省略することはできるが、ジョブ定義ファイルにおけるイベントの引数は実際にそのイベントに存在するものでなければならない。たとえば、ジョブ定義ファイル内の「runlevel」(引数なし)はすべてのrunlevelイベントにマッチするが、「runlevel S arg2」はどのrunlevelイベントにもマッチしない。runlevelイベントは1つしか引数を取らないからだ。

/etc/event.dディレクトリのジョブ定義ファイル

 UbuntuにおけるSysVinitからUpstart initへの移行に伴い、/etc/event.dディレクトリに定義されるジョブの数は増えることになる。ここでは、Upstartパッケージによってこのディレクトリに置かれたジョブのいくつかを説明する。

 /etc/event.d/rc2というジョブ定義ファイルは、その他のrc?タスクによく似たrc2タスクを定義したものだ。このrc2タスクは、システムがマルチユーザモード(イベント名は「runlevel 2」)に入ると起動され、システムがそれ以外の実行レベル(「runlevel [!2]」)に移ると停止する。スクリプトの最初の部分は、runlevelユーティリティを呼び出している。これにより、システムを実行レベル2に移行させ(Upstartには実際の実行レベルは存在しない)、2つの変数に値が割り当てられる。実際に作業を行うのはexecコマンドであり、2という引数を用いて/etc/init.d/rcスクリプトが実行される。このスクリプトは、/etc/rc?.dディレクトリから引数に対応したリンク先を呼び出す。この場合はrc2のタスクによって、/etc/rc2.dディレクトリ内のリンク先にあたるinitスクリプトが実行される。

$ cat /etc/event.d/rc2 
# rc2 - runlevel 2 compatibility
#
# This task runs the old sysv-rc runlevel 2 ("multi-user") scripts. It
# is usually started by the telinit compatibility wrapper.

start on runlevel 2

stop on runlevel [!2]

console output
script
	set $(runlevel --set 2 || true)
	if [ "$1" != "unknown" ]; then
		PREVLEVEL=$1
 		RUNLEVEL=$2
		export PREVLEVEL RUNLEVEL
	fi

	exec /etc/init.d/rc 2
end script

ttyサービス

 以下に示すのは、tty1のgettyプロセスを起動して監視するサービスのためのジョブ定義ファイルである。

$ cat /etc/event.d/tty1 
# tty1 – getty
#
# This service maintains a getty on tty1 from the point when
# the system is started until it is shut down again.

start on runlevel 2
start on runlevel 3
start on runlevel 4
start on runlevel 5

stop on runlevel 0
stop on runlevel 1
stop on runlevel 6

respawn
exec /sbin/getty 38400 tty1

 このサービスは、実行レベルが2~5のいずれかのイベント発生時(つまり、システムがマルチユーザモードに入ったとき)にgettyプロセスを起動し、実行レベルが0、1、6のいずれかになったとき(つまり、システムのシャットダウン、シングルユーザモードへの移行、リブートが行われたとき)に停止する。「respawn」キーワードは終了時にジョブを再起動するようにinitに指示するものであり、「exec」はtt1のgettyプロセスをボーレート38,400で実行する。以下のinitctlはtty1サービスが起動されてプロセス4747として稼動中であることを、psはこのプロセスに関する情報をそれぞれ示している。

$ sudo initctl list tty1 
tty1 (start) running, process 4747
$ ps -ef | grep 4747 
root   4747   1 0 Jul02 tty1   00:00:00 /sbin/getty 38400 tty1

rc-defaultタスクとinittab

 SysVinitでは、/etc/inittabファイルの「initdefault」エントリを使って、システムの起動時にどの実行レベルに移るかをinitに対して指定する。Ubuntuではデフォルトの状態でinittabファイルが存在せず、Upstartのinitデーモンはブートしたシステムを(rc-defaultタスクを利用して)マルチユーザモード(デフォルトの実行レベル2)に移す。システムを別の実行レベルでブートさせたい場合には、inittabファイルを作成する。次のようにすれば、システムはシングルユーザモード(実行レベルS)でブートする。

$ cat /etc/inittab 
:id:S:initdefault:

 システムがシングルユーザ(リカバリ)モードになっていてrootアカウントのロックが解除されている場合は、initがrootプロンプトの表示前にrootパスワードの入力を求めてくる。それ以外の場合は、パスワードを入力しなくてもrootプロンプトが表示される。

 ただし、実行レベル0(停止)または6(再起動)ではシステムが正常に起動しないため、そのようなブート設定は行わないこと。マルチユーザモード(実行レベル2)でブートさせるには、inittabファイル(存在する場合)を削除するか、先ほどの例の「S」の部分を「2」で置き換えたinittabファイルを作成すればよい。

Copyright 2008 Mark G. Sobell.

Linux.com 原文