近頃ではほぼ全てのLinuxディストリビューションでsystemdによるOS起動へと移行している。互換性のためか/etc/init.d/*にSysV init用のスクリプトを現在でも見かけるが、実際は中からsystemdの制御コマンドsystemctlが呼ばれている物も多く、徐々に廃止されていく流れだろう。
systemdについての理解が浅かったこともあり、今更ながら色々調べて記事にまとめてみた。
systemdを使うメリットとは?
systemdがもたらす最大のメリットはなんといってもOS起動の高速化で、これはサービスの並列起動によって実現されている。
しかし、サービスの並列起動というのは案外難しい。例えばネットワークが立ち上がってないとサーバを起動出来ない、といった依存関係を解決する必要があるからだ。systemdではそういった依存関係を設定ファイルに記述する仕組みが備わっている。
systemdの設定ファイル「ユニットファイル」
systemdの設定ファイルは「ユニットファイル」と呼ばれる。ユニットファイルのマニュアルページにはユニットファイルの一般的な書式とディレクトリに関する決めごとが書かれており、様々な構成要素の関係とそれぞれのマニュアルページへのリンクも張られていて、全体像を把握するならこのマニュアルページを起点にするとよい。
systemdのユニットファイルは主に「サービスユニットファイル」と「ターゲットユニットファイル」の2種類がある。
サービスユニットファイル:個々のサービス起動を記述する
各種サーバなどサービスの設定は拡張子.serviceのサービスユニットファイルに記述する。サービスユニットファイルは以下の3種類のセクションからなる。
- [Unit]セクション : After=もしくはBefore=でユニットの起動順序を指定し、Wants=もしくはRequires=で他のユニットとの依存関係を記述する。
- [Service]セクション : サービスユニットファイル固有のセクション。ExecStart=でサービス開始時に実行するコマンドと引数の指定などを行う。
- [Install]セクション : systemdはこのセクションの中身を直接見ない。代わりにここの設定内容に基づいてsystemctl enable/disableコマンド実行時にシンボリックリンクでユニットの依存関係ツリーがファイルシステム上に作られる。
後で詳しく触れるが、この「依存関係」への理解がsystemdの使いこなしには特に重要だ。
ターゲットユニットファイル:ユニットを束ねる
拡張子.targetのターゲットユニットファイルは、サービスユニットファイルのように何かを起動するための物ではなく、ユニットを束ねる役割を果たす。
ほぼ全てのターゲットユニットにはターゲットユニットファイル名と同じ名前のディレクトリ(“ターゲットユニットファイル名.wants”など)が作られていて、この中に起動対象の他のユニットへのシンボリックリンクが張られている(後述のsystemctl enableコマンドでシンボリックリンクが張られる)。ターゲットユニットはこれらシンボリックファイルで関連付けられたユニットを一つの束として扱い、ターゲットユニットファイルに記述された他ユニットとの起動順や依存関係に関する設定に基づいて束ねたユニットがいつ起動されるべきかを決定する。
ファイルシステム上のディレクトリ構造そのものに意味がある
シンボリックリンクの有無でサービスの起動が決まる
systemdのユニットファイルで依存関係を定義する方法は2種類存在する。
- [Unit]セクションにRequires=もしくはWants=を記述する
- [Install]セクションにWantedBy=(もしくは滅多に使われないがRequiredBy=)を記述する
サービスユニットファイルの[Install]セクションにターゲットユニットを指定しておくと、systemctl enableで前述のターゲットユニット名の付いたディレクトリにシンボリックが張られ、systemctl disabledでシンボリックが削除される。そして、ターゲットユニットが起動対象となると、このシンボリックリンクが張られた先のサービスユニットも起動対象となる。実はsystemctl enable/disableを使わなくても、手動でターゲットユニット名のディレクトリにシンボリックリンクを操作しても結果は同じになる。
SysV initではサービスの起動をシェルスクリプトで陽に指定しなければならなかったが、systemdではシンボリックリンクの有無でサービスの起動する/しないが決まるようにした所が画期的だ。いちいち設定ファイルの中身を調べなくても、lsコマンドでターゲットユニットに紐付いたディレクトリの中身を調べれば起動対象かどうかが簡単に判る。
ディレクトリに優先順位がある
ユニットファイルを置けるディレクトリは複数存在するが、優先度が異なる。殆どのディストリビューションでは以下のディレクトリ構成になっているようだ(確認した限りUbuntu, RedHat, ArchLinux, raspbianで全て同じ)。
- /lib/systemd/system/ : インストールされたパッケージのデフォルトのユニットファイルが置かれる。
- /etc/systemd/system/ : ユーザ定義ファイルで、こちらの方が優先度が高い。後から読み込まれ、デフォルト設定と置き換えられる。
デフォルトのユニットファイルを直接編集してしまうとパッケージをアップデートした時に設定が上書きされる可能性があるため、ユニットファイルを/etc/systemd/system/にコピーしてから変更するか、次に述べるのドロップインディレクトリに差分を記述する方が無難だ。
ドロップインディレクトリに設定の差分だけを追加出来る
foo.serviceというサービスユニットファイルに対して、foo.service.d/という名前でドロップインディレクトリを掘ってその中に設定ファイルを作成すると、その設定内容もfoo.serviceに書かれたのと同じものとして読み込まれる。これはデフォルト設定に少しだけ追加設定を加えたい場合に便利。ドロップインディレクトリを掘る場所は/etc/systemd/system/でOK。
systemdにおけるユニットの依存関係と起動順序
「依存関係」と「起動順序」は別物
混乱しがちだが、依存関係と起動順序が別物ということをまずは押さえておきたい。ざっくり言うと、
- 依存関係 : 起動する/しないを決める
- 起動順序 : 他のユニットより前/後といった起動タイミングを決める
といったところだろうか。特に「依存関係」が直感的には分かりづらいが、
- 余計なものを起動させたくないので、他から必要とされる物だけを起動させる
という大原則に基づいていると考えればわかりやすい。
システムブート時の動作
ブート時にカーネルパラメータで指定しない限り、systemd起動時に最初に見に行くのがdefault.targetだ。
とあるディストリビューションのdefault.targetファイルの中身は以下。
/lib/systemd/system/default.target
[Unit]
Description=Graphical Interface
Documentation=man:systemd.special(7)
Requires=multi-user.target
Wants=display-manager.service
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target display-manager.service
AllowIsolate=yes
このdefault.targetではAfter=にmulti-user.target、rescue.service、rescue.target、display-manager.serviceの4つが指定されており、これらのサービスとターゲットユニットに属する全てのサービスの起動が完了した後でdefault.targetのユニットファイルが実行されるという挙動をする。つまり、このdefault.targetの記述内容を起点にしてユニット間の依存関係を辿っていき、全ての依存関係を満足させようとした結果としてシステムの起動が完了する感じ。
ちなみにここで指定されているmulti-user.targetのサービスが起動すると、昔のSysV initで言うところのrunlevel 5相当になる。
systemctlコマンド
systemdの制御はsystemctlコマンドを使って行う。個人的によく使うコマンドは以下。
- 設定内容の表示
$ systemctl show foo.service
- 設定の有効化
$ sudo systemctl enable foo.service
- 設定の無効化
$ sudo systemctl disble foo.service
- 設定のリロード。ユニットファイル変更後、これを実行しないと変更内容が反映されない。
$ sudo systemctl daemon-reload
- サービスの手動起動
$ sudo systemctl start foo.service
- サービスの手動再起動
$ sudo systemctl restart foo.service
サービス起動順序の指定と確認方法
systemdには出来るだけユニットを並列起動させて起動を高速化したい大原則があるので、不必要な依存関係は定義するべきではない。しかし、あるサービスを起動する前に別のサービスが立ち上がっていて欲しい場合はままある。例えば、先にbarが立ち上がっていないとfooの起動が失敗するような時は、以下のようにUnitセクションにAfter=を記述することで解決出来る。
foo.service
[Unit]
:
After=bar.service
あるいはBefore=の記法を使って、
bar.service
[Unit]
:
Before=foo.service
でも良いだろう。
実際の起動順を確認する方法
しかし、どういうわけか指定した順番で起動してくれない場合がある。そんな時はsystemd-analyze plotコマンドを使うとシステムのブート時にユニットが実際にどの順番で起動しているか確認出来る。
$ systemd-analyze plot > bootseq.svg
svg形式なのでブラウザなどで表示可能。何がどの順番で起動しているか非常に見やすい形で表示される。
上の図はビットマップ画像にしてしまっているが、svgの実体はXMLなのでブラウザで表示させれば文字列検索も出来、興味のあるユニットを探し出すのも容易だ。
起動順が思い通りにならない原因を探る
思い通りの順番で起動しない原因として、WantedByで指定するターゲットユニットがまずいなど依存関係に問題がある場合がある。ユニットの依存関係はsystemctl list-dependenciesコマンドを使うとターゲットユニットごとにグループ化されてツリー状に表示されるのでわかりやすい。
$ systemctl list-dependencies
default.target
● ├─accounts-daemon.service
● ├─apport.service
:(略)
● ├─ureadahead.service
● └─multi-user.target
● ├─anacron.service
● ├─apache2.service
:(略)
● ├─whoopsie.service
● ├─basic.target
● │ ├─-.mount
● │ ├─alsa-restore.service
● │ ├─alsa-state.service
:(略)
systemd.unitのマニュアルページにははっきりとは書かれていないので推測ではあるが、After=やBefore=はこのツリーでレベルをまたぐような場合、つまり異なるターゲットユニット間ではうまく働かないようだ。Before=,After=による順序指定は同一ターゲットユニットの中で行うのが無難だろう。
まとめ
というわけで、systemdについて要点をまとめると、以下のような感じ。
- サービスの並列起動でシステム起動が大幅に高速化された
- 設定を読み出すディレクトリに優先順位が付けられている(/etc/systemd/systemの設定が優先される/ドロップインディレクトリに設定の差分が記述出来る)
- サービスが起動する/しないはターゲットユニットに関連付けられたディレクトリのシンボリックリンクの有無で決まる
- Before=,After=による起動順の指定は、同じターゲットユニットの中で行うのが確実
- 設定が思い通りにならない時はsystemd-analyzeやsystemctl list-dependenciesを活用しよう
少々ややこしい所もあるが、シェルスクリプトベースのSysV initはスクリプトの作者によって記法も作法もバラバラだったのに対し、systemdではすっきり統一されたので慣れればカスタマイズは非常に楽になったと思う。
コメント