シェルからプログラムを起動すると、通常は子の作成、リダイレクトの準備、プログラムイメージの置換、終了待機を行います。この流れから、Linux のプロセス管理にある タスクの識別、資源の共有、終了後の回収 という境界が見えてきます。
| 疑問 | 読む場所 |
| メモリを共有するスレッドが同時実行できるのはなぜ? | 第 1 節:タスクと資源オブジェクト |
| fork でファイル位置は共有するのに、メモリ書き込みは分離される? | 第 1~2 節:参照と COW |
| exec で PID が同じままプログラムが変わる? | 第 4 節:イメージ置換 |
| 終了した子が Z として残るのはなぜ? | 第 5 節:終了と回収 |
順に読むとライフサイクルを追えます。CPU の配分はスケジューラ、実行の引き継ぎはコンテキストスイッチで扱います。
1. task_struct:識別情報と資源をつなぐ
スケジュール可能な各スレッドは、それぞれ task_struct、カーネルスタック、スケジューリング状態を持ちます。スレッドグループ内でアドレス空間、ファイル記述子表、シグナル処置を共有しても、TID、レジスタの文脈、シグナルマスク、スレッド宛ての保留シグナルは個別です。資源の共有はスケジューリング実体の統合を意味せず、2 スレッドは 2 CPU で同時に実行できます。
ログの「PID」が何を指すかを確認します。 getpid() はグループの TGID、gettid() は呼び出しスレッドの TID を返します。グループリーダーでは一致し、他のスレッドでは通常異なります。PID namespace によっても番号は変わります。コンテナの PID 1 はホストでは別の番号になり得るため、ログを対応させるにはタスクと観測側の namespace の両方が必要です。
task_struct はプロセスの全内容を内包する箱ではありません。寿命の異なるオブジェクトをポインタと参照でつなぎます。
| オブジェクト・フィールド | 役割 | そこからは言えないこと |
mm | ユーザーアドレス空間、ページテーブル、VMA | mm の共有はカーネルスタックの共有ではない |
files | 記述子番号から開いたファイルへの対応 | 別々の表でも同じオープンファイル記述を参照できる |
fs | カレントディレクトリ、ルート、umask | 記述子表とは別のオブジェクト |
sighand / signal | シグナル処置とグループのシグナル・統計状態 | マスクは引き続きスレッドごと |
| スケジューリング関連 | ポリシー、優先度、実行キューの記帳 | TASK_RUNNING は CPU 使用中とは限らない |
cred と namespace 参照 | 資格情報と資源の見え方 | タスク作成で namespace が自動新設されるわけではない |
2 つの fd 3 が同じオフセットを進める理由
fork 後の親子は別々の記述子表を持ちますが、両方の fd 3 が同じ struct file を指すことがあります。初期オフセットを 0 とします。
| 操作 | 記述子表 | 共有オブジェクト |
| 親が 100 バイトの読み取りに成功 | 親子の fd 3 はどちらも存在 | オフセットが 100 へ進む |
| 子が続けて読む | 子の表の fd 3 を使う | 共有オフセット 100 から読む |
| 子が fd 3 を閉じる | 子の参照だけを除去 | 親の参照は残る |
2 本の矢印が同じオブジェクトを指すことが、オフセット共有の理由です。
独立したオフセットには通常、別々の open() が必要です。dup() と fork() は元のオープンファイル記述への参照を引き継ぎます。
図を準備しています
次の読み取りはどこから始まる?fork 継承では両者が同じオフセットを進め、別々の open では個別のオフセットを持ちます。任意の順序で読み取り、同じ操作履歴で比較できます。
{"id":"process-file-sharing","title":"次の読み取りはどこから始まる?","summary":"fork 継承では両者が同じオフセットを進め、別々の open では個別のオフセットを持ちます。任意の順序で読み取り、同じ操作履歴で比較できます。","height":950,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"presets\" role=\"group\" data-i18n-label=\"models\"><button data-mode=\"fork\" data-i18n=\"fork\"></button><button data-mode=\"open\" data-i18n=\"open\"></button></div>\n<div class=\"read-size\"><label for=\"read-size\" data-i18n=\"readSize\"></label><output id=\"read-size-value\" for=\"read-size\">2</output><input id=\"read-size\" type=\"range\" min=\"1\" max=\"4\" step=\"1\" value=\"2\" data-i18n-label=\"readSize\"></div>\n<div id=\"scene\" class=\"scene\">\n <div class=\"readers\"><div id=\"parent\" class=\"reader\"><span data-i18n=\"parent\"></span><small data-i18n=\"readResult\"></small><strong id=\"parent-result\">AB</strong><code>fd 3</code><button data-reader=\"parent\" data-i18n=\"read\"></button></div><div id=\"child\" class=\"reader\"><span data-i18n=\"child\"></span><small data-i18n=\"readResult\"></small><strong id=\"child-result\">CD</strong><code>fd 3</code><button data-reader=\"child\" data-i18n=\"read\"></button></div></div>\n <svg class=\"links\" viewBox=\"0 0 600 64\" preserveAspectRatio=\"none\" aria-hidden=\"true\"><path id=\"parent-link\"/><path id=\"child-link\"/></svg>\n <div class=\"open-files\"><div id=\"file-a\" class=\"open-file\"><div class=\"file-title\"><span data-i18n=\"opened\"></span><b>A</b></div><div class=\"bytes\" aria-hidden=\"true\"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span><span>G</span><span>H</span></div><div class=\"position\"><i id=\"cursor-a\"></i></div><div class=\"offset\"><span data-i18n=\"next\"></span><strong id=\"offset-a\">4</strong></div></div><div id=\"file-b\" class=\"open-file\"><div class=\"file-title\"><span data-i18n=\"opened\"></span><b>B</b></div><div class=\"bytes\" aria-hidden=\"true\"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span><span>G</span><span>H</span></div><div class=\"position\"><i id=\"cursor-b\"></i></div><div class=\"offset\"><span data-i18n=\"next\"></span><strong id=\"offset-b\">2</strong></div></div></div>\n</div>\n<p class=\"caption\" data-i18n=\"contents\"></p>\n<div class=\"copy\"><p id=\"conclusion\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"sharedConclusion\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"separateConclusion\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"beforeRead\"></p></div>\n<div class=\"demo-controls\"><button id=\"auto-play\" aria-pressed=\"false\"><span id=\"auto-label\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"auto\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"pause\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"replayAuto\"></span></button><button id=\"restart\" data-i18n=\"restart\"></button></div><p class=\"note\" data-i18n=\"note\"></p>\n","css":".intro{font-size:14px;color:var(--muted);margin:8px 0 18px}.note{font-size:12px;color:var(--muted);margin-top:16px}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.controls{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.controls button{min-height:42px}.muted{color:var(--muted)}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.caption{font-size:12px;color:var(--muted)}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.demo-controls{display:flex;gap:10px;align-items:stretch;margin-top:18px}.demo-controls button{min-width:0;min-height:42px;font-size:13px}#auto-play{display:grid;flex:1}#auto-play>span{grid-area:1/1}#auto-play[aria-pressed=\"true\"]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}#restart{color:var(--muted)}\n.scene{margin-top:22px}.readers{display:grid;grid-template-columns:1fr 1fr;gap:18px}.reader{border:1px solid var(--rule);border-radius:10px;padding:14px;text-align:center;transition:background .3s,border-color .3s}.reader span,.reader small,.reader code{display:block}.reader span{font-weight:600;font-size:14px}.reader small{color:var(--muted);font-size:12px;min-height:2.8em}.reader strong{display:block;font:600 26px var(--viz-font-ui);margin:6px 0}.reader code{font-size:12px;color:var(--muted)}.reader.read{background:var(--accent-soft);border-color:var(--accent)}.links{display:block;width:100%;height:64px;overflow:visible}.links path{fill:none;stroke:var(--accent);stroke-width:2;vector-effect:non-scaling-stroke;transition:d .3s}.open-files{display:grid;grid-template-columns:1fr 1fr;gap:18px;position:relative}.open-file{padding:16px;border:1px solid var(--accent);background:var(--accent-soft);border-radius:10px;min-width:0;transition:opacity .3s,background .3s}.scene.shared #file-a{grid-column:1/3;width:72%;justify-self:center}.scene.shared #file-b{position:absolute;inset:0 0 0 auto;width:calc(50% - 9px);visibility:hidden;opacity:0;pointer-events:none}.file-title{display:flex;justify-content:space-between;align-items:start;gap:6px;font-size:12px;min-height:2.8em;color:var(--muted)}.file-title b{color:var(--accent);font-size:16px}.bytes{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:10px}.bytes span{font:16px var(--viz-font-mono);text-align:center;padding:10px 0;background:var(--paper);border:1px solid var(--rule);transition:background .3s}.bytes span+span{border-left:0}.bytes .latest{background:var(--second-soft);color:var(--ink)}.position{position:relative;height:12px;border-bottom:1px solid var(--accent);margin-bottom:10px}.position i{position:absolute;bottom:-4px;width:8px;height:8px;border-radius:50%;background:var(--accent);transform:translateX(-50%);transition:left .4s}.offset{font-size:12px;display:flex;justify-content:space-between;align-items:center;gap:6px;min-height:2.8em}.offset strong{font:600 20px var(--viz-font-ui)}.caption{margin-top:12px}.controls button{width:100%}\n@media(max-width:480px){.readers,.open-files{gap:10px}.reader{padding:12px 8px}.open-file{padding:12px 8px}.scene.shared #file-a{width:80%}.file-title,.offset{min-height:4.2em}.scene.shared #file-b{width:calc(50% - 5px)}}\n\n.reader button{display:block;width:100%;margin-top:12px;font-size:13px;min-height:42px}.bytes{grid-template-columns:repeat(8,minmax(0,1fr))}.bytes span{font-size:13px}.reader strong{min-height:1.4em}@media(max-width:480px){.bytes span{font-size:10px}.reader button{font-size:12px;padding-inline:5px}}\n\n.read-size{display:grid;grid-template-columns:1fr auto;gap:6px 12px;margin-top:18px;font-size:13px;align-items:center}.read-size input{grid-column:1/3;width:100%;margin:0}.read-size output{color:var(--accent);font-variant-numeric:tabular-nums}\n","js":"const $ = s => document.querySelector(s);\nconst contents = 'ABCDEFGH';\nlet mode = 'fork', history = [], readSize = 2;\nfunction calculate() {\n const offsets = [0, 0], results = {parent: '—', child: '—'}, ranges = [[0,0],[0,0]];\n for (const {who, size} of history) {\n const i = mode === 'fork' || who === 'parent' ? 0 : 1;\n const start = offsets[i], chunk = contents.slice(start, start + size);\n offsets[i] += chunk.length;\n results[who] = chunk || 'EOF';\n ranges[i] = [start, offsets[i]];\n }\n return {offsets, results, ranges};\n}\nfunction finished() {\n const {offsets} = calculate();\n return offsets[0] === contents.length && (mode === 'fork' || offsets[1] === contents.length);\n}\nfunction playbackLabel(running) {\n $('#auto-play').setAttribute('aria-pressed', String(running));\n $('#auto-label').textContent = viz.t(running ? 'pause' : finished() ? 'replayAuto' : 'auto');\n}\nfunction draw() {\n const shared = mode === 'fork', {offsets, results, ranges} = calculate();\n $('#scene').classList.toggle('shared', shared);\n document.querySelectorAll('[data-mode]').forEach(e => e.setAttribute('aria-pressed', String(e.dataset.mode === mode)));\n $('#parent-link').setAttribute('d', shared ? 'M150 0 C150 32 300 32 300 64' : 'M150 0 L150 64');\n $('#child-link').setAttribute('d', shared ? 'M450 0 C450 32 300 32 300 64' : 'M450 0 L450 64');\n for (const who of ['parent', 'child']) {\n $('#'+who+'-result').textContent = results[who];\n $('#'+who).classList.toggle('read', results[who] !== '—');\n }\n for (const [i, file] of ['a','b'].entries()) {\n $('#offset-'+file).textContent = offsets[i];\n $('#cursor-'+file).style.left = offsets[i] / contents.length * 100 + '%';\n document.querySelectorAll('#file-'+file+' .bytes span').forEach((e,j) => e.classList.toggle('latest', j >= ranges[i][0] && j < ranges[i][1]));\n }\n $('#conclusion').textContent = viz.t(!history.length ? 'beforeRead' : shared ? 'sharedConclusion' : 'separateConclusion');\n document.querySelectorAll('[data-reader]').forEach(e => e.textContent = viz.t('read').replace('{n}', readSize));\n $('#read-size-value').textContent = readSize;\n playbackLabel(player.playing);\n}\nconst player = viz.playback({\n delay: 1800,\n canAdvance: () => !finished(),\n advance() {\n let who = history.at(-1)?.who === 'parent' ? 'child' : 'parent';\n const {offsets} = calculate();\n if (mode === 'open' && offsets[who === 'parent' ? 0 : 1] === contents.length) who = who === 'parent' ? 'child' : 'parent';\n history.push({who, size: readSize}); draw();\n },\n onChange: playbackLabel,\n});\ndocument.querySelectorAll('[data-reader]').forEach(e => e.addEventListener('click', () => { player.pause(); history.push({who:e.dataset.reader, size:readSize}); draw(); }));\ndocument.querySelectorAll('[data-mode]').forEach(e => e.addEventListener('click', () => { player.pause(); mode = e.dataset.mode; draw(); }));\n$('#read-size').addEventListener('input', e => { player.pause(); readSize = Number(e.target.value); draw(); });\n$('#auto-play').addEventListener('click', () => {\n if (!player.playing && finished()) { history = []; draw(); }\n player.toggle();\n});\n$('#restart').addEventListener('click', () => { player.pause(); history = []; draw(); });\ndraw();\n","audio":false,"strings":{"auto":"自動デモ","beforeRead":"オフセットはすべて 0 です。好きな側の読み取りボタンを押し、結果と参照先を観察してください。","child":"子プロセス","contents":"各窓は同じファイル ABCDEFGH を表示します。強調部分は直近に読んだバイト、点は次に読む位置です。","eyebrow":"01 · ファイル参照","fork":"fork で継承","heading":"次の読み取りはどこから始まる?","intro":"読み取り長を調整し、親と子を好きな順番で読み取らせます。開き方を変えると同じ操作履歴で再計算します。","models":"ファイルの開き方","next":"次に読む位置","note":"クリック順に読み取りが成功し、他のアクセスはないものとします。末尾付近では要求より短くなり、末尾では EOF を返します。自動デモは交互に読み、手動操作で一時停止します。","open":"別々に open","opened":"オープンファイル記述","parent":"親プロセス","pause":"一時停止","read":"{n} バイト読む","readResult":"直近の読み取り","readSize":"1 回に要求するバイト数","replace":"イメージを置換","replayAuto":"デモを再生","restart":"リセット","separateConclusion":"各 open は個別のオフセットを管理します。読み取ったプロセスの参照先だけが進み、もう一方の位置は変わりません。","sharedConclusion":"2 つの参照は 1 つのオフセットを指します。どちらが読んでもその位置が進み、もう一方の次の読み取りもそこから続きます。","write":"パイプへ出力","writeCount":"受信回数"}}
ソースを読むとき: current の実装はアーキテクチャに依存します。カーネルスタックの切り捨て式や固定の task_struct オフセットを前提にしないでください。
2. 作成:関係を複製してもデータを即座にコピーするとは限らない
fork() は独立した実行の流れを持つ子を作ります。親には子の PID、子には 0 が返り、その後の実行順は保証されません。作成には識別情報、資源参照、スケジューリング実体、初期実行文脈の構築が必要です。ユーザーのプライベートメモリは通常コピーオンライトで即時のデータコピーを減らしますが、ページテーブルやメタデータの処理にもコストがあります。「task_struct を 1 つコピーするだけ」ではありません。
プライベートページ:最初は共有し、書き込みを分離する
コピーオンライトはコピーの遅延と分離を両立します。最初に親子が同じ物理ページを参照しているとします。親が自身のプライベートマッピングへ初めて書くと書き込み保護フォールトが発生し、カーネルはプライベート書き込みの意味を満たすページマッピングを構築します。子には元の内容が見えます。物理ページの共有は実装手段で、書き込みが相互に混入しないことが意味上の約束です。MAP_SHARED は別に考えます。ページテーブルとフォールトの詳細は仮想メモリへ進みます。
clone フラグで共有関係を選ぶ
clone() / clone3() のフラグには依存関係があります。CLONE_THREAD は CLONE_SIGHAND を要求し、後者は CLONE_VM を要求します。
| 作成方法 | アドレス空間とファイル表 | 各タスクで独立 |
| 典型的な POSIX スレッド | mm、ファイル表、シグナル処置を共有 | スタック、スレッド識別、実行文脈 |
| 通常の fork | mm と表は別、下位の共有オブジェクトあり | 子の識別、スタック、実行文脈 |
namespace は通常継承されます。新設には対応フラグ、権限などの条件が必要です。
関数とスタックを受け取る glibc の clone() ラッパーと、アーキテクチャ依存の生システムコール ABI は区別します。kernel_clone()、copy_process() は実装を探す入口であり、アプリケーション向けインターフェースではありません。clone(2)
マルチスレッドの fork:解錠する人がいないロック
スレッド B がユーザー空間のロックを保持している間に A が fork すると、子には A だけが残りますが、ロック済みのメモリ状態は継承され得ます。子の A がそのロックを取得しようとすると、子にはいない B の解放を待ち続ける可能性があります。
このためマルチスレッドの fork から exec まで、子には非同期シグナル安全な関数だけを呼ぶなどの制約があります。準備を posix_spawn() に任せる方が管理しやすい場合があります。fork(2)
vfork はさらに厳しい制約があります。 子は一時的にアドレス空間を共有し、呼び出し元スレッドは停止しますが、親の他のスレッドは動き得ます。子で通常の C 処理を自由に実行できるわけではありません。vfork(2)
3. 実行と待機:状態はスケジューリング条件を表す
TASK_RUNNING は実行中と実行可能の両方を含みます。1 CPU にこの状態のタスクが 10 個あっても、通常同時に動くのは 1 つです。I/O、ロック、イベントを待つタスクはスリープすることがあります。起床は実行資格を戻しますが、実際に CPU を得る時刻はスケジューラが決めます。
| 観測値 | 意味 | 解釈の限界 |
| R | 実行中または実行可能 | CPU 飽和や待ち時間はこれだけでは不明 |
| S | 割り込み可能なスリープ | EINTR になるかは API と再開規則にも依存 |
| D | 割り込み不能系の待機 | ディスク I/O 以外もある。カーネルの待機箇所を調べる |
| T / t | ジョブ制御停止 / トレース停止 | 通常のスリープや終了とは異なる |
| Z | 終了済み、回収待ち | 実行を止め、回収に必要な情報を保持 |
内部状態はビットとプロトコルで構成され、終了は exit_state などでも管理されます。観測文字を単純な列挙値として代入するモデルではありません。TASK_KILLABLE は対応する待機を致命的シグナルで中断可能にしますが、「新しいカーネルでは D がすべて kill 可能」という意味ではありません。
待機では「条件確認後、スリープ登録前」にイベントが落ちる隙間をなくします。READ_ONCE() と schedule() だけでは不十分です。wait_event*() などを使い、条件に必要なロックやメモリ順序でデータを公開します。起床取りこぼしはコンテキストスイッチで具体的に扱います。
4. exec:識別を保ち、プログラムイメージを置換する
成功した execve() は別プロセスを作らず、元の呼び出し位置にも戻りません。同じプロセスの識別のもとで、新しいアドレス空間、ユーザースタック、入口状態を読み込みます。動的リンク ELF では通常インタプリタ/動的リンカから始まり、ランタイム初期化を経て main() に到達します。カーネルが main を直接呼ぶわけではありません。
| 対象 | exec 成功後 | アプリケーションへの影響 |
| プライベートアドレス空間 | ヒープ、スタック、マッピングを置換 | 旧ポインタを新プログラムのデータに使えない |
| 他のスレッド | 元のグループの他スレッドを終了 | 非リーダーが呼ぶ場合は TID も調整 |
| ファイル記述子 | 通常 FD_CLOEXEC のないものを保持 | 準備済みの標準入出力を引き継げる |
| シグナル状態 | ハンドラはリセット、無視は通常保持、マスクは保持 | 旧関数を新イメージのコールバックにできない |
| 資格情報 | ファイル属性と実行制約に依存 | no_new_privs、マウント属性、トレースを考慮 |
シェルのリダイレクトは記述子の保持を利用します。 子がパイプ端を標準出力へ複製してから exec すると、新プログラムは作成手順を知らずに同じオープンファイルへ書き込めます。
図を準備しています
プログラムが変わっても同じプロセス?exec は同じプロセス識別のもとでコード、データ、スタックを置換します。CLOEXEC のない標準出力は元のパイプ接続を保ちます。
{"id":"process-exec","title":"プログラムが変わっても同じプロセス?","summary":"exec は同じプロセス識別のもとでコード、データ、スタックを置換します。CLOEXEC のない標準出力は元のパイプ接続を保ちます。","height":950,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div id=\"process\" class=\"process\"><header><strong data-i18n=\"child\"></strong><span class=\"mono\">PID 4201</span></header>\n <div class=\"image-title\"><span data-i18n=\"space\"></span><span id=\"program-name\"></span></div>\n <div class=\"image-stack\"><section id=\"old-image\" class=\"program-image\"><div class=\"code\"><span class=\"caption\" data-i18n=\"code\"></span><button id=\"replace\" class=\"code-action\"><code>execve(\"./sender\", …);</code><span data-i18n=\"replace\"></span></button><code id=\"unreachable\">puts(\"old\");</code></div><div class=\"memory\"><div><span data-i18n=\"data\"></span><code>buffer = \"old\"</code></div><div><span data-i18n=\"stack\"></span><code>launch()</code><code>count = 3</code></div></div></section><section id=\"new-image\" class=\"program-image\" aria-hidden=\"true\"><div class=\"code\"><span class=\"caption\" data-i18n=\"code\"></span><button id=\"write\" class=\"code-action\"><code>write(1, \"ready\\n\", 6);</code><span data-i18n=\"write\"></span></button><code>/* message loop */</code></div><div class=\"memory\"><div><span data-i18n=\"data\"></span><code>message = \"ready\\n\"</code></div><div><span data-i18n=\"stack\"></span><code>main(argc, argv)</code><code>argv[0] = \"sender\"</code></div></div></section></div>\n <div class=\"fd\"><code>fd 1</code><span data-i18n=\"stdout\"></span></div>\n</div>\n<div class=\"pipe-path\"><i id=\"packet\"></i><span>↓</span><span data-i18n=\"pipe\"></span><span>↓</span></div>\n<div class=\"receiver\"><span data-i18n=\"parent\"></span><div><strong id=\"received\">—</strong><small id=\"writes\"></small></div></div>\n<div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"beforeNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"afterNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"replacedNote\"></p></div>\n<div class=\"demo-controls\"><button id=\"auto-play\" aria-pressed=\"false\"><span id=\"auto-label\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"auto\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"pause\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"replayAuto\"></span></button><button id=\"restart\" data-i18n=\"restart\"></button></div>\n<p class=\"note\" data-i18n=\"note\"></p>\n","css":".intro{font-size:14px;color:var(--muted);margin:8px 0 18px}.note{font-size:12px;color:var(--muted);margin-top:16px}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.controls{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.controls button{min-height:42px}.muted{color:var(--muted)}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.caption{font-size:12px;color:var(--muted)}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.demo-controls{display:flex;gap:10px;align-items:stretch;margin-top:18px}.demo-controls button{min-width:0;min-height:42px;font-size:13px}#auto-play{display:grid;flex:1}#auto-play>span{grid-area:1/1}#auto-play[aria-pressed=\"true\"]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}#restart{color:var(--muted)}\n.process{border:2px solid var(--accent);border-radius:12px;overflow:hidden}.process header{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:14px;background:var(--accent-soft);font-size:14px}.process header span{color:var(--accent)}.image-title{display:flex;justify-content:space-between;gap:10px;padding:16px 16px 8px;color:var(--muted);font-size:12px}.image-title span{flex:1}#program-name{text-align:right}.image-stack{display:grid;margin:0 14px;border:1px solid var(--rule);border-radius:8px;background:var(--surface);overflow:hidden}.program-image{grid-area:1/1;min-width:0;padding:14px;transition:opacity .4s,transform .4s;transform:translateY(0)}#new-image{opacity:0;visibility:hidden;transform:translateY(8px);background:var(--second-soft)}.process.replaced #old-image{opacity:0;visibility:hidden;transform:translateY(-8px)}.process.replaced #new-image{opacity:1;visibility:visible;transform:translateY(0)}.code{min-width:0}.code code{display:block;white-space:normal;overflow-wrap:anywhere;font-size:14px;line-height:1.7}#unreachable{color:var(--muted)}.memory{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}.memory>div{min-width:0;border-top:1px solid var(--rule);padding-top:10px}.memory span,.memory code{display:block;font-size:12px;overflow-wrap:anywhere}.memory span{color:var(--muted);margin-bottom:4px}.fd{display:flex;align-items:center;gap:12px;margin:14px;padding:10px 12px;background:var(--accent-soft);border:1px solid var(--accent);border-radius:6px;font-size:12px}.fd code{white-space:nowrap;color:var(--accent);font-size:15px}.pipe-path{position:relative;display:flex;flex-direction:column;align-items:center;font-size:12px;color:var(--accent);line-height:1.5;padding:2px 0}.pipe-path span:first-of-type,.pipe-path span:last-of-type{font-size:20px}.pipe-path i{position:absolute;left:calc(50% + 35px);top:0;width:7px;height:7px;border-radius:50%;background:var(--second);opacity:0}.pipe-path.flow i{animation:packet .6s ease-in-out}.receiver{display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--accent);background:var(--accent-soft);border-radius:8px;padding:14px;font-size:13px}.receiver strong{font:600 20px var(--viz-font-ui)}.controls button{display:grid;width:100%}.controls button>span{grid-area:1/1}.image-title{min-height:4.2em}\n@keyframes packet{0%{opacity:0;transform:translateY(0)}20%{opacity:1}85%{opacity:1}100%{opacity:0;transform:translateY(65px)}}\n@media(max-width:480px){.program-image{padding:12px}.image-stack{margin:0 10px}.memory{grid-template-columns:1fr}.memory>div{min-height:5.4em}.process header{padding:12px}.code code{font-size:12px}.fd{margin:12px 10px}}\n\n.code-action{width:100%;display:flex;flex-wrap:wrap;justify-content:space-between;text-align:left;gap:6px 12px;padding:10px;margin:6px 0;background:var(--paper);border-color:var(--accent)}.code-action code{font-size:13px}.code-action span{font-size:12px;color:var(--accent)}.receiver>div{text-align:right}.receiver small{display:block;font-size:12px;color:var(--muted);min-height:1.7em}\n","js":"const $ = s => document.querySelector(s);\nlet replaced = false, writes = 0;\nfunction playbackLabel(running) {\n $('#auto-play').setAttribute('aria-pressed', String(running));\n $('#auto-label').textContent = viz.t(running ? 'pause' : writes ? 'replayAuto' : 'auto');\n}\nfunction draw() {\n $('#process').classList.toggle('replaced', replaced);\n $('#old-image').setAttribute('aria-hidden', String(replaced));\n $('#new-image').setAttribute('aria-hidden', String(!replaced));\n $('#program-name').textContent = viz.t(replaced ? 'newProgram' : 'oldProgram');\n $('#received').textContent = writes ? 'ready' : '—';\n $('#writes').textContent = viz.t('writeCount') + ' ' + writes;\n $('#explanation').textContent = viz.t(!replaced ? 'beforeNote' : !writes ? 'replacedNote' : 'afterNote');\n playbackLabel(player.playing);\n}\nfunction replace() { replaced = true; draw(); }\nfunction write() {\n if (!replaced) return;\n writes++; draw();\n const pipe = document.querySelector('.pipe-path');\n pipe.classList.remove('flow'); void pipe.offsetWidth; pipe.classList.add('flow');\n}\nconst player = viz.playback({delay: 1800, canAdvance: () => writes === 0,\n advance() { if (!replaced) replace(); else write(); }, onChange: playbackLabel});\n$('#replace').addEventListener('click', () => { player.pause(); replace(); });\n$('#write').addEventListener('click', () => { player.pause(); write(); });\n$('#auto-play').addEventListener('click', () => {\n if (!player.playing && writes) { replaced = false; writes = 0; document.querySelector('.pipe-path').classList.remove('flow'); draw(); }\n player.toggle();\n});\n$('#restart').addEventListener('click', () => { player.pause(); replaced = false; writes = 0; document.querySelector('.pipe-path').classList.remove('flow'); draw(); });\ndraw();\n","audio":false,"strings":{"afterNote":"PID 4201 と fd 1 からパイプへの接続は同じです。sender は新しいコードとスタックを使い、継承した標準出力へ ready を書きました。","auto":"自動デモ","beforeNote":"外枠はプロセスの識別、内部は現在のプログラムです。exec 成功で旧コード、変数、スタックが置換され、次の puts(\"old\") は実行されません。","child":"子プロセス","code":"実行するコード","data":"データ / ヒープ","eyebrow":"02 · イメージの置換","heading":"プログラムが変わっても同じプロセス?","intro":"子の標準出力はパイプにつながっています。コードの横の操作でプログラムを置換し、新プログラムからの出力をその都度実行できます。","newProgram":"新プログラム sender","note":"単一スレッドの exec 成功例で、fd 1 に CLOEXEC はありません。コードは抜粋で、リンカとランタイム初期化は省略。新プログラムはメッセージループに留まり、ready を繰り返し出力できます。","oldProgram":"元の起動プログラム","parent":"親が受信した内容","pause":"一時停止","pipe":"元のパイプ P","read":"2 バイト読む","readResult":"直近の読み取り","replace":"イメージを置換","replacedNote":"イメージは置換され、PID と出力接続は同じです。まだ出力していません。新プログラムの「パイプへ出力」を押して接続を観察します。","replayAuto":"デモを再生","restart":"リセット","space":"ユーザーアドレス空間","stack":"ユーザースタック","stdout":"標準出力の接続を保持","write":"パイプへ出力","writeCount":"受信回数"}}
通常 exec が戻ったら失敗として errno を処理します。ただし、復帰不能な段階を越えたまれな失敗では、旧イメージに戻らずプロセスが終了することがあります。execve(2)
5. exit と wait:終了しても zombie が残る理由
子の終了時に親は別の作業をしているかもしれません。結果まで即座に捨てると、親は成功、エラー終了、シグナルによる終了を後から知ることができません。そのため終了はアドレス空間やファイルへの参照を解放しつつ、終了結果と関連メタデータを適切な待機者のために保持します。
zombie はユーザーコードを実行しませんが、カーネルメモリを全く使わないわけではありません。また、親が同じオープンファイルを保持したり、別プロセスが共有メモリを使ったりするため、終了で全オブジェクトが破棄されるわけでもありません。多くのオブジェクトでは最後の参照の解放が寿命を決めます。
ユーザー空間の後処理とカーネルの終了を分ける
exit() は終了ハンドラと標準 I/O のフラッシュを行い、_exit() はその処理を省きます。Linux の glibc の _exit() は通常 exit_group でスレッドグループを終了しますが、生の SYS_exit は呼び出しスレッドだけを終了します。他のスレッドまで消えた理由を調べる際は libc と生システムコールを区別します。
観察と回収を分ける
waitpid() や waitid() は終了結果を消費できます。waitid(..., WNOWAIT) は後の待機を可能にしたまま観察します。次の図では両者を分け、終了前に押してもブロックしないよう WNOHANG を指定しています。
図を準備しています
終了コードを読むと回収も終わる?終了は結果を保持します。WNOWAIT は終了コードを取得しても消費せず、回収が待機可能な結果を消費します。親が取得した値は残ります。
{"id":"process-exit-wait","title":"終了コードを読むと回収も終わる?","summary":"終了は結果を保持します。WNOWAIT は終了コードを取得しても消費せず、回収が待機可能な結果を消費します。親が取得した値は残ります。","height":950,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"world\"><section id=\"child\" class=\"actor\"><header><strong data-i18n=\"child\"></strong><code>PID 4201</code></header><div class=\"work\" id=\"work\"><span class=\"pulse\"></span><span id=\"work-label\"></span></div><div class=\"record\" id=\"record\"><span data-i18n=\"record\"></span><strong id=\"record-value\">—</strong><small id=\"record-label\"></small></div><button id=\"exit\" data-i18n=\"exit\"></button></section>\n<div class=\"transfer\" aria-hidden=\"true\"><span>→</span><i id=\"transit\">7</i></div>\n<section class=\"actor parent\"><header><strong data-i18n=\"parent\"></strong><span data-i18n=\"waiting\"></span></header><div class=\"parent-result\"><span data-i18n=\"received\"></span><strong id=\"parent-value\">—</strong><small id=\"parent-label\"></small></div><div class=\"parent-actions\"><button id=\"observe\" data-i18n=\"observe\"></button><button id=\"reap\" data-i18n=\"reap\"></button></div></section></div>\n<div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"liveNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"zombieNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"observedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"reapedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"earlyNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"againNote\"></p></div>\n<details><summary data-i18n=\"calls\"></summary><div class=\"call\"><code id=\"call\"></code><p id=\"return-value\"></p></div><p class=\"note\" data-i18n=\"nonblocking\"></p></details>\n<div class=\"controls\"><button id=\"reset\" data-i18n=\"reset\"></button></div><p class=\"note\" data-i18n=\"note\"></p>\n","css":".intro{font-size:14px;color:var(--muted);margin:8px 0 18px}.note{font-size:12px;color:var(--muted);margin-top:16px}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.controls{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.controls button{min-height:42px}.muted{color:var(--muted)}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.caption{font-size:12px;color:var(--muted)}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.world{display:grid;grid-template-columns:1fr 30px 1fr;align-items:stretch}.actor{min-width:0;padding:16px;border:1px solid var(--rule);border-radius:10px;display:flex;flex-direction:column;gap:14px}.actor header strong,.actor header code,.actor header span{display:block}.actor header strong{font-size:14px}.actor header code,.actor header span{font-size:12px;color:var(--muted)}.work{display:flex;align-items:center;gap:8px;font-size:12px;min-height:3em;color:var(--accent)}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent);flex-shrink:0}.record{border:1px dashed var(--rule);border-radius:8px;padding:12px;transition:background .3s,opacity .3s;min-width:0}.record>span,.parent-result>span{display:block;font-size:12px;color:var(--muted);min-height:2.8em}.record strong,.parent-result strong{display:block;font:600 30px var(--viz-font-ui);margin:10px 0}.record small,.parent-result small{display:block;font-size:12px;color:var(--muted);min-height:4.2em;overflow-wrap:anywhere}.actor button{font-size:13px;min-width:0;min-height:44px}.actor>button{margin-top:auto}.parent-actions{display:grid;gap:8px;margin-top:auto}.parent-result{padding:12px;border:1px solid var(--rule);border-radius:8px;background:var(--surface);min-width:0}.transfer{position:relative;display:flex;align-items:center;justify-content:center;color:var(--muted)}.transfer>span{font-size:20px}.transfer i{position:absolute;top:45%;font-style:normal;color:var(--second);font-weight:700;opacity:0}.transfer.moving i{animation:result-copy .45s ease-in-out}#child.zombie .pulse,#child.reaped .pulse{background:var(--muted)}#child.zombie .work,#child.reaped .work{color:var(--muted)}#child.zombie .record{border-style:solid;border-color:var(--second);background:var(--second-soft)}#child.reaped .record{opacity:.45}#child.reaped header code{text-decoration:line-through}.parent.has-result .parent-result{border-color:var(--accent);background:var(--accent-soft)}details{margin-top:16px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:10px 0}.call code{white-space:normal;overflow-wrap:anywhere}.call p{font-family:var(--viz-font-mono);overflow-wrap:anywhere}.controls button{color:var(--muted)}\n@keyframes result-copy{0%{opacity:0;transform:translateX(-10px)}20%{opacity:1}80%{opacity:1}100%{opacity:0;transform:translateX(10px)}}\n@media(max-width:480px){.world{grid-template-columns:1fr 20px 1fr}.actor{padding:10px 8px;gap:10px}.record,.parent-result{padding:10px 8px}.record>span,.parent-result>span{min-height:4.2em}.record small,.parent-result small{min-height:7em}.actor header{min-height:4.5em}.work{min-height:4.2em}.actor button{padding-inline:7px;font-size:12px;min-height:5.2em}}\n","js":"const $ = s => document.querySelector(s);\nlet state = 'live', parentHasResult = false, observed = false, message = 'liveNote', call = '', result = '—';\nfunction draw() {\n $('#child').className = 'actor ' + state;\n $('#work-label').textContent = viz.t(state === 'live' ? 'running' : state === 'zombie' ? 'stopped' : 'gone');\n $('#record-value').textContent = state === 'zombie' ? '7' : '—';\n $('#record-label').textContent = viz.t(state === 'live' ? 'pending' : state === 'reaped' ? 'consumed' : observed ? 'stillHere' : 'waitable');\n $('#parent-value').textContent = parentHasResult ? '7' : '—';\n $('#parent-label').textContent = viz.t(parentHasResult ? 'saved' : 'notReceived');\n document.querySelector('.parent').classList.toggle('has-result', parentHasResult);\n $('#exit').disabled = state !== 'live';\n $('#explanation').textContent = viz.t(message);\n $('#call').textContent = call || viz.t('noCall');\n $('#return-value').textContent = result;\n}\nfunction transfer() {\n const el = document.querySelector('.transfer');\n el.classList.remove('moving'); void el.offsetWidth; el.classList.add('moving');\n}\n$('#exit').addEventListener('click', () => { state = 'zombie'; message = 'zombieNote'; call = '_exit(7)'; result = viz.t('noReturn'); draw(); });\nfor (const kind of ['observe', 'reap']) $('#'+kind).addEventListener('click', () => {\n call = kind === 'observe' ? 'waitid(P_PID, 4201, …, WEXITED | WNOWAIT | WNOHANG)' : 'waitpid(4201, &status, WNOHANG)';\n if (state === 'live') { message = 'earlyNote'; result = kind === 'observe' ? '0; si_pid = 0' : '0'; }\n else if (state === 'reaped') { message = 'againNote'; result = '−1; errno = ECHILD'; }\n else {\n parentHasResult = true;\n if (kind === 'observe') { observed = true; message = 'observedNote'; result = '0; si_pid = 4201; si_status = 7'; }\n else { state = 'reaped'; message = 'reapedNote'; result = '4201; WEXITSTATUS(status) = 7'; }\n transfer();\n }\n draw();\n});\n$('#reset').addEventListener('click', () => { state = 'live'; parentHasResult = false; observed = false; message = 'liveNote'; call = ''; result = '—'; draw(); });\ndraw();\n","audio":false,"strings":{"againNote":"子は既に回収済みなので、再度の待機は ECHILD です。親が以前取得した終了コード 7 は残ります。","calls":"システムコールと戻り値を見る","child":"子プロセス","consumed":"消費済み、待機不可","earlyNote":"終了結果はまだありません。ここではブロックせずに戻り、子は実行を続けます。","exit":"コード 7 で終了","eyebrow":"03 · 終了と回収","gone":"回収済み","heading":"終了コードを読むと回収も終わる?","intro":"子の保持する終了結果と、親が取得した値を並べます。子を終了させてから、観察と回収を比較します。","liveNote":"子はまだ実行中で、終了結果はありません。親が待機しても、それによって子が終了するわけではありません。","noCall":"まだ呼び出していません","noReturn":"戻らない。子の実行は停止","nonblocking":"終了前にも試せるよう、待機には WNOHANG を付けます。WNOWAIT は結果の消費、WNOHANG はブロックの有無を制御します。","notReceived":"まだ取得していない","note":"SIGCHLD がデフォルトの通常の待機可能な単一スレッドの子です。左枠は待機用の情報で、独立したカーネル構造体ではありません。自動回収、ptrace、遅延解放は省略します。","observe":"観察 · WNOWAIT","observedNote":"両側に 7 があります。親は結果を読みましたが、WNOWAIT は消費しません。左の結果は残り、子は Z のままです。","parent":"親プロセス","pending":"まだ生成されていない","reap":"回収 · waitpid","reapedNote":"親が取得した 7 は右側に残ります。左側の待機可能な結果は消費され、子は回収されました。","received":"親が取得した終了コード","record":"カーネルに残る終了結果","reset":"最初から","running":"プログラム実行中","saved":"親が取得した値","stillHere":"保持中、再待機可能","stopped":"Z · 実行は停止","waitable":"待機者のために保持","waiting":"待機を行う側","zombieNote":"子の実行は停止し、終了コード 7 がカーネルに残りました。親はまだ取得していません。これが図の Z 状態です。"}}
明示的な SIGCHLD の SIG_IGN 設定や SA_NOCLDWAIT は自動回収の動作に影響します。親が先に終了すると、適用される subreaper または PID namespace の init が孤児を引き取ります。
SIGCHLD のデフォルト動作が「無視」であることと、明示的な SIG_IGN の回収動作は同じではありません。waitpid(2)
実行例:パイプでライフサイクルを追う
Linux の単一スレッドの例で fork、リダイレクト、exec、終了の観察、回収をつなぎます。lifecycle.c に保存し、cc -std=c11 -O2 -Wall -Wextra -Werror lifecycle.c -o lifecycle でコンパイルします。受信する PID は exec 後のプログラムから届きます。上の図と異なり最初の 2 回の待機は結果が得られるまでブロックします。終了コード 7 を観察し、次に消費し、最後に再待機できないことを確認します。
#define _GNU_SOURCE
#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv) {
(void)argv;
if (argc == 2) {
printf("%ld\n", (long)getpid());
return 7;
}
struct sigaction sa = { .sa_handler = SIG_DFL };
sigemptyset(&sa.sa_mask);
assert(sigaction(SIGCHLD, &sa, NULL) == 0);
int p[2];
assert(pipe2(p, O_CLOEXEC) == 0);
assert(p[0] > STDERR_FILENO && p[1] > STDERR_FILENO);
pid_t child = fork();
assert(child >= 0);
if (child == 0) {
close(p[0]);
if (dup2(p[1], STDOUT_FILENO) < 0) _exit(126);
close(p[1]);
execl("/proc/self/exe", "lifecycle", "child", (char *)NULL);
_exit(127);
}
close(p[1]);
FILE *in = fdopen(p[0], "r");
assert(in != NULL);
long reported;
assert(fscanf(in, "%ld", &reported) == 1);
assert(reported == (long)child);
assert(fclose(in) == 0);
siginfo_t info = {0};
assert(waitid(P_PID, child, &info, WEXITED | WNOWAIT) == 0);
assert(info.si_code == CLD_EXITED && info.si_status == 7);
int status;
assert(waitpid(child, &status, 0) == child);
assert(WIFEXITED(status) && WEXITSTATUS(status) == 7);
errno = 0;
assert(waitpid(child, &status, WNOHANG) == -1 && errno == ECHILD);
puts("exec preserved PID; exit 7 observed; child reaped");
}
ここで dup2() が作る標準出力には CLOEXEC がありません。元のパイプ記述子には CLOEXEC があり、明示的にも閉じます。最終的な標準出力にまで CLOEXEC を付けると、新プログラムはそこから PID を報告できません。assert は実験の前提を表すため、NDEBUG を定義せずにコンパイルしてください。実サービスでは割り込み、資源不足、後処理にも通常のエラー処理が必要です。
6. カーネルスレッドと現場の調査
カーネルスレッドは通常 mm == NULL で、実行時に active_mm を借りることがあります。task、カーネルスタック、スケジューリングポリシーを持ち、ブロックもします。常に init_mm を使うという前提は置けません。
停止にはスレッドの協力が必要です。 kthread_stop() は停止を要求し、起床させ、終了を待ちます。関数側が kthread_should_stop() を確認し、待機経路も確認の機会を与える必要があります。永久に完了しない待機にはまっていれば、停止要求だけでは終了できません。
workqueue は管理されたワーカープールで処理し、投入ごとに専用スレッドを新設するわけではありません。モジュールやデバイスの終了時は、新規処理を止め、進行中の処理をキャンセルまたは待機し、最後にメモリを解放します。停止を要求しただけではデータ参照がなくなったと証明できません。
/proc/PID/status の Pid、Tgid、NSpid、Threads から調べ、/proc/PID/task/TID/ のスレッド表示へ進みます。/proc/PID/stat の括弧付き comm には空白が入り得るため、空白分割と固定インデックスでは正しく扱えません。R なら待ち時間、D なら待機対象、Z なら親の回収ロジックを調べ、記号から原因へ進みます。