initシステムの系譜(SysV init・Upstart・systemd)
なぜ起動はsystemdで速くなったのか。PID1の役割から、SysV initの逐次起動、Upstartのイベント駆動、systemdの依存解決と並列化まで、設計の進化を系統で整理し起動を内部から読み解けるようにします。
- 1.PID1は全プロセスの祖先かつ孤児の引き取り手で、終了するとカーネルパニックになる特別なプロセスです。
- 2.SysV initはランレベルごとにスクリプトを直列実行し、Upstartはイベント駆動、systemdは依存グラフの並列起動へと進化しました。
- 3.systemdはサービスをcgroupで束ねてプロセス群を確実に追跡・制御し、socket活性化で起動順序問題そのものを解消します。
PID1とは何者か
カーネルがブートを終えて最初に exec するユーザー空間プロセスが PID 1、すなわち init です。ブート段でどうやって PID 1 にたどり着くかは ブートチェーンの内部 で扱いました。本稿はその先、PID 1 が起動後に担う サービス管理の設計 が、SysV init から Upstart を経て systemd へどう進化したかを系統でたどります。
PID 1 は通常のプロセスと違い、カーネルから特別扱いされます。要点は3つです。
- 全プロセスの祖先:以後生成されるプロセスはすべて PID 1 の子孫になります。
- 孤児の引き取り手(reaper):親が先に死んだ子は PID 1 に再ペアレントされ、PID 1 が
wait()してゾンビを刈り取ります(ゾンビプロセスと孤児プロセス参照)。 - 終了が許されない:PID 1 が exit するとカーネルは
Kernel panic - Attempted to kill init!を出して停止します。
カーネルは PID 1 に対し、ハンドラを登録していないシグナルの既定動作を適用しません。つまり SIGTERM や SIGKILL を送っても、init 側が明示的にハンドラを持たない限り無視されます。これは「誤って init を殺してシステムを落とす」事故を防ぐための保護で、init 自身が処理すると決めたシグナルだけが効きます。
SysV init:ランレベルと逐次起動
最初の系統は、System V 由来の SysV init です。中核概念は ランレベル(runlevel) で、システムの動作状態を 0〜6 の番号で表します。
| ランレベル | 意味(一般的なRed Hat系) |
|---|---|
| 0 | 停止(halt) |
| 1 / S | シングルユーザー(保守モード) |
| 3 | マルチユーザー+ネットワーク(CUI) |
| 5 | マルチユーザー+GUI |
| 6 | 再起動(reboot) |
init は /etc/inittab を読み、initdefault で指定された既定ランレベルへ遷移します。各ランレベルには /etc/rc.d/rcN.d/ というディレクトリが対応し、その中に起動スクリプトへの シンボリックリンク が並びます。リンク名の規則が起動の本質を決めます。
/etc/rc3.d/ の中身(例)
S10network -> ../init.d/network # S=start、10=順序番号
S12syslog -> ../init.d/syslog
S80httpd -> ../init.d/httpd
K30httpd -> ../init.d/httpd # K=kill(停止時に使う)
rc スクリプトの動作(擬似コード):
for link in sort(Sxx*): # 番号の昇順に
run link start # 1本ずつ直列に実行
wait_until_exit() # 終わるまで次に進まない
ここに SysV init の本質的な限界が2つあります。第一に 逐次(シリアル)実行 です。S10 が完了するまで S12 は始まらないため、起動時間は各スクリプトの所要時間の 総和 になります。第二に 依存関係が番号で暗黙表現される ことです。「network の後に httpd」を S10 と S80 の番号差で表すしかなく、依存は人間が番号付けで管理する暗黙の約束に過ぎません。番号が衝突したり順序を読み違えたりすれば、起動順序のバグになります。
各サービスは /etc/init.d/foo {start|stop|status} というシェルスクリプトで、start) の中で自前で & によるデーモン化や PID ファイル書き込みを行います。プロセスの追跡は PID ファイル頼みで、子プロセスが二重 fork して逃げると init は追えなくなります。「stop したのにプロセスが残る」「status が嘘をつく」といった問題の根はここにあります。
Upstart:イベント駆動への転換
逐次起動の遅さと、ホットプラグ(USB 接続やネットワーク到達)のような 非同期な出来事に対応できない 問題を解こうとしたのが、Ubuntu が開発した Upstart(2006年〜)です。発想を イベント駆動 に変えました。
SysV init が「ランレベルという固定の状態へ順に進む」モデルだったのに対し、Upstart は 「イベントが発生したら、それを待っているジョブを起動する」 モデルです。ジョブ定義(/etc/init/foo.conf)に、どのイベントで起動・停止するかを宣言します。
# /etc/init/foo.conf(概念例)
start on (started networking) # networking が起動したら開始
stop on runlevel [016] # ランレベル 0/1/6 で停止
respawn # 落ちたら自動再起動
exec /usr/sbin/foo --no-daemon # フォアグラウンドで実行
イベント(startup、started X、デバイス接続など)が発火すると、それを start on で待つジョブが起動します。あるジョブの起動完了が次のイベントになり、連鎖的にシステムが立ち上がります。依存を番号ではなく 「何の後に」を名前で宣言 できる点が前進で、独立なジョブは並列に起動できます。respawn による自動再起動も、SysV では自前実装だった機能を宣言一行に畳み込みました。このイベント/非同期トリガという発想は、I/O 多重化の世界の流れ(イベント駆動I/Oモデルの系譜)とも通じます。
イベント駆動は強力ですが、「このジョブを動かすには結局どのイベント群が必要か」を全体俯瞰しにくいという弱点があります。start on A and B の連鎖を追わないと依存の全体像が見えず、あるイベントが発火しないと依存ジョブが永久に起動しないデッドロック的状況も起きえます。依存を「到達したい状態」から逆算する発想は、次の systemd で結実します。
systemd:依存グラフと並列起動
現在の主流である systemd(2010年〜)は、設計の軸を イベントの連鎖 から 依存グラフの解決 へ再び移しました。すべてを unit という統一概念で表します。
| unit種別 | 表すもの |
|---|---|
| .service | デーモン/プロセス(起動・停止・再起動を管理) |
| .socket | 待ち受けソケット(後述のsocket活性化) |
| .mount / .automount | ファイルシステムのマウント点 |
| .target | 到達したい状態(ランレベルの後継、複数unitの束) |
| .timer | 時刻・周期起動(cronの代替) |
各 service unit は依存関係を 明示的なディレクティブ で宣言します。中心は2軸です。Requires=/Wants= が 「何を一緒に必要とするか」(依存の有無)を、After=/Before= が 「どの順で起動するか」(順序)を表します。この2軸が独立している点が重要で、Requires=(必要だが順序は問わない)と After=(順序だけ規定)を分けることで、systemd は依存と順序を別々に最適化できます。
# foo.service(概念例)
[Unit]
Requires=network-online.target # これが起動できないと foo も失敗
After=network-online.target # 起動順序:network の後
[Service]
ExecStart=/usr/sbin/foo --foreground
Restart=on-failure
[Install]
WantedBy=multi-user.target # この target に組み込まれる
systemd はこれらの宣言から 有向グラフ を構築し、トポロジカルに解決します。順序辺(After)で前後が定まっていない unit 同士は 同時に起動 されます。結果、起動時間は SysV のような所要時間の総和ではなく、依存チェーン上の最長経路(クリティカルパス) に近づきます。これが「systemd は速い」の正体です。
依存解決と並列起動(擬似コード):
graph = build_dependency_graph(all_units) # After/Before を辺に
ready = units_with_no_pending_predecessor(graph)
while ready not empty:
for u in ready (in parallel): # 順序制約のないものは同時に
start(u)
ready = newly_unblocked_units(graph) # 完了で次段が解放
cgroup統合:プロセス群を確実に追跡する
systemd のもう一つの本質的な前進が cgroup との統合 です。SysV init が PID ファイル頼みでプロセスを見失っていた問題を、systemd は構造的に解決しました。
systemd は各 service を起動するとき、その service 専用の cgroup を作り、ExecStart で起動したプロセスをその中に入れます。子が二重 fork してデーモン化しようとも、fork() で生まれた子孫は 同じ cgroup に属したまま です。cgroup はプロセスの集合をカーネルが追跡する仕組みなので、init はもはや PID を当てずっぽうで追う必要がありません。
/sys/fs/cgroup/.../foo.service/
cgroup.procs: 1234 1240 1305 ... # foo の全プロセスをカーネルが列挙
systemctl stop foo:
→ foo.service の cgroup 内の全 PID に SIGTERM
→ 猶予後に残存プロセスへ SIGKILL # 取り逃しゼロ
この「サービス=cgroup」という対応により、systemctl stop は 取り残しなくプロセス群全体を止められ、MemoryMax= や CPUQuota= といった リソース制限をサービス単位で課せます。cgroup そのものの仕組みは 名前空間と cgroups、v2 での階層モデルは cgroup v2の階層モデルとコントローラ を参照してください。
systemd は service に先立って .socket unit だけを開き、待ち受けを始めます。クライアントが接続するとカーネルがその接続をバッファし、systemd が初めて対応 service を起動して、開いておいたソケットを fd として渡します。すると「A が起きるまで B を待つ」という順序依存が不要になります。B は A のソケットへ繋ぎに行けば、A の起動完了を待たずに送信でき、A は後から立ち上がって溜まった接続を処理します。依存を順序ではなく 遅延起動 で解く発想です。
三世代を一枚で比較する
init システムの系譜(年代と分岐):
1983 SysV init ── ランレベル+rcN.d、逐次・直列起動
│ 依存は番号(Sxx/Kxx)で暗黙表現
│
2006 Upstart (Ubuntu) ── イベント駆動、start on / stop on
│ 非同期トリガに対応、並列起動へ
│
2010 systemd ── unit+依存グラフ、並列起動
cgroup統合・socket活性化・target
| 観点 | SysV init | Upstart | systemd |
|---|---|---|---|
| 設計モデル | ランレベル(状態遷移) | イベント駆動 | 依存グラフ(unit) |
| 起動方式 | 逐次・直列 | イベント連鎖で部分並列 | 依存解決による並列 |
| 依存の表現 | 番号で暗黙 | start on で明示(局所的) | Requires/After で明示(全体) |
| プロセス追跡 | PIDファイル頼み | 親子追跡の改善あり | cgroupで確実に把握 |
| 設定の記述 | シェルスクリプト | 宣言的なジョブ定義 | 宣言的なunitファイル |
| 起動時間の目安 | 各処理時間の総和 | 総和より短縮 | クリティカルパス相当 |
押さえるべき分岐は3点です。(1) SysV は「直列・番号で暗黙依存」、systemd は「並列・依存グラフで明示」。(2) systemd が速い理由は依存チェーンの最長経路まで起動時間を縮められるから。(3) systemd がプロセスを取りこぼさないのは service を cgroup で束ねるから。Upstart は両者をつなぐ「イベント駆動」の中間世代、と位置づけると系譜が一本につながります。
まとめ
- PID 1 はカーネルが最初に起動する特別なプロセスで、全プロセスの祖先・孤児の刈り取り手であり、終了するとカーネルパニックになる。
- SysV init はランレベルと
rcN.dで 逐次・直列 に起動し、依存を番号で暗黙表現するため遅く壊れやすかった。 - Upstart は イベント駆動 へ転換し、非同期トリガと宣言的な依存・自動再起動を導入して部分並列化を実現した。
- systemd は 依存グラフ を解いて並列起動し、起動時間をクリティカルパスまで縮めた。
- systemd は service を cgroup で束ねてプロセス群を確実に追跡・制御し、socket活性化 で起動順序問題そのものを解消した。
PID 1 に至るまでの前段は ブートチェーンの内部、孤児プロセスの引き取りは ゾンビプロセスと孤児プロセス も合わせてどうぞ。
OS Article
initシステムの系譜(SysV init・Upstart・systemd)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
init
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
SysV initはランレベルごとにスクリプトを直列実行し、Upstartはイベント駆動、systemdは依存グラフの並列起動へと進化しました。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「init / systemd」に近いか確認する。
- 強みである「PID1は全プロセスの祖先かつ孤児の引き取り手で、終了するとカーネルパニックになる特別なプロセスです。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。