🎓 レベル:基礎 | 重要度:A(必須)
📎 前提:OSの役割とカーネル | 関連:CPUスケジューリング・競合状態とクリティカルセクション
要点(BLUF)
- プロセスは「実行中のプログラム」で、独立したアドレス空間を持つ。互いに干渉しない代わりに、通信にはIPC(プロセス間通信(IPC))が要る。
- スレッドはプロセス内の実行の流れ。同じプロセスのスレッドはメモリを共有するので軽くて速いが、共有ゆえにデータ競合(競合状態とクリティカルセクション)が起きる。
- CPUを切り替えるとき、OSは今の状態を PCB に退避し別のを復元する。これが**文脈切り替え(コンテキストスイッチ)**で、ただのオーバーヘッド。
概念 ── プログラムとプロセスは違う
ディスク上の実行ファイルは「レシピ(静的)」、それを読み込んでCPUが実行している状態が「料理中(動的)」=プロセスです。同じプログラムから複数のプロセスを起動でき(ブラウザのタブごとのプロセスなど)、それぞれが独立した状態を持ちます。
1つのプロセスは、メモリ上に次の領域を持ちます。
flowchart TB
subgraph PS["プロセスのアドレス空間(上が高位アドレス)"]
stack["スタック(関数呼び出し・局所変数 / 下に伸びる)"]
gap["… 空き …"]
heap["ヒープ(malloc等の動的確保 / 上に伸びる)"]
bss["BSS・データ(大域変数・静的変数)"]
text["テキスト(機械語命令・読み取り専用)"]
end
このレイアウトがどう作られるかは リンクとロード(実行ファイルがプロセスになるまで) で詳しく扱います。
仕組み① ── PCBと文脈切り替え
OSは各プロセスの情報を PCB(Process Control Block) という構造体で管理します。中身は、プロセスID、状態、PC・レジスタの退避値、メモリ管理情報(ページテーブルの位置:仮想記憶とページング)、開いているファイル一覧など。
CPUは1つ(1コア)しかないので、複数プロセスを高速に切り替えて並行に見せます。切り替えの瞬間にOIが行うのが文脈切り替え。
sequenceDiagram
participant A as プロセスA
participant K as カーネル
participant B as プロセスB
A->>K: タイムスライス終了/割り込み
Note over K: Aのレジスタ・PCをA-PCBへ退避
Note over K: B-PCBからBのレジスタ・PCを復元
K->>B: Bの続きから実行再開
ポイントは、文脈切り替えは何の仕事も進めない純粋なオーバーヘッドだということ。レジスタ退避・復元、キャッシュやTLB(仮想記憶とページング)の中身が新プロセス向けに入れ替わる(汚染される)コストもかかります。だから切り替えは「必要なときだけ」が原則で、頻度はスケジューラ(CPUスケジューリング)が握ります。
仕組み② ── プロセスの状態遷移
プロセスは一生のうちにいくつかの状態を行き来します。
stateDiagram-v2
[*] --> 生成: 作成(fork)
生成 --> 実行可能: 準備完了
実行可能 --> 実行中: ディスパッチ(CPU割り当て)
実行中 --> 実行可能: タイムスライス終了(プリエンプト)
実行中 --> 待機: I/O要求などでブロック
待機 --> 実行可能: I/O完了(割り込み)
実行中 --> 終了: exit
終了 --> [*]
肝は 実行中 → 待機。プロセスがI/Oを頼んで結果を待つ間(入出力とバス・割り込み)、CPUを手放して別の実行可能プロセスに譲ります。これでCPUが遊ばない。I/O完了の割り込みが来たら、待機していたプロセスが再び実行可能に戻ります。
仕組み③ ── プロセス生成(fork/exec)とスレッド
UNIX系では、プロセスは fork で親のコピーとして生まれ、exec で中身を別プログラムに入れ替えます。
pid_t pid = fork(); // 親のほぼ完全な複製を作る(戻り値で親子を判別)
if (pid == 0) { // 子プロセス側
execvp("ls", args); // 自分自身を ls に置き換えて実行
} else { // 親プロセス側
wait(NULL); // 子の終了を待つ
}
シェルがコマンドを起動する仕組みがこれです。fork で自分を複製し、子側で exec して目的のコマンドに化け、親は wait で待つ。
スレッドは、この重いプロセスの中に複数の「実行の流れ」を持たせる軽量版。同じプロセスのスレッドはコード・ヒープ・大域変数・開いたファイルを共有し、各自が専用に持つのはスタックとレジスタ(=PC)だけです。
| プロセス | スレッド(同一プロセス内) | |
|---|---|---|
| アドレス空間 | 独立(保護される) | 共有 |
| 生成・切替コスト | 重い | 軽い |
| 通信 | IPCが必要 | 共有メモリで直接 |
| 一方のクラッシュ | 他に波及しにくい | プロセス全体が落ちうる |
| 危険 | 干渉しにくい | データ競合が起きやすい |
仕組みの直観 ── なぜこの設計か
- プロセスを分離する理由:障害と悪意の封じ込め。1つが暴走しても他のメモリを壊せない(物理メモリと論理アドレス の保護)。代償が通信コスト(IPC)。
- スレッドが共有する理由:1つの仕事を複数の流れで並行処理したいとき、分離は重すぎる。共有で軽量・高速にする。代償が同期の難しさ(排他制御(ロック・セマフォ・ミューテックス))。
- fork/execに分ける理由:「複製」と「置換」を分離すると、その隙間で標準入出力の付け替え(リダイレクト・パイプ)など柔軟な操作ができる。UNIXの組み合わせ哲学。
⚠️ よくある誤解・落とし穴
- 「スレッドは常にプロセスより速くて良い」→ 共有ゆえにデータ競合のバグが入りやすい。分離が要る場面ではプロセスが適切。
- 「文脈切り替えは無料」→ レジスタ退避+キャッシュ/TLB汚染で実コストがある。多すぎる切り替えは性能を食う(性能の評価とアムダールの法則)。
- 「fork は新しいプログラムを起動する」→ fork は自分の複製を作るだけ。別プログラムにするのは exec。
- 「プロセス=1スレッド固定」→ 1プロセスに多数のスレッドを持てる。並行処理の単位はスレッド。
対応ラボ
cs-foundations-study/labs/02-02_fork.c(Linux: fork/exec/wait の最小C。親子の出力順とPIDを観察する参照コード)。
関連
- どのプロセスにいつCPUを渡すかは CPUスケジューリング
- 分離されたプロセス同士の通信は プロセス間通信(IPC)
- スレッド共有が生む競合は 競合状態とクリティカルセクション
- アドレス空間の生成過程は リンクとロード(実行ファイルがプロセスになるまで)