めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Systemd入門(1) - Unitの概念を理解する

Linuxの起動処理は、これまでinit/upstartと呼ばれる仕組みで行われていました。Red Hat Enterprise Linux 7 (RHEL7)では、これが、systemdと呼ばれるまったく新しい仕組みに置き換わります。Fedoraでは、すでに先行してsystemdが採用されていますが、この連載(?)では、Fedora 17での実装をベースとして、systemdの考え方や仕組み、利用方法を説明していきます。今回は、systemdの動作の基礎となる「Unit」の概念を理解します。

systemdを採用したFedoraでLinuxの基礎を学びなそう!という方には、「「独習Linux専科」サーバ構築/運用/管理――あなたに伝えたい技と知恵と鉄則」がお勧めです。(^^/

systemdの考え方

参考資料
Rethinking PID 1:systemdの開発者であるLennartによるイントロダクション。
systemd System and Service Manager:systemdのプロジェクトページ

従来のinit/upstratでは、「サービス」の単位で処理が管理されており、大雑把には次の流れでシステムが起動します。

・/etc/rc.d/rc.sysinitでルートファイルシステムのマウントなど、サービス起動の下準備を行う。
・/etc/init.d/以下のシェルスクリプトで順次サービスを起動する。

rc.sysinitを含めて、個々のサービスの起動処理はすべてシェルスクリプトで実装されており、スクリプトの書き方次第でさまざまな処理が実施可能ですが、その一方でいくつかの欠点もあります。

・お作法に従わないシェルスクリプトがあるとシステムの動作が予測不能になる。
・シェルスクリプトを順番に実行していくので、システムの起動に時間がかかる。
・システム起動後にオンデマンドでサービスを起動・停止する仕組みがない。
・サービスごとにcgroupsでリソース配分を調整するなどの仕組みがない。

最後のcgroupsは、サービスの起動スクリプトを書き換えて対応することもできなくはないですが、すべてのサービスが自動的にcgroupsの管理下に入るようなシステム標準の仕組みがあった方が便利ですよね。systemdは、シェルスクリプトに依存した起動処理を完全に排除してこれらの問題を解決します。

Unitによる管理

systemdでは、「Unit」という単位で処理を管理します。これまで、rc.sysinitやサービス起動スクリプトが実施していた処理の内容は、すべて、Unitとして定義されます。Unitは、「target」「mount」「service」「device」など、役割によってタイプがわかれています。それぞれのUnitは、依存関係が定義されており、PID=1の最初のプロセスとしてsystemdが起動すると、「default.target」というUnitを頂点とする、依存関係のツリーを構築した後、依存するUnitを起動していきます。

この時、それぞれのUnitは、依存関係(AはBが必要)の情報とは別に、起動順序(AはBの後に起動)についての設定が与えられます。起動順序の指定がない場合、依存関係のあるUnitについても並列に起動処理が行われます。systemdは、順序関係の情報をもとにして、複数のUnitをできるかぎり並列に起動していきます。

Unit間の「依存関係」と「順序関係」を分けて定義できるところが、systemdの特徴と言えるかも知れません。Upstartでは、イベントの連鎖によって依存する処理が順次実行されていきますが、この場合は、「依存関係=順序関係」となるため、「依存はしているが並列に起動してもかまわない」というような処理を並列化することができませんでした。

さらに、Unitの中には、「特定のデバイスが接続された」「D-Bus経由でサービスが要求された」など、システム稼働中に発生するイベントをトリガーとして、オンデマンドに起動するものもあります。つまり、必ずしもシステム起動時に立ち上げる必要の無いサービスを起動処理から除外することが可能になります。

このように、systemdを利用すると、

・シェルスクリプトを使わずにsystemdが直接Unit(サービス)を起動する。
・Unitの起動処理を可能な限り並列化する。
・Unitの起動をオンデマンド化する。

という工夫にによって、システムの起動時間が圧倒的に短縮されます。

ちなみに、LennartのBlogによると、systemdが標準提供するUnitで実施可能な処理には、次のようなものがあります。

  • Checking and mounting of all file systems
  • Updating and enabling quota on all file systems
  • Setting the host name
  • Configuring the loopback network device
  • Loading the SELinux policy and relabelling /run and /dev as necessary on boot
  • Registering additional binary formats in the kernel, such as Java, Mono and WINE binaries
  • Setting the system locale
  • Setting up the console font and keyboard map
  • Creating, removing and cleaning up of temporary and volatile files and directories
  • Applying mount options from /etc/fstab to pre-mounted API VFS
  • Applying sysctl kernel settings
  • Collecting and replaying readahead information
  • Updating utmp boot and shutdown records
  • Loading and saving the random seed
  • Statically loading specific kernel modules
  • Setting up encrypted hard disks and partitions
  • Spawning automatic gettys on serial kernel consoles
  • Maintenance of Plymouth
  • Machine ID maintenance
  • Setting of the UTC distance for the system clock

ファイルシステムのfsckとマウントなど、これまで、rc.sysinitの中で実行されていた処理も個別のUnitとして提供されていることがわかります。

Unitの依存関係

まず、Unitの定義ファイルは以下のディレクトリ配下にあります。/usr/lib/systemd/system/以下の設定ファイルを変更する際は、一旦、/etc/systemd/system/以下にコピーしてから変更するようにします。

ディレクトリ 説明
/usr/lib/systemd/system システムデフォルトの設定。RPMパッケージが提供するデフォルト設定を配置する。
/etc/systemd/system ユーザ設定。ここに同名のファイルを配置するとこちらのファイルの内容が優先される。

Unitには、「target」「service」などのタイプがあると説明しましたが、設定ファイル名の末尾「.target」「.service」で判別ができます。また、ある設定ファイルに対して、「設定ファイル名.wants」というディレクトリが用意されている場合、このディレクトリ内に前提となるUnitを記載する(前提Unitの設定ファイルへのシンボリックリンクを作成する)ことで、依存関係が定義されます。

依存関係の定義方法には、.wantsディレクトリを使う他に、設定ファイル内の[Unit]セクションにおいて、「Wants=」オプション、もしくは「Requires=」オプションで指定する方法があります。一般に、システム的に必須の依存関係は「Wnats/Requires=」オプションに記載しておき、システム管理者が環境に応じて設定する依存関係は、.wantsディレクトリを使用します。特に、後述のsystemctlコマンドで、サーバ起動時に自動起動するサービスを設定した場合は、.wantsディレクトリにシンボリックリンクを作成することで、自動起動が設定されます。

「Wants=」と「Requires=」には微妙な違いがあります。Requiresの場合は、前提のUnitが起動に失敗すると、このUnitの起動処理を中断します。一方、Wantsの場合は、前提のUnitが起動に失敗した場合でも、このUnitの起動処理は継続します。

話が抽象的になってきたので、少し具体例を示しておきましょう。まず、主なUnitのタイプをまとめておきます。

Unitのタイプ 説明
mount 指定のファイルシステムをマウントする
automount   オートマウント処理を実施する(automountdの代替的な機能)
service 指定のバイナリを実行する(主にはデーモンの起動に使用する)
socket systemdがSocketをListenして、接続があるとプロセスに受け渡す(xinetdの代替的な機能)
path 指定のファイルが作成されると、指定されたサービスを起動する
device udevから通知されたデバイスを表す
target 複数のUnitをまとめるために使用する

上記のようにデーモンプロセスの起動など、従来の「サービス」に相当するUnitは「service」です。一方、「target」は、複数のUnitを束ねるために使用するもので、それ自体は特定の処理を行うわけではありません。たとえば、従来rc.sysinitの内部で行っていた処理に対応するservice群を「sysinit.target」の前提Unitとしてまとめておけば、「sysinit.taget」を起動することで、これらをまとめて実行することができます。

sysinit.targetに対して、依存関係が設定されているUnitは、「system.target.wants」ディレクトリにあるはずですが、実際に確認すると次のようになっています。

# tree /usr/lib/systemd/system/sysinit.target.wants
/usr/lib/systemd/system/sysinit.target.wants
|-- cryptsetup.target -> ../cryptsetup.target
|-- dev-hugepages.mount -> ../dev-hugepages.mount
|-- dev-mqueue.mount -> ../dev-mqueue.mount
|-- plymouth-read-write.service -> ../plymouth-read-write.service
|-- plymouth-start.service -> ../plymouth-start.service
|-- proc-sys-fs-binfmt_misc.automount -> ../proc-sys-fs-binfmt_misc.automount
|-- sys-fs-fuse-connections.mount -> ../sys-fs-fuse-connections.mount
|-- sys-kernel-config.mount -> ../sys-kernel-config.mount
|-- sys-kernel-debug.mount -> ../sys-kernel-debug.mount
|-- systemd-ask-password-console.path -> ../systemd-ask-password-console.path
|-- systemd-binfmt.service -> ../systemd-binfmt.service
|-- systemd-journald.service -> ../systemd-journald.service
|-- systemd-modules-load.service -> ../systemd-modules-load.service
|-- systemd-random-seed-load.service -> ../systemd-random-seed-load.service
|-- systemd-sysctl.service -> ../systemd-sysctl.service
|-- systemd-tmpfiles-setup.service -> ../systemd-tmpfiles-setup.service
`-- systemd-vconsole-setup.service -> ../systemd-vconsole-setup.service

前述のように、.wantsディレクトリ配下に前提Unitの設定ファイルへのシンボリックリンクが作成されています。それぞれのUnit名から、特殊ファイルシステムのマウントやカーネルパラメータの設定(sysctl)などの処理が想像されます。それから、設定ファイル内に直接に記載された前提Unitもあります。次のように、「local-fs.target」と「swap.target」が前提として設定されています。このように、targetの前提として、さらに他のtargetをネストすることもできるわけです。

# cat /usr/lib/systemd/system/sysinit.target | grep -E '(Wants|Requires)'
Wants=local-fs.target swap.target

それでは、「sysinit.target」自体はどのようにして起動されるのでしょうか? システム上でsystemdが起動すると、まずはじめに「default.target」を起動するようになっています。したがって、default.targetの前提としてsysinit.targetを設定することで、これが行われます。

default.targetからの依存関係ツリー

それでは改めて、default.targetからどのような依存関係が定義されているかを見てみましょう。default.targetはシステム管理者が変更できるように、/etc/systemd/system/以下に用意されており、他のtargetへのシンボリックリンクになっています。このシンボリックリンクが指す先を変えることは、従来の「デフォルトrunlevelを変更する」という処理に相当します。テキストログイン環境のFedora 17では、次のようになっています。

# ls -l default.target
lrwxrwxrwx. 1 root root 36  9月 14 19:57 default.target -> /lib/systemd/system/runlevel3.target
# ls -l /lib/systemd/system/runlevel3.target
lrwxrwxrwx. 1 root root 17  9月 14 19:33 /lib/systemd/system/runlevel3.target -> multi-user.target

ちょっとまわりくどいですが、シンボリックリンクを2回たどって、「/lib/systemd/system/multi-user.target」を差しています。最初からmulti-user.targetを指してもいいのですが、旧来のrunlevelとの対応が分かるように、runlevel3.targetというシンボリックリンクが用意されているようですね。その他には、次のようなシンボリックリンクが用意されています。

# ls -l /lib/systemd/system/runlevel*.target
lrwxrwxrwx. 1 root root 15  9月 14 19:33 /lib/systemd/system/runlevel0.target -> poweroff.target
lrwxrwxrwx. 1 root root 13  9月 14 19:33 /lib/systemd/system/runlevel1.target -> rescue.target
lrwxrwxrwx. 1 root root 17  9月 14 19:33 /lib/systemd/system/runlevel2.target -> multi-user.target
lrwxrwxrwx. 1 root root 17  9月 14 19:33 /lib/systemd/system/runlevel3.target -> multi-user.target
lrwxrwxrwx. 1 root root 17  9月 14 19:33 /lib/systemd/system/runlevel4.target -> multi-user.target
lrwxrwxrwx. 1 root root 16  9月 14 19:33 /lib/systemd/system/runlevel5.target -> graphical.target
lrwxrwxrwx. 1 root root 13  9月 14 19:33 /lib/systemd/system/runlevel6.target -> reboot.target

したがって、systemdは、「multi-user.target」の前提となるUnitを探して、これを起動します。これもまた、システム管理者が変更できるように、/etc/systemd/system以下に.wantsディレクトリが用意されています。

# tree /etc/systemd/system/multi-user.target.wants
/etc/systemd/system/multi-user.target.wants
|-- NetworkManager.service -> /usr/lib/systemd/system/NetworkManager.service
|-- arp-ethers.service -> /usr/lib/systemd/system/arp-ethers.service
|-- auditd.service -> /usr/lib/systemd/system/auditd.service
|-- crond.service -> /usr/lib/systemd/system/crond.service
|-- remote-fs.target -> /usr/lib/systemd/system/remote-fs.target
|-- rsyslog.service -> /usr/lib/systemd/system/rsyslog.service
|-- sendmail.service -> /usr/lib/systemd/system/sendmail.service
|-- sm-client.service -> /usr/lib/systemd/system/sm-client.service
`-- sshd.service -> /usr/lib/systemd/system/sshd.service

それっぽいserviceが起動されることがわかります。設定ファイルに内に直接に記載された前提は次のとおりです。

# cat /usr/lib/systemd/system/multi-user.target | grep -E '(Wants|Requires)'
Requires=basic.target

# cat /usr/lib/systemd/system/basic.target | grep -E '(Wants|Requires)'
Requires=sysinit.target sockets.target

これもちょっとまわりくどいですが、「multi-user.target -> basic.target -> sysinit.target」という順番で、最終的にsysinit.targetがmulti-user.targetの前提になっていることがわかります。おそらく、複数の"runlevel"で必要なサービスをbasic.targetにまとめているのでしょう。

この例から分かるように、設定ファイルだけを追って、依存関係の全体像を把握するのはかなり面倒です。Fedora 19では、「systemctl list-dependencies」コマンドにより、systemdが実際に認識している依存関係を表示することができるようになっています。

systemctlコマンドによるUnitの確認

「systemctl list-units」コマンドを利用すると、現在の設定で稼働しているUnitを確認することができます。次は、稼働しているserviceの一覧です。

# systemctl list-units --type=service
UNIT                             LOAD   ACTIVE SUB     JOB DESCRIPTION
auditd.service                   loaded active running     Security Auditing Service
crond.service                    loaded active running     Command Scheduler
dbus.service                     loaded active running     D-Bus System Message Bus
fedora-readonly.service          loaded active exited      Configure read-only root support
fedora-storage-init-late.service loaded active exited      Initialize storage subsystems (RAID, LVM, etc.)
fedora-storage-init.service      loaded active exited      Initialize storage subsystems (RAID, LVM, etc.)
fedora-wait-storage.service      loaded active exited      Wait for storage scan
getty@tty1.service               loaded active running     Getty on tty1
ip6tables.service                loaded active exited      IPv6 firewall with ip6tables
iptables.service                 loaded active exited      IPv4 firewall with iptables
lvm2-monitor.service             loaded active exited      Monitoring of LVM2 mirrors, snapshots etc. using dmeventd or p
NetworkManager.service           loaded active running     Network Manager
rsyslog.service                  loaded active running     System Logging Service
sendmail.service                 loaded active running     Sendmail Mail Transport Agent
sm-client.service                loaded active running     Sendmail Mail Transport Client
sshd.service                     loaded active running     OpenSSH server daemon
systemd-journald.service         loaded active running     Journal Service
systemd-logind.service           loaded active running     Login Service
systemd-remount-fs.service       loaded active exited      Remount Root and Kernel File Systems
systemd-sysctl.service           loaded active exited      Apply Kernel Variables
systemd-tmpfiles-setup.service   loaded active exited      Recreate Volatile Files and Directories
systemd-user-sessions.service    loaded active exited      Permit User Sessions
systemd-vconsole-setup.service   loaded active exited      Setup Virtual Console
udev-settle.service              loaded active exited      udev Wait for Complete Device Initialization
udev-trigger.service             loaded active exited      udev Coldplug all Devices
udev.service                     loaded active running     udev Kernel Device Manager

serviceの中には、デーモンプロセスとして起動したままになるものと、必要な処理を実行して終了するものがあります。「SUB」が「running」のものは、デーモンとして起動中のもので、「exited」は必要な処理が終了した状態のものです。その他のタイプも同様に確認できます。mountタイプはファイルシステムをマウントすることが仕事でしたが、次のように有効化されてマウント処理が終わっていることがわかります。

# systemctl list-units --type=mount
UNIT                    LOAD   ACTIVE SUB     JOB DESCRIPTION
-.mount                 loaded active mounted     /
boot.mount              loaded active mounted     /boot
dev-hugepages.mount     loaded active mounted     Huge Pages File System
dev-mqueue.mount        loaded active mounted     POSIX Message Queue File System
media.mount             loaded active mounted     Media Directory
sys-kernel-config.mount loaded active mounted     Configuration File System
sys-kernel-debug.mount  loaded active mounted     Debug File System

一方、現在の状況とは関係なく、すべてのUnitを表示するには次のようにします。次はserviceタイプの例です。

# systemctl list-unit-files --type=service
UNIT FILE                                   STATE   
arp-ethers.service                          enabled 
auditd.service                              enabled 
autovt@.service                             disabled
console-getty.service                       disabled
console-shell.service                       disabled
crond.service                               enabled 
dbus-org.freedesktop.hostname1.service      static  
dbus-org.freedesktop.locale1.service        static  
dbus-org.freedesktop.login1.service         static  
dbus-org.freedesktop.NetworkManager.service enabled 
dbus-org.freedesktop.timedate1.service      static  
dbus.service                                static  
debug-shell.service                         disabled
display-manager.service                     static  
dm-event.service                            disabled
dnsmasq.service                             disabled
dracut-shutdown.service                     static  
emergency.service                           static  
fedora-autorelabel-mark.service             static  
fedora-autorelabel.service                  static  
fedora-configure.service                    static  
fedora-import-state.service                 static  
fedora-loadmodules.service                  static  
fedora-readonly.service                     static  
fedora-storage-init-late.service            static  
fedora-storage-init.service                 static  
fedora-wait-storage.service                 static  
fsck-root.service                           static  
fsck@.service                               static  
getty@.service                              enabled 
halt-local.service                          static  
halt.service                                static  
hibernate.service                           static  
ip6tables.service                           enabled 
iptables.service                            enabled 
kexec.service                               static  
lvm2-monitor.service                        enabled 
messagebus.service                          static  
NetworkManager-wait-online.service          disabled
NetworkManager.service                      enabled 
plymouth-halt.service                       static  
plymouth-kexec.service                      static  
plymouth-poweroff.service                   static  
plymouth-quit-wait.service                  static  
plymouth-quit.service                       static  
plymouth-read-write.service                 static  
plymouth-reboot.service                     static  
plymouth-start.service                      static  
poweroff.service                            static  
prefdm.service                              static  
quotacheck.service                          static  
quotaon.service                             static  
rc-local.service                            static  
rdisc.service                               disabled
reboot.service                              static  
rescue.service                              static  
rsyslog.service                             enabled 
saslauthd.service                           disabled
sendmail.service                            enabled 
serial-getty@.service                       static  
single.service                              static  
sm-client.service                           enabled 
sshd.service                                enabled 
suspend.service                             static  
syslog.service                              enabled 
...(以下省略)...

ここで、「enabled」「disabled」「static」という表記が出てきました。これは、以前のchkconfigによるon/offに対応するものです。まず、「enabled/disabled」が表示されているものは、次のようにシステム起動時の自動起動(default.targetの前提とするかどうか)をon/offすることができます。

# systemctl disable sshd.service
rm '/etc/systemd/system/multi-user.target.wants/sshd.service'

# systemctl enable sshd.service
ln -s '/usr/lib/systemd/system/sshd.service' '/etc/systemd/system/multi-user.target.wants/sshd.service'

これは、sshd.serviceの自動起動を無効化/有効化している例ですが、systemctlコマンドの実行直後に「rm/ln」コマンドが表示されています。有効化した場合は、multi-user.target.wantsディレクトリにシンボリックリンクをおいて、multi-user.targetの前提としています。無効化した場合は、シンボリックリンクを削除して依存関係をなくしています。先に説明した仕組みと動きが合致していますね。

一方、[Install]セクションを持たないserviceは、有効化/無効化の対象にはなりません。先の出力で「static」と表示されたものがこれにあたります。一般には、他のUnitからの固定的な依存関係で起動することになります。たとえば、plymouth-poweroff.serviceは「static」になっていますが、これは、システムをシャットダウンする際に起動するpoweroff.targetの前提として設定されています。

なお、serviceを有効化した際に、どのtargetの前提として設定するかは、そのserviceの設定ファイルの[Install]セクションにおいて、「WantedBy=」オプションで指定されています。

# cat /usr/lib/systemd/system/sshd.service 
...(中略)...
[Install]
WantedBy=multi-user.target

WantedByには、次にように、複数のtargetを指定することも可能です。

[Install]
WantedBy=multi-user.target rescue.target

ただし、runlevel 5に相当する「graphical.target」に対しては、「multi-user.target」が前提として定義されていますので、multi-user.targetで有効化されたserviceは、自動的にgraphical.targetでも有効化されます。multi-user.targetとgraphical.targetの両方を指定する必要はありません。

# cat /usr/lib/systemd/system/graphical.target | grep -E '(Wanted|Requires)'
Requires=multi-user.target

まとめると、

・runlevel 3と5で有効化したい → WantedByにmulti-user.targetを指定
・runlevel 5だけで有効化したい → WantedByにgraphical.targetを指定

ということになります。

最後に、systemdが特定のUnitに対して認識している設定の詳細は、次のコマンドで表示します。該当Unitに対して、デフォルト値として自動設定されているオプションを確認することもできます。

# systemctl show sshd.service
Id=sshd.service
Names=sshd.service
Requires=systemd-journald.socket basic.target
WantedBy=multi-user.target
Conflicts=shutdown.target
Before=shutdown.target multi-user.target
After=syslog.target network.target auditd.service systemd-journald.socket basic.target
Description=OpenSSH server daemon
...(以下省略)...