カーネルの初期化が終わった後

最初のプロセスの仕事

プログラムを起動すると、Linuxカーネルは、実行に使うメモリや開いているファイルなどをまとめて管理します。この実行中のプログラムと実行環境をプロセスプロセス実行中のプログラムに、仮想アドレス空間やファイルディスクリプタ表などの実行環境を持たせた管理単位。と呼びます。

Linuxカーネルは各プロセスへprocess IDという番号を割り当てます。略称がPIDです。ユーザー空間で最初に動くプロセスには1が割り当てられるため、このプロセスをPID 1PID 1process IDが1で、孤児になったプロセスの終了状態を回収するプロセス。と呼びます。

親を失った子プロセスは、最終的にPID 1へ引き取られます。PID 1はその終了状態を回収できます。Linuxの起動時に開始したPID 1が終了するとカーネルはpanicするため、システムが動いている間はPID 1も動き続けます。これらは、カーネルがPID 1に適用する規則です。

Linuxの起動後には、通信の待ち受け、ログの記録、時刻の同期などを担うプログラムが動きます。利用者が画面から直接起動しなくても、常時または要求に応じて動くこうした処理を、この章ではサービスサービス利用者やほかのプログラムへ提供される処理や機能を、この教材でまとめて指す言葉。と呼びます。サービスを起動し、状態を監視し、必要に応じて停止するソフトウェアがサービスマネージャーです。

現在の主要なLinuxディストリビューションでは、systemdsystemdPID 1として起動処理とサービス管理を担う、ユーザー空間のソフトウェア群。がPID 1として動き、サービスマネージャーの役割を担います。systemdはユーザー空間のソフトウェアです。SysV init、OpenRC、runitなど、別のinitを使う構成もあります。

カーネルがPID 1に適用する規則

子孫の終了状態を回収する

孤児になった子孫の親になります。ほかのプロセスとは、シグナルの配送と終了に関する規則も異なります。

systemdが担当する処理

起動とサービス管理

必要なサービスを起動し、動作中のプロセスを追跡します。サービスごとにCPUとメモリの配分や、使える権限も設定します。

10.1
カーネルからユーザー空間へ処理を渡す

電源投入からPID 1まで

電源投入直後は、マザーボードのファームウェアが起動するデバイスを選びます。続いて、OSを読み込むプログラムであるブートローダーがLinuxカーネルをRAMへ配置します。カーネルはハードウェアを初期化し、ユーザー空間で最初に動くプログラムを探します。

Linuxのディレクトリツリーは、一番上にあるディレクトリを起点にします。このディレクトリをrootと呼び、パスでは/と表します。rootを含むファイルシステムがrootファイルシステムです。initramfsinitramfs起動中に一時的なrootファイルシステムとして使い、ストレージを読み出す準備をする小さなユーザー空間。は、起動中に一時的なrootファイルシステムとして使われます。ストレージを読み出す準備が終わると、PID 1は/usrや/etcを含むrootファイルシステムへ切り替えます。

電源投入直後

ファームウェア

起動するデバイスを選び、ブートローダーへ処理を渡します。

10.2
PID 1が引き取る

終了した子プロセスを回収する

終了した子プロセスの情報を誰も受け取らないと、カーネルのプロセス表に終了状態が残り続けます。数が増えれば、新しいプロセスを作る妨げになります。

子プロセスが終了すると、カーネルは終了コードやCPU使用時間などを保持し、親へSIGCHLDを送ります。親がwait系のシステムコールで情報を読むまで残る状態をゾンビプロセスゾンビプロセス実行は終わっているが、親が終了状態をまだ受け取っていないプロセス。と呼びます。親が先に終了していれば、最も近いsubreapersubreaper孤児になった子孫プロセスを、PID 1より手前で引き取るよう指定されたプロセス。かPID 1が子を引き取ります。親がSIGCHLDをSIG_IGNに設定するか、SA_NOCLDWAITを使う場合は、カーネルが終了した子を自動で回収します。

通常の親子関係

親が子の終了状態を受け取る

子が終了すると、親はSIGCHLDを受け取り、wait系のシステムコールで終了状態を回収します。

ホストとは別のプロセス一覧を使う

コンテナ内のPID 1

同じLinux上でコンテナを動かす場合、コンテナ内のプロセスには、ホスト上の全プロセスを含まない独立したプロセス一覧を見せます。その一覧の中でも、終了した子を回収するPID 1が必要です。

プロセスから見えるPIDの範囲を分ける機能をPID namespaceと呼びます。PID namespaceは、第07章で説明したnamespacenamespacePID、マウント、ネットワーク、ホスト名などの参照範囲を、プロセス群ごとに分ける仕組み。の一種です。Linuxの起動時に作られる範囲とは別に、コンテナを起動するソフトウェアはコンテナ用のPID namespaceを作れます。その中で最初に動くプロセスにはPID 1が割り当てられますが、ホスト側から見ると同じプロセスに別のPIDがあります。

Linux起動時のPID namespace

システム全体のPID 1

このPID 1が終了すると、カーネルは処理を続けられないためpanicします。

コンテナ用のPID namespace

最初のプロセスがPID 1になる

このPID 1が終了すると、カーネルは同じPID namespaceに残るプロセスへSIGKILLを送ります。その後、このPID namespaceには新しいプロセスを作れません。

シグナル

通常のプロセスとは配送規則が異なる

同じPID namespaceのプロセスは、PID 1が処理を登録したシグナルだけを送れます。親側のPID namespaceからは、SIGKILLとSIGSTOPを送れる例外があります。

10.3
PID 1の仕事からサービス管理へ

サービスと起動順序を管理する

主要なLinuxディストリビューションでは、PID 1として動くsystemdが、起動時に多数のサービスを開始し、動作中も停止や再起動を管理します。ここでは、Linuxカーネルが実行するものと、systemdが管理するものを先に分けます。

用語が属する層を分ける

実行されるものと、systemdが管理するもの

一般的なコンピューター用語

OSが扱う実行単位と、提供する処理の呼び方

提供される処理や機能サービス

通信の待ち受けやログの記録など、利用者やほかのプログラムが利用する処理や機能を指します。

処理を担うプロセスの呼び方daemon

システムを監視したり、ほかのプロセスへ機能を提供したりするプロセスです。サービスと一対一になるとは限りません。

サービスは「何を提供するか」を表します。daemonは、その処理を担うプロセスの役割を表します。

OSが実行環境をまとめる単位プロセス

実行中のプログラムと、仮想アドレス空間や開いているファイルなどをまとめた実行環境です。

同じプロセスに属するスレッド

スレッドは同じプロセスのメモリやファイルを共有します。Linuxカーネルは各スレッドをタスクとして管理し、CPUへ割り当てます。

スレッドA タスク / TID 201スレッドB タスク / TID 202
systemd固有の用語

unitの状態を管理する

一時的な操作要求job

start、stop、reloadなど、unitの状態を変える要求です。処理が終わればjobも完了します。

systemdが保持する管理対象service unit

起動するプログラム、依存関係、再起動条件などを保持し、起動したプロセスを監視します。

unitのcgroupで追跡主プロセス補助プロセス
Linuxカーネルが実行するのはタスクです。systemdはunitの状態と、unitに属するプロセスを管理します。

systemdは、起動、停止、監視する対象と、その依存関係をunitunitsystemdが起動、停止、監視、依存関係の管理を行う対象。として保持します。プロセスを管理するservice unitのほか、待ち受けsocket、mount point、timer、複数のunitをまとめるtargetなども同じ依存関係のグラフへ置けます。

unitを起動する要求が届くと、systemdは依存関係をたどり、同時に必要となるunitの操作をtransactionへまとめます。unitごとの開始や停止の要求がsystemd jobsystemd jobunitをstart、stop、reloadする要求を、systemdが実行順序とともに管理する一時的な単位。です。jobは実行順序を待つキューへ入り、順序を指定していない処理は並行して進められます。

WantsとRequires

一緒に起動するunitを指定する

どちらも、ほかのunitを起動対象へ加えます。Wantsでは相手の起動に失敗しても自分を起動できます。Requiresでは、相手が明示的に停止・再起動されると自分も停止・再起動されます。Afterも指定した相手の起動に失敗すると、自分を起動しません。開始順序はWantsやRequiresだけでは決まりません。

10.4
主プロセスだけでなく、サービス全体を追う

service unitごとにcgroupを作る

従来のdaemondaemonデーモンシステムを監視したり、ほかのプロセスへ機能を提供したりするプロセス。は、起動元のプロセスから独立するためにforkを重ね、主プロセスのPIDをファイルへ記録することがありました。補助プロセスが増えると、一つのPIDだけでは、停止すべきプロセスやサービス全体の使用量を判断できません。

第07章で説明したcgroupは、複数のプロセスを階層的なグループとして管理するカーネルの仕組みです。systemdはservice unitごとにcgroupを作ります。forkした子も同じcgroupへ残るため、サービス全体を停止し、CPUやメモリの使用量をまとめて制御できます。

main PIDを記録する

親子関係だけでは、サービスの全プロセスを追い続けにくい

従来のdaemonは、起動したプロセスからforkして親子関係を切り離すことがありました。管理側はPIDファイルなどで主プロセスを特定します。

プロセスの親子関係とは別の境界

service unitとcgroupを対応づける

systemdが起動したプロセスは、そのservice unit用のcgroupへ置かれます。カーネルが保持する所属情報を使うため、PIDファイルだけに依存せず管理できます。

子プロセスが増えても追跡できる

forkした子孫も同じcgroupに残る

プロセスの親子関係やmain PIDが変わっても、明示的に移動しない限りcgroupの所属は引き継がれます。

プロセス群を一つの対象として扱う

停止と資源配分をサービス全体へ適用する

シグナル、CPUの配分、メモリ使用量などを、cgroupに属するプロセス群へまとめて適用できます。

systemdが作るcgroup階層では、system.sliceなどのsliceが複数のunitをまとめます。serviceとscopeは、その下でプロセスを保持します。この形は、cgroup v2で原則として内部ノードへプロセスを置かない規則に対応しています。

cgroup階層は、複数の管理プログラムが同じ場所を書き換えると整合を保てません。systemdは階層全体を管理し、コンテナを起動するソフトウェアなどへ一部を任せる場合は、Delegate=で操作できる部分木を明示します。

第07章のweb.serviceとbatch.serviceの階層を見直す

10.5
サービスの起動前に待ち受けを始める

socket activationで接続とプロセス起動を分ける

通常のネットワークサービスは、起動後にsocket()、bind()、listen()を呼んで接続を待ちます。この順序では、サービスの起動が終わるまで接続を受けられません。使われる可能性が低いサービスも、待ち受けを始めるためだけに常時起動しておくことになります。

socket activationsocket activationサービスより先に待ち受けsocketを用意し、接続を契機にサービスを起動する方法。では、systemdが待ち受けsocketを先に用意します。接続はカーネルのlisten queueで待ち、systemdは対応するserviceを起動して、そのsocketのファイルディスクリプタを渡します。サービスの起動中も接続を保持でき、実際に接続された時点でプロセスを起動する構成も選べます。

待ち受けを先に始める

socketを用意

systemdがsocket()、bind()、listen()を呼び、待ち受けを始めます。

10.6
プログラムを実行する前に境界を作る

unitの設定から実行環境を作る

ネットワークサービスが侵害された場合でも、書き換えられるファイル、使えるシステムコール、取得できる権限をあらかじめ狭めておけば、同じコンピューター内のほかの処理へ被害が広がりにくくなります。

systemdはservice unitから起動条件を読み取り、子プロセスをcgroupへ配置します。続いて、必要なnamespaceを作り、capabilityを減らし、seccompのシステムコールフィルターなどを設定してから、サービスのプログラムをexecve()します。プログラムのコードが動き始める時点で、カーネルが強制する境界ができています。

namespaceとcgroupの仕組みを見る capabilityとseccompの仕組みを見る

10.7
PID 1へすべての機能を詰め込まない

周辺機能は別のプロセスで動く

systemdという名前は、PID 1として動くサービスマネージャーだけでなく、Linuxシステムの基本機能を担うソフトウェア群も指します。デバイスの処理、ログの記録、ログインの管理、名前解決は、PID 1の内部で動くのではなく、それぞれ独立したプロセスとして起動されます。