systemdの歴史と採用された理由
SysV initの依存関係管理とプロセス追跡の限界を、Upstartやlaunchdの試みと比較します。4年で各ディストリビューションがsystemdへ移行した経緯と、妥当だった反対意見を解説します。
systemd が採用された理由
systemd の歴史は、SysV init では実現できなかった 2 つの課題から始まります。SysV init(System V init。AT&T Unix から Linux が受け継いだ起動システム)には、サービスの依存関係を記述する手段がありませんでした。また、サービスの起動後に、どのプロセスがそのサービスに属するかを把握する手段もありませんでした。systemd は、シェルスクリプトでは利用できないカーネル機能によって、この 2 つの課題に対応しました。プロセスの追跡には control group を、起動順序の制御には事前に開いた listening socket を使用します。以降の歴史は、この 2 つの仕組みが userland の他の部分へ広がった経緯です。そこから反対意見が生まれましたが、その中には妥当なものもありました。
SysV init が実際に行っていたこと
SysV システムでは、PID 1(プロセス ID 1。kernel が最初に起動するプロセス)が /etc/inittab を読み取り、runlevel を選択して、その runlevel 用のスクリプトを実行していました。スクリプトは /etc/init.d/ に置かれていました。/etc/rc3.d/ 内の symbolic link が、実行するスクリプトと実行順序を決めていました。そのため、/etc/rc3.d/S20nginx は /etc/init.d/nginx を指し、start を引数として呼び出されていました。
#!/bin/sh
### BEGIN INIT INFO
# Provides: nginx
# Required-Start: $local_fs $remote_fs $network $syslog
# Required-Stop: $local_fs $remote_fs $network $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
### END INIT INFO
case "$1" in
start)
start-stop-daemon --start --quiet --pidfile /run/nginx.pid \
--exec /usr/sbin/nginx
;;
stop)
start-stop-daemon --stop --quiet --retry TERM/30/KILL/5 \
--pidfile /run/nginx.pid
;;
esacS20nginx 内の 20 は依存関係ではなく、位置を示します。このスクリプトが S19 の後、S21 の前に実行されることを示すだけです。理由は示さないため、検証することはできません。また、人が安全だと判断しない限り、互いに無関係な 2 つのスクリプトを安全に同時実行することもできません。
rc プログラムは各スクリプトを順番に実行し、終了するまで待機していました。ネットワークアドレスを待って 30 秒間ブロックするスクリプトがあると、ネットワークをまったく使用しないサービスも含め、boot 全体が 30 秒間停止しました。
そのスクリプトの先頭にある LSB(Linux Standard Base)header は、この問題を内部から解決する試みでした。2011 年の Debian 6.0 は insserv をデフォルトにしました。これは各スクリプトから Required-Start を読み取り、graph を構築して symbolic link の番号を振り直します。これにより、Debian は独立したスクリプトを startpar で同時実行できるようになりました。これは改善でしたが、より深い問題は解決しませんでした。依存関係が依然として、スクリプトの終了に依存していたためです。S20nginx が 0 を返すことは、shell function が戻ったことを意味します。nginx が接続を受け付けていることを意味するわけではありません。
init スクリプトでは解決できなかった5つの問題
- 並列起動。ファイル名による順序付けは、マシン上のすべてのサービスに対する完全順序です。そのため、起動時間は各サービスの処理時間の合計と同じになります。
- 準備完了状態の判定。start スクリプトはデーモンを fork した時点で終了します。デーモンがリクエストを処理できる状態になった時点ではありません。そのため、次のスクリプトが早すぎるタイミングで起動することがよくあります。
- 監視。デーモンが2回 fork すると親プロセスが終了し、デーモンは端末から切り離されて PID 1 に再親化されます。init は子プロセスの終了を認識できますが、生き残ったプロセスとの信頼できる関連付けを持ちません。
- オンデマンド起動。inetd(インターネットスーパーサーバー)は、接続が到着したときにデーモンを起動できました。しかし、独自の設定ファイルを持つ別個のシステムであり、起動時にほかのサービスを起動する順序には何も対処しませんでした。
- リソース制御。init スクリプトでは、サービスのメモリ使用量や CPU 使用率を制限できませんでした。
ulimitは1つのプロセスに適用されるだけで、niceはスケジューラにしか作用しません。そのため、サービスの子プロセスが暴走すると、システム上のほかのプロセスと区別できませんでした。
監視の欠落は、日常の運用で最も大きな問題になりました。PID ファイルがその回避策でした。デーモンはプロセス ID を /run/nginx.pid に書き込み、stop 関数はそのファイルを読み取ります。デーモンが強制終了されると、ファイルは残ります。その後、カーネルがその番号を別のプロセスに再利用すると、start-stop-daemon --stop --pidfile はその番号を現在使用しているプロセスにシグナルを送信します。古い PID ファイルが残っていると、init スクリプトは誤ったプロセスを終了させます。
launchd は最初にソケットの問題を解決した
Apple は Dave Zarzycki が記述した launchd を、2005 年に Mac OS X 10.4 でリリースしました。1 つのプロセスが init、rc、xinetd、crond、watchdogd を置き換えました。
コピーする価値があった考え方は、ソケットアクティベーションです。launchd は最初にすべての待ち受けソケットを作成し、その後でデーモンを起動します。まだ起動していないデーモンにクライアントが接続しても、connection refused にはなりません。デーモンが accept() を呼び出すまで、カーネルがそのソケットの backlog キューに接続を保持するためです。2 つのデーモン間の起動順序を人間が指定する必要もなくなります。ソケットが処理します。
launchd は Mach IPC(プロセス間通信)を基盤として構築されています。Mach IPC は Apple の XNU kernel に属するもので、Linux に相当するものはありません。そのコードを移植するのは現実的ではありませんでした。それでも、この考え方は広まりました。
Upstart はイベントを処理単位にした
Canonical の Upstart は Scott James Remnant が開発し、2006 年 10 月に Ubuntu 6.10 でリリースされました。Fedora 9 から Fedora 14、RHEL 6、Chrome OS でも採用されました。runlevel をイベントに置き換え、job にその job を開始・停止するイベントを指定しました。
# /etc/init/example.conf
start on filesystem and net-device-up IFACE!=lo
stop on runlevel [!2345]
respawn
respawn limit 10 5
exec /usr/local/bin/exampledjob の数が増えると、2 つの問題が明らかになりました。1 つ目は依存関係の向きです。job は「このイベントが発生したら自分を開始する」と記述するため、何が何に依存するかという情報が適切でないファイルに置かれます。サービスは必要なものを把握できますが、翌年に何がそのサービスを必要とするかまでは把握できません。サービスを追加する際、既存の job を編集して新しいイベントを発生させる必要が生じることもありました。
2 つ目は追跡です。Upstart は、fork する daemon を fork() の呼び出し回数を ptrace で数えて追跡していました。この値は expect fork または expect daemon として設定します。fork 数を誤ると、Upstart はすでに終了したプロセスを監視したり、すでに発生した fork を待ち続けたりします。エラーを表示せずに initctl start がハングすることが症状として現れますが、job ファイルからその理由を説明する方法はありません。
また、Upstart に貢献するには Canonical の contributor agreement に署名する必要がありました。これは技術上の欠陥ではありませんが、開発に参加する人々に影響を与えました。
PID 1 を再考する、2010年4月
2010年4月30日、Lennart Poettering は「PID 1 を再考する」という記事を公開しました。Kay Sievers もこのプロジェクトに参加しました。その主張は4つの項目から成ります。
- 起動するものを減らす。多くのサービスは、実際に要求されるまで起動を待てます。
- ソケットで順序を確定できる場合は、起動順序を個別に定義しない。すべてのソケットを一括して開き、その後ですべてを同時に起動します。
- PID ファイルではなく、control group でプロセスを追跡する。
- サービスを宣言的なファイルで記述する。これにより、1つの記述をすべてのディストリビューションで使用できます。
その年に最初のリリースが行われました。Fedora 14 は2010年11月に systemd をオプションとして提供し、Fedora 15 は2011年5月に systemd をデフォルトにしました。
監視を信頼できるものにした cgroups
cgroup(control group)は、プロセスをグループ化するためのカーネル機能です。2008 年に Linux 2.6.24 へ統合されました。systemd は各サービスを専用の cgroup に配置します。子プロセスは親プロセスの cgroup を継承し、権限のないプロセスは cgroup の外へ自分自身を移動できません。そのため、二重 fork を使ってもプロセスを隠せません。PID 1 は、各 unit に属するプロセスの正確な集合を常に保持します。サービスの停止とは、その cgroup 内のすべてを kill することです。これがデフォルトの KillMode=control-group の動作です。
systemctl status はそのグループを出力します。
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-08-11 09:14:22 UTC; 3min ago
Main PID: 1042 (nginx)
Tasks: 3 (limit: 4653)
Memory: 6.1M (peak: 7.4M)
CGroup: /system.slice/nginx.service
├─1042 "nginx: master process /usr/sbin/nginx"
├─1043 "nginx: worker process"
└─1044 "nginx: worker process"このブロックが、古い PID ファイルの問題に対する答えです。リストはカーネルの状態なので、古くなるファイル自体が存在しません。
同じツリーには制限も適用されます。cgroup は、プロセス追跡に使われるようになる前から、アカウンティング用に設計されていたためです。MemoryMax=、CPUQuota=、TasksMax= は、それぞれ 1 行で設定できます。サービスにメモリと CPU の上限を設定することは、現在では drop-in ファイルを用意するだけです。2009 年には、誰も作成しなかった shell script へのパッチが必要でした。
2011年から2015年にすべてのディストリビューションが切り替えた理由
- Fedora 15、2011年5月。
- openSUSE 12.1、2011年11月。
- Mageia 2、2012年5月。
- Arch Linux、2012年10月から新規インストール時のデフォルト。
- RHEL 7、2014年6月。
- SLES 12、2014年10月。
- Debian 8、2015年4月。
- Ubuntu 15.04、2015年4月。
RHEL 7 の系譜は他のものより長く続きました。CentOS 7 がそれを再構築しており、多くの管理者が初めて unit ファイルに触れたのは、Red Hat Linux から CentOS、Rocky、AlmaLinux へと続く長い経緯の中でのことだったためです。
理由の大半は地味でした。そのため、切り替えは速く進みました。
- 1つの unit ファイルがすべてのディストリビューションで動作するため、上流プロジェクトは
.serviceファイルを配布するようになりました。ディストリビューション側も、リリースごとにパッケージごとの shell script を保守する必要がなくなりました。 - ConsoleKit の保守が2012年ごろに終了した後、デスクトップセッションの追跡は
systemd-logindに移行しました。GNOME には logind が必要だったため、systemd を採用しないディストリビューションは代替実装を用意する必要がありました。その代替実装であるelogindは、systemd の logind を分離して個別に保守するものです。 - デバイスマネージャーである udev は、2012年4月に systemd のソースツリーへ統合されました。udev を配布するディストリビューションは、systemd のリポジトリも追跡することになりました。これを受けて、Gentoo は
eudevを fork しました。 - コンテナでは、プロセスの確実な追跡とサービス単位の制限がより重要になりました。どちらも cgroup の機能だからです。コンテナのプロセスをどの supervisor が管理するかという問題は、Docker Compose の stack を再起動後に復旧させる場合にも、現在まで残っています。
Debian の決定が大きな議論を呼びました。Technical Committee は2014年2月に投票し、票が同数になったため、議長の Bdale Garbee が systemd を支持する決定票を投じました。数日後、Ubuntu は Upstart を継続せず、Debian に追随すると発表しました。Debian 開発者の一部は2014年11月にディストリビューションを fork して Devuan とし、2017年5月に Devuan 1.0 をリリースしました。
公平な形で述べた異論
対象範囲。 1 つのプロジェクトが、現在では PID 1、ログデーモン、ログインセッション管理、デバイスマネージャー、ネットワーク設定デーモン、DNS (domain name system) リゾルバー、NTP (network time protocol) クライアント、コンテナランナー、ブートローダーを提供しています。これらは個別のバイナリであり、すべてをインストールする必要はないという通常の反論は正しいものの、異論への回答にはなっていません。デスクトップ環境が logind を必要とし、logind が systemd のツリーから分離してリリースされれば、選択の自由は失われます。これが議論でいう結合であり、実際に起きたことです。
バイナリジャーナル。 journald はプレーンテキストではなく、索引付きのバイナリ形式に書き込みます。テキストでは得られなかった機能として、unit 単位および優先度単位のフィルタリング、構造化フィールド、送信元プログラムが偽装できないメタデータがあります。journald 自身が unit と cgroup を記録するためです。journalctl -u nginx -p err --since "-1h" は grep を日付の正規表現に置き換えます。コストも現実に存在します。起動できないマシンでは、レスキューシェルから less を使ってログを読めません。代わりに、マウントしたディスクを journalctl に指定します。
sudo journalctl --directory /mnt/var/log/journal --boot -1 --priority errここには、1 度はまる別の落とし穴もあります。journald はログを /run/log/journal に保持します。これはメモリ上であり、/var/log/journal が存在する場合を除きます。存在しないマシンでは、再起動後に journalctl -b -1 で表示できるものがありません。まさにログが必要になるタイミングで確認できなくなります。確認して修正します。
journalctl --disk-usage
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journaldこれで journalctl --disk-usage は /var/log/journal の下にアーカイブ済みジャーナルを報告するはずです。プレーンテキストも必要な場合は、/etc/systemd/journald.conf で ForwardToSyslog=yes を設定し、rsyslog をインストールしたままにします。
ブートのデバッグ可能性。 unit が停止すると、コンソールには 1 行だけが表示され、それ以上は何も表示されません。
[ *** ] A start job is running for Wait for Network to be Configured (1min 32s / no limit)さらに調査するためのツールは存在します。停止中には systemctl list-jobs、停止後には systemd-analyze blame と systemd-analyze critical-chain、カーネルのコマンドラインでは systemd.log_level=debug を使用できます。この異論を公平に述べるなら、init スクリプトは sh を知っている人なら上から下まで読めました。一方、停止した unit の調査には、十数個あるコマンドのどれを使うべきか知っている必要があります。これは現実のコストです。管理者ごとに 1 度発生し、多くの管理者が同時にそのコストを負担しました。
全員に適用されるデフォルト変更。 2016 年の systemd 230 では、ログアウト時に残ったユーザープロセスを終了するよう logind のデフォルトが変更されました。切り離された tmux と screen のセッションは、それらを開始したセッションが終了すると停止しました。ディストリビューションは KillUserProcesses=no を /etc/systemd/logind.conf で提供し、サポートされる回答は loginctl enable-linger <user> です。1 つのプロジェクトの 1 つのデフォルトが、数百万人の依存していた習慣を変えました。これが「userland の多くを 1 か所に集めすぎる」ことの実際の意味です。
デフォルトの依存関係はセキュリティ面になる。 2024 年 3 月、xz-utils のバックドアは Debian と Ubuntu の sshd を標的にしました。上流の OpenSSH は libsystemd にリンクしていません。これらのディストリビューションは、sshd が systemd に準備完了を通知できるようパッチで組み込みました。その結果、libsystemd がバックドアの存在する liblzma を引き込みました。準備完了プロトコル自体は、$NOTIFY_SOCKET に指定されたソケットへ送信する単一の datagram です。そのため、これにライブラリは必要ありませんでした。systemd は対策として、dlopen で圧縮ライブラリをロードするようにしました。これにより、圧縮ライブラリはデフォルトではリンクされなくなります。同じ構造を示す、関連する種類のバグもあります。2017 年には、数字で始まる User= の値が無効として扱われず、unit が失敗する代わりに root として実行されました。その結果、タイプミスが権限昇格になりました。後のバージョンでは、その unit の起動を拒否します。
systemctl プロンプトで自分のシステムの履歴を確認する
上記の各問題は、今では読めるファイル内の1つのディレクティブになっています。
- 直列起動は
After=とWants=になり、systemd-analyze critical-chainで起動を実際に妨げたものを確認できます。 - 準備完了の通知は
Type=notifyになり、サービスは要求を処理できる状態になるとREADY=1を$NOTIFY_SOCKETに書き込みます。古いデーモン向けにはPIDFile=を指定するType=forkingも残っており、PID ファイルが現れない場合はstart operation timed out. Terminating.で失敗するタイプです。誤ったタイプを選ぶと、起動したデーモンがすでに終了しているのに unit が active と報告することがあります。そのため、行を記述する前に、デーモンの実際の起動方法に対応する Type= の種類を把握しておくことが重要です。 - 監視は cgroup が担うため、
RestartSec=を指定したRestart=on-failureでラッパースクリプトを置き換えられ、StartLimitBurst=でクラッシュループが無限に続くのを防げます。 - inetd は
.serviceunit の隣に置く.socketunit になりました。 ulimitはMemoryMax=、CPUQuota=、TasksMax=になりました。- init スクリプトの
su - appuser -c行はUser=、NoNewPrivileges=yes、ProtectSystem=strictになりました。そのため、権限を持たないユーザーでサービスを実行することが unit の標準的な構成であり、追加作業ではありません。
[Unit]
Description=Example API
Wants=network-online.target
After=network-online.target postgresql.service
Requires=postgresql.service
[Service]
Type=notify
ExecStart=/usr/local/bin/exampled
Restart=on-failure
RestartSec=2
MemoryMax=512M
CPUQuota=50%
TasksMax=128
User=exampled
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
StateDirectory=exampled
[Install]
WantedBy=multi-user.targetこのファイル内の1行には、誰もが一度は犯す間違いがあります。Requires=postgresql.service は依存関係であり、起動順序ではありません。これは Postgres が失敗すると unit も失敗することを示しますが、Postgres を先に起動することは示しません。After=postgresql.service がなければ、両方が同時に起動し、サービスはまだ何も待ち受けていないポートへ接続しようとします。2つが別々に指定されるのは意図的です。片方だけが必要になる場合があるためです。ProtectSystem=strict はこのサービス用にファイルシステムを読み取り専用でマウントします。そのため StateDirectory= が指定されています。これにより、/var/lib 以下にサービスが書き込めるパスを1つ用意できます。
2026 のサーバーで 2005 年の構成を最も分かりやすく確認できるのは、Ubuntu 24.04 の SSH です。Ubuntu 24.04 は 2026 年 8 月時点で systemd 255 をリリースしています。ssh.service はデフォルトでソケットアクティベーションされます。ssh.socket が待ち受けソケットを保持し、接続が到着すると sshd が起動します。そのため /etc/ssh/sshd_config 内の Port 2222 は効果がありません。ポートを開いたプロセスは sshd ではないためです。変更先は socket unit です。
sudo systemctl edit ssh.socket[Socket]
ListenStream=
ListenStream=2222空の ListenStream= は、パッケージ提供の unit から継承した値を消去します。これを省略すると両方のポートが残ります。systemd はリストを置き換えず、末尾に追加するためです。その後で適用と確認を行います。作業中は SSH の2つ目のセッションを開いたままにしてください。
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -lntp | grep 2222ss には、sshd ではなく systemd が所有する port 2222 のソケットが1つだけ表示されるはずです。これが、20年後の VPS における launchd の設計です。以前の動作を使う場合は、sudo systemctl disable --now ssh.socket に続けて sudo systemctl enable --now ssh.service を実行すると、常駐する sshd が起動し、自身の設定から Port を再度読み込みます。
これらの詳細がどうなるかは、実行している release によって異なります。そのため、アップグレードを計画する前に、LTS の Ubuntu release と interim の Ubuntu release の違いを把握しておくとよいでしょう。複数のマシンで unit file がどこでも同一であることが、複数のサーバーを1か所から管理することが現在では shell scripting の問題ではなく configuration の問題になった理由です。また、自分で unit を記述する場合、service と timer の組み合わせによって、2009 年に init スクリプトと cron の1行に分けていた処理を実現できます。
FAQ
Linux ディストリビューションが SysV init を systemd に置き換えた理由は何ですか?
エンジニアリング上の理由が 2 つと、保守上の理由が 1 つあります。SysV init はファイル名の順序でサービスを起動していましたが、これは依存関係ではなく位置に基づく順序です。また、親プロセスから fork して離れた daemon を追跡できなかったため、古い PID ファイルによって別のプロセスを終了させることがありました。systemd は socket activation と依存関係ディレクティブで起動順序を解決し、control group でプロセスの追跡を解決しました。保守上の理由が普及の速さを決めました。すべてのディストリビューションで同じ unit file を使えるため、upstream プロジェクトは .service ファイルを提供し、ディストリビューションのメンテナーはパッケージごとに shell script を作成しなくなりました。Fedora 15 は 2011 年 5 月に切り替え、Ubuntu 15.04 は 2015 年 4 月まで残った最後の主要なディストリビューションでした。
systemd は 1 つの巨大な binary ですか?
いいえ。source tree からは、多数の独立した program が build されます。PID 1 は /usr/lib/systemd/systemd ですが、journald、logind、udevd はそれぞれ独自の binary を持つ別の process です。ls /usr/lib/systemd/ を実行すると、自分の環境で確認できます。現在も続く批判は binary のサイズではなく、release の連動に関するものです。これらの program は一緒に release され、private interface を共有するため、ディストリビューションは通常、それらを一式として取り込みます。また、GNOME などの software は logind を明示的に要求するようになりました。
systemd なしで Linux を実行できますか?
はい。Devuan は sysvinit を提供し、Gentoo は OpenRC をデフォルトにし、Void は runit を使用し、Alpine は OpenRC と組み合わせた busybox init を使用し、Slackware は BSD style の script を維持しています。代わりに、互換性への対応が必要です。logind を要求する desktop software には elogind が必要です。これは systemd の logind を standalone package として保守するものです。また、現在では server software の多くが .service file だけを提供するため、startup script を自分で作成して保守する必要があります。
journal が通常の text file ではなく binary 形式なのはなぜですか?
journald は index 付きで structured field を保存するためです。これにより、unit ごとの filtering、priority filtering、そして送信元 program が偽装できない metadata を利用できます。journald は log line を信頼せず、自身で unit、cgroup、実際の UID を記録します。その代わり、rescue system から読む場合も含め、読み取りには journalctl が必要です。rescue system では、マウントした disk を journalctl --directory /mnt/var/log/journal で指定します。text 形式も必要な場合は、/etc/systemd/journald.conf で ForwardToSyslog=yes を設定します。
/etc/init.d script の編集に代わる方法は何ですか?
drop-in file を使用します。/usr/lib/systemd/system/ にある unit を直接編集しないでください。package upgrade によって上書きされるためです。sudo systemctl edit nginx.service を実行すると、systemd は /etc/systemd/system/nginx.service.d/override.conf を作成し、packaged unit に重ねて適用します。systemctl cat nginx.service は統合後の結果を表示し、systemd-delta はその machine 上のすべての override を一覧表示します。手動で編集した後は、sudo systemctl daemon-reload を実行してください。実行しないと、次の command で Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk. と表示されます。