systemdのアーキテクチャとユニット・依存解決
systemctlの裏で何が動くかが構造から腑に落ちます。PID1マネージャ・ユニット依存グラフ・socket活性化・cgroup隔離・journald/logindの役割を内部まで追い、サービスの挙動を予測できます。
- 1.systemdのPID1はマネージャ本体で、全ユニットを有向グラフとして解決し、順序制約のないものを並列起動します。
- 2.依存はRequires/Wantsで有無を、After/Beforeで順序を表し、socket活性化はソケットを先に開くことで起動順序依存そのものを消します。
- 3.各サービスは専用cgroupに束ねられて隔離・追跡され、ログはjournald、ユーザーセッションはlogindが構造的に管理します。
PID1としてのsystemd:マネージャの正体
systemd は単なる init 置き換えではなく、システム全体の状態を管理する マネージャ です。SysV init から systemd への系譜と「なぜ並列起動が速いか」は initシステムの系譜 で扱いました。本稿はその先、systemd の 内部構造 ——ユニットの依存解決、socket活性化、cgroup隔離、補助デーモン群——を分解します。
PID 1 として起動した systemd は、二つの空間で動きます。システムインスタンス(PID 1、/usr/lib/systemd/system/ などを読む)と、ログインしたユーザーごとに起動する ユーザーインスタンス(systemd --user)です。両者は構造が同じで、対象が異なります。
systemd の状態は すべてユニット(unit)として表現 されます。ユニットはメモリ上のオブジェクトで、それぞれが小さな状態機械を持ちます。サービスなら inactive → activating → active → deactivating と遷移し、Restart= の条件で failed に落ちて再起動されます。systemctl はこの状態機械を D-Bus 経由 で操作するクライアントに過ぎません。
systemctl start foo.service の流れ:
systemctl → (D-Bus) → systemd PID1 のマネージャ
→ foo.service ユニットの load(未ロードなら .service を解析)
→ 依存ユニットを連鎖的に load
→ トランザクションを構築(後述)
→ ジョブをキューに積み、依存順に実行
ユニットは複数ディレクトリから検索され、優先順位は /etc/systemd/system/(管理者)> /run/systemd/system/(実行時)> /usr/lib/systemd/system/(配布元)です。さらに foo.service.d/*.conf という ドロップイン で元ファイルを書き換えずに一部ディレクティブだけ上書き・追記できます。systemctl cat foo が最終的な合成結果を、systemctl show foo が解決後の全プロパティを表示します。
ユニット種別と依存グラフ
ユニットには種別があり、拡張子で区別されます。中心となるのは次の4種です。
| 種別 | 表すもの | 代表的な使いどころ |
|---|---|---|
| .service | プロセス/デーモンの管理 | 常駐サービス、ワンショット処理 |
| .socket | 待ち受けソケット | socket活性化による遅延起動 |
| .timer | 時刻・周期トリガ | cronの代替(対応serviceを起動) |
| .target | 複数ユニットの束(同期点) | ランレベル後継、起動段階の名前付け |
依存関係は 2つの独立した軸 で宣言します。これが systemd 依存解決の核心です。
- 依存の有無(requirement):
Requires=(強い依存。相手が起動できないと自分も失敗)、Wants=(弱い依存。相手が失敗しても続行)、Conflicts=(同時にactiveにできない排他)。 - 順序(ordering):
After=/Before=のみが起動・停止の 時間順序 を決めます。
重要なのは、依存の有無と順序が直交している ことです。Requires=bar.service だけ書いて After=bar.service を書かなければ、bar は必要だが 順序は規定されず並列に 起動します。「必要かどうか」と「どの順か」を別々のディレクティブにしたことで、systemd は依存と順序をそれぞれ独立に最適化できます。
foo.service(要点):
[Unit]
Requires=bar.service # bar が必要(起動できないと foo も失敗)
After=bar.service # かつ bar の後に起動する
[Service]
ExecStart=/usr/sbin/foo
Restart=on-failure
[Install]
WantedBy=multi-user.target # enable 時に target の .wants/ にリンクされる
systemd はロードされた全ユニットから 有向グラフ を作ります。After/Before の順序辺で前後が決まっていないユニット同士は 同時に起動 されるため、起動時間は各処理の総和ではなく、依存チェーン上の 最長経路(クリティカルパス) に近づきます。systemd-analyze critical-chain がこの経路を、systemd-analyze plot が時系列を可視化します。
systemctl start は単発のジョブではなく トランザクション を作ります。要求されたユニットと、その依存に必要なジョブ(start/stop/restart)をまとめて1つの集合にし、順序辺で整合性を検証してからアトミックに実行します。途中で 順序の循環(After のループ) が見つかると、systemd は循環を構成する辺のうち順序辺を1本削って循環を破壊し、警告を出して起動を続けます。Requires の循環など破壊不能な矛盾はトランザクション全体を拒否します。
socket活性化:起動順序依存を消す
socket活性化(socket activation)は systemd の設計思想を最もよく表す機構です。狙いは 「Aが起動し終わるまでBを待つ」という順序依存そのものを消す ことです。
仕組みはこうです。systemd は service より先に .socket ユニットだけを開き、listen() した待ち受けソケットを 自分(PID 1)が保持 します。クライアントが接続すると、カーネルは接続を accept キューに溜めます。そこで初めて systemd が対応する service を起動し、開いておいたソケットのファイルディスクリプタを fd 3 番から順に渡します。service 側は環境変数 LISTEN_FDS(渡された fd の本数)と LISTEN_PID(宛先 PID)を見て、その fd を accept() します。
foo.socket → foo.service の連携:
foo.socket: [Socket] ListenStream=/run/foo.sock
foo.service: [Service] ExecStart=/usr/sbin/foo # ソケットはfdで受領
boot 時: systemd が /run/foo.sock を listen(serviceは未起動)
接続到来: カーネルが接続をバッファ → systemd が foo.service を起動
→ fd 3 を渡す(LISTEN_FDS=1, LISTEN_PID=foo の PID)
foo 側: 既存の fd を accept() してリクエスト処理
これが効く理由は2つあります。第一に 並列起動の徹底:依存先のソケットは boot 初期に全部開けるので、サービス本体は互いの起動完了を待たずに「相手のソケットへ繋ぎに行く」だけでよく、溜まった接続は相手が後から処理します。第二に オンデマンド起動:接続が来るまで service を起動しないので、めったに使われないデーモンを常駐させずに済みます。fd を別プロセスへ渡すこの仕組みの土台は Unixドメインソケットとfd受け渡し で、accept キューの実体は ソケットとTCP接続のカーネル状態管理 で扱っています。
socket活性化は service 側が sd_listen_fds 規約に対応している ことを前提とします。自前で socket()/bind()/listen() してしまう旧来デーモンには LISTEN_FDS を渡しても無視されます。また、再起動でソケットを失わないためには .socket を生かしたまま .service だけ再起動する設計が必要で、これにより デーモン更新中も接続を取りこぼさない(接続はソケットに溜まり続ける)という利点も得られます。
cgroupによるサービス隔離
systemd は各ユニットを 専用の cgroup に束ねます。これは追跡と資源制御の両方を構造的に解決する中核機構です。
service を起動するとき、systemd は system.slice/foo.service のような cgroup を作り、ExecStart のプロセスをそこへ入れます。子が二重 fork してデーモン化しても、fork() の子孫は 同じ cgroup に属したまま です。cgroup はカーネルがプロセス集合を列挙できる仕組みなので、systemd は PID ファイルに頼らず、cgroup.procs を読むだけでサービスの全プロセスを正確に把握できます。
cgroup 階層(slice によるツリー化):
-.slice # ルート
├ system.slice # システムサービス群
│ ├ foo.service # foo の全プロセスがここに集約
│ └ sshd.service
├ user.slice # ユーザーセッション群
│ └ user-1000.slice
└ machine.slice # コンテナ/VM(machinectl)
systemctl stop foo:
→ foo.service の cgroup 内の全 PID に SIGTERM
→ TimeoutStopSec 経過後、残存プロセスへ SIGKILL(取り逃しゼロ)
この「サービス=cgroup」対応により、MemoryMax=・CPUQuota=・TasksMax= といった 資源制限をユニット単位で 課せます。systemd は cgroup を3階層の slice で組織化し、.slice 単位でも制限を委譲できます。cgroup v2 の統一階層・委譲・no-internal-process 制約は cgroup v2の階層モデルとコントローラ で詳述しています。プロセスのシグナル配送の正確な意味づけは シグナル配送の内部 も参照してください。
Type= は systemd が「いつ起動完了とみなすか」を決めます。simple(ExecStart のプロセスを本体とみなし即 active)、forking(古典的デーモン、親が exit したら active。PIDFile 併用)、notify(sd_notify で READY=1 を受けて active)、oneshot(プロセス終了をもって完了、初期化処理向け)。notify は「準備完了」を service 自身が宣言できるため、依存サービスの起動順序が最も正確になります。
補助デーモン:journaldとlogind
systemd は PID 1 本体に加え、役割を分担する補助デーモン群を持ちます。代表が journald と logind です。
systemd-journald は構造化ログの集約デーモンです。従来の syslog がテキスト行を /var/log/ に追記するだけだったのに対し、journald は各ログを キー・バリューのフィールド集合 を持つバイナリエントリとして保存します。エントリには MESSAGE や PRIORITY のほか、_PID・_UID・_SYSTEMD_UNIT・_BOOT_ID などのメタデータが付与されます。とくに _ で始まるフィールドはカーネルや systemd が 信頼できる形で付与 する(アプリが詐称できない)一方、MESSAGE や PRIORITY は送出元プロセスが指定する値です。
journald のログ源と問い合わせ:
入力: /dev/log (syslog互換) ・ /dev/kmsg (カーネル) ・ stdout/stderr
(各 service の標準出力は自動でjournalに接続される)
保存: /run/log/journal(揮発)または /var/log/journal(永続)
journalctl -u foo.service # ユニット単位で抽出
journalctl -b -p err # 今回ブートのerror以上
journalctl _SYSTEMD_UNIT=foo.service _PID=1234
メタデータがインデックス化されているため、「特定ユニットの今回ブートのエラーだけ」を テキスト grep ではなくフィールド検索 で取り出せるのが本質的な違いです。service の stdout/stderr が自動で journal に流れるのも、cgroup によりどのプロセスがどの service かを systemd が知っているからです。
systemd-logind はユーザーセッションとシートを管理します。ユーザーがログインするたびに session と、ユーザー単位の user-NNN.slice(cgroup)を作り、ログイン中のプロセスをそこに束ねます。これにより「ユーザーがログアウトしたら、そのセッションの全プロセスを確実に終了させる」「アクティブセッションにだけデバイス(GPU・音声)へのアクセス権を与える」といった制御が、ポーリングではなく cgroup の所属で実現します。電源・サスペンド・画面ロックの抑止(inhibitor lock)も logind が仲裁します。
| デーモン | 責務 | 管理単位 |
|---|---|---|
| systemd (PID1) | ユニットの依存解決と起動・監視 | unit / job / slice |
| systemd-journald | 構造化ログの集約と問い合わせ | journalエントリ(メタデータ付き) |
| systemd-logind | ログインセッションとシート管理 | session / seat / user slice |
| systemd-udevd | デバイス検出とdevノード生成 | device unit との連携 |
全体像を一枚で
systemd アーキテクチャの俯瞰:
[systemctl/loginctl/journalctl] ← クライアント
│ D-Bus
▼
┌───────────────────────────────┐
│ systemd (PID 1) マネージャ │
│ ・unit のロードと状態機械 │
│ ・依存グラフ → トランザクション│
│ ・並列起動(クリティカルパス) │
└───────────────────────────────┘
│ 各serviceを専用cgroupへ
▼
cgroup ツリー(slice/service) ──→ 追跡・資源制限・確実な停止
│ │stdout/stderr
.socket(先に開く)→ 順序依存を解消 ▼
journald(構造化ログ)
logind: session/seat を user slice で管理
押さえる点は4つです。(1) 依存は Requires/Wants(有無)と After/Before(順序)が 直交 し、両方書かないと「必要だが並列」になる。(2) socket活性化は ソケットを先に開いて fd を渡す ことで起動順序依存を消す。(3) 各サービスは 専用 cgroup に束ねられ、二重 fork しても取り逃さず停止・資源制限できる。(4) journald はログを メタデータ付き構造化エントリ で保存し、ユニット単位のフィールド検索を可能にする。
まとめ
- systemd の PID 1 はマネージャ本体 で、状態をすべてユニットとして持ち、
systemctlは D-Bus クライアントに過ぎない。 - 依存は 有無(Requires/Wants)と順序(After/Before)が直交 し、systemd は全ユニットを有向グラフとして トランザクション で解決、順序制約のないものを並列起動してクリティカルパスまで起動を縮める。
- socket活性化 はソケットを先に開いて fd を渡すことで、起動順序依存とオンデマンド起動を同時に解決する。
- 各サービスは 専用 cgroup に束ねられ、確実な追跡・停止とユニット単位の資源制限を構造的に実現する。
- journald は構造化ログを、logind はセッションを cgroup ベースで管理し、systemd を単なる init から「システム状態の統合マネージャ」へ押し上げている。
systemd 以前との対比は initシステムの系譜、資源制御の詳細は cgroup v2の階層モデルとコントローラ も合わせてどうぞ。
OS Article
systemdのアーキテクチャとユニット・依存解決を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
systemd
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
依存はRequires/Wantsで有無を、After/Beforeで順序を表し、socket活性化はソケットを先に開くことで起動順序依存そのものを消します。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「systemd / cgroup」に近いか確認する。
- 強みである「systemdのPID1はマネージャ本体で、全ユニットを有向グラフとして解決し、順序制約のないものを並列起動します。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。