更新履歴(1件・最終更新 2026年09月05日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 技術的な主張・コード・注意事項を維持し、生成手順・終了方針・監視方法を小見出しと比較表で整理した。
- 初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.22170940)
以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。
小村 豪(2026)「親が落ちたあとに何が残るか ── Job Objectで子プロセスを飼う」合同会社小村ソフト. https://comcomponent.com/blog/windows-job-object-child-process-lifetime/
- DOI(登録済みアーカイブ)
- 10.5281/zenodo.22170940
- DOI(前回登録した版)
- 10.5281/zenodo.22170941
UIをTask Managerで終了したのに、カメラが再オープンできない。監視アプリは落ちているのに、SDKのヘルパーがCOMポートを握ったままになっている。親を再起動すると二重起動になり、共有メモリや名前付きパイプに問題が起きる。装置SDKを別プロセスに分離すると、こうした「親が落ちたあと」の事故に出会います。
原因を考える出発点は、Windowsでは、親プロセスが終了しても子・孫のプロセスは自動では終了しないことです。起動時の親子関係と、終了時の寿命管理は別物です。この空白を埋める道具が Job Object です。
この記事では、まず親の終了処理だけでは足りない理由を確認し、次にJobへの入れ方・終了方針・監視方法を整理します。最後に、装置連携で起きる事故と調査手順へつなげます。
対象読者は、装置SDKを別プロセスに分離しているWinForms / WPF / サービスの開発者です。前提環境はWindows 10/11(入れ子ジョブとPROC_THREAD_ATTRIBUTE_JOB_LISTを使う箇所)で、コードはC++(Win32 API)とC#(.NET 6以降)で示します。難易度は中級です。
本記事は、「応答なし」・シャットダウン・スリープ復帰・名前付きパイプの各記事の延長として、プロセスの外側の寿命を扱います。
1. まず結論
設計の出発点は、「どう終了させるか」より先に「何を残してはいけないか、何を残したいか」を決めることです。
Job Objectはプロセスツリーを一つの単位にする仕組みです。ただし、親の消失と同時に子孫を強制終了させることと、その子孫のダンプや最終状態を残すことは、そのままでは両立しません。装置の占有を回収するのか、診断材料を先に残すのかを決めます。
- 寿命を管理する単位は、親子の系譜ではなくJobです。プロセスのグループに制限・通知・一括終了を付けます。一度所属させたプロセスは終了まで外れられず、Windows 8以降では入れ子にできます。12
- 子を走らせる前に、Jobへの所属を確定させます。起動後にAssignすると、その間に生まれた孫を取りこぼします。
CREATE_SUSPENDEDと生成時のJOB_LISTでは、閉じられる競合窓も異なります(4章)。 - 自動回収と診断情報の保存は、終了方針として選びます。KillOnJobCloseの発火条件は「最後のJobハンドルが閉じること」です。親のクラッシュには強い一方、道連れで強制終了される子孫の事後解析には弱いため、両方が必要なら監視側が先に採取してから終了させます(5章)。3
計測アプリが欲しいのは、殺すこと自体ではありません。装置を占有したまま残さないことと、異常終了を観測できることです。
| 知りたいこと | 読む章 |
|---|---|
| なぜ親の終了処理だけでは足りないのか | 2〜3章: 親子関係とJobの役割 |
| 子孫を取りこぼさず管理するには | 4〜6章: 生成・終了方針・監視 |
| SDKやサービスで何が問題になるのか | 7〜9章: 事故例・資源制限・入れ子とbreakaway |
| 現場で何を調べ、どう選ぶのか | 10〜11章: 調査手順と判断表 |
以下の知識マップは、各要素の関係を見返すためのものです。仕組みから読みたい場合は、2章へ進んでください。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. なぜ WaitForExit では足りないのか
終了を「待つ」ことと、寿命を結びつけることは別
Process.WaitForExit() は「親が子の終了を待つ」APIです。この記事で考えるのは逆方向、親が先に死んだときに子をどうするかです。Windowsの親子関係だけでは、親の終了は子へ伝わりません。
Process.Kill() や CloseMainWindow() も、それ自体で孫プロセスと孫が握る装置ハンドルまで面倒を見るものではありません。終了したプロセス自身のハンドルは解放されますが、問題は生き残った子孫が握っているハンドルです。
.NET の Kill(entireProcessTree: true) は子孫をたどって終了させますが、列挙中の生成や親が先に死んだ場合の取りこぼしがあり、親自身がクラッシュした後には呼ばれません。親の後始末コードだけに依存しない仕組みが必要です。
親が終了する主な理由ごとに、現場に何が残るかを並べます。
| 親が死ぬ理由 | 子に起きること | 現場に残るもの |
|---|---|---|
| UIを×で閉じ、終了処理が不完全 | 何も起きない(Process オブジェクトが破棄されるだけ) |
ヘルパープロセス、カメラのロック |
| Task Managerで親だけKill | 子は生存し続ける | COMポート、USB、共有メモリ |
| 未処理例外でクラッシュ | 親の finally が走る保証はない |
一時ファイル、排他ロック |
| サービス停止のタイムアウト | SCMが面倒を見るのは親だけ | セッション0に子が残留 |
flowchart TB
accTitle: 親の寿命と装置の占有寿命のずれ
accDescr: 親プロセスの終了で親の待機とProcessオブジェクトは消えるが、子と孫のプロセスは生き続け、装置ハンドル・名前付きパイプ・ロックファイルの占有も残り続ける
parent["親プロセスの終了"] --> gone["消える:親の待機・Processオブジェクト"]
parent --> live["残る:子・孫プロセス"]
live --> dev["装置ハンドルの占有"]
live --> pipe["名前付きパイプのサーバ側"]
live --> lock["ロックファイル・共有メモリ"]
図1: 親の寿命と装置の占有寿命はずれている。親側の後始末コードは、親が異常な死に方をしたときほど走らない。
対象は「OSは動いていて、親だけが終了した」ケース
シャットダウンやスリープでOSが全体を止める流れは、シャットダウン記事とスリープ復帰記事の領分です。本記事は、OSは元気なまま、親だけが終了するケースを扱います。計測の現場ではこちらのほうが頻度が高く、残留にも気づきにくいのです。
3. Job Objectとは何か
基本操作は、作る・所属させる・設定する・調べる
Job Objectは、プロセスのグループを一つの単位として管理するカーネルオブジェクトです。基本操作を役割で分けると、次の4つになります。14
| API | 役割 |
|---|---|
CreateJobObject |
まだプロセスが所属していないJobを作る |
AssignProcessToJobObject |
プロセスをJobへ所属させる |
SetInformationJobObject |
制限などを設定する |
QueryInformationJobObject |
CPU時間・ページフォールト・プロセス数などの会計情報を読む |
所属は不可逆で、プロセスが終了するまで外れられません。また、会計情報には終了したプロセスの分も含まれます。
所属したプロセスが CreateProcess で作る子は、既定で同じJobに所属します。つまり、孫も曾孫も自動で入ることがJobの価値の中心です。1 ただし、breakawayやWMIによる代理起動など、所属から外れる経路は9章で分けて確認します。
flowchart TB
accTitle: Job Objectの基本構造
accDescr: 親プロセスが作ったJob Objectに子と孫が所属し、Jobはプロセス木単位で制限の強制・完了ポートへの通知・一括終了を行う
parent["親プロセス"] --> job["Job Object"]
job --> child["子(装置SDKホスト)"]
child --> gc1["孫(メーカーヘルパー)"]
job -.-> lim["制限(メモリ・CPU)"]
job -.-> note["通知(完了ポート)"]
job -.-> kill["一括終了"]
図2: Jobは「制限」「通知」「一括終了」の3つをプロセス木単位で提供する容器である。
OSの世代差と、サンドボックスとの違い
Windows 7以前は1プロセス1ジョブ、Windows 8から入れ子(複数所属)が可能になりました。5 本文はWindows 10/11を前提とし、Windows 7以前で注意する点は9章とFAQで扱います。
Jobに入れるだけで、コンテナやサンドボックスになるわけではありません。ネットワークアクセスは制限できず、アクセストークン(権限)は別の仕組みです。UI制限だけでセキュリティ境界を作ることもできません。本記事での役割は、あくまでプロセスツリーの寿命と資源を一つの単位にすることです。
4. 正しい入れ方 ── 生成と所属の競合
起動してからAssignすると、その間に孫が生まれる
子プロセスは、走り始めた最初の数ミリ秒で孫を生むことがあります。SDKのヘルパー起動は典型です。Process.Start() の後でPIDを取得してAssignすると、Assignより先に生まれた孫がJobの外に出ます。
flowchart TB
accTitle: 走り始めてから入れると孫を取りこぼす
accDescr: Process.Startの直後から子は走っており、AssignProcessToJobObjectを呼ぶまでの競合窓の間に子が生んだ孫はJobの外に出てしまう
s["Process.Startで子が走り出す"] --> w["Assignまでの競合窓"]
w --> g["この間に生まれた孫"]
g --> out["Jobの外で走り続ける"]
s --> a2["AssignProcessToJobObject"]
a2 --> in2["以後に生まれる孫だけ入る"]
図3: 競合窓は数ミリ秒でも、SDKヘルパーの起動はまさにそこで起きる。
手順A: 停止状態で生成し、所属させてから走らせる
互換性の広い古典的な方法は、CREATE_SUSPENDED を使う手順です。子が先に動いて孫を生む窓を、次の順序で閉じます。67
CreateJobObjectでJobを作るSetInformationJobObjectで制限を先にセットするCREATE_SUSPENDEDを付けてCreateProcessする(初期スレッドは走らない)AssignProcessToJobObjectで入れる- 失敗したらResumeせず、その場で
TerminateProcess(Jobの外で1命令も走らせない) ResumeThreadで走らせる
手順Aが閉じるのは、孫の生成との競合窓です。親自身のクラッシュに対する窓は残ります。手順3と4の間で親がクラッシュすると、まだJobに入っていないサスペンド状態の子が残ります。走りはしませんが、自動では消えません。
親のクラッシュ耐性まで含めてこの窓を閉じたい場合は、次の手順Bを使います。
flowchart TB
accTitle: SUSPENDEDで起動してからJobへ入れる手順
accDescr: Jobを作って制限を設定し、CREATE_SUSPENDEDで子を起動してAssignProcessToJobObjectで所属させ、失敗したらResumeせずTerminateProcessで止め、成功したらResumeThreadで走らせる
a["CreateJobObjectでJobを作る"] --> b["SetInformationJobObjectで制限"]
b --> c["CREATE_SUSPENDEDで子を起動"]
c --> d["AssignProcessToJobObject"]
d -->|"成功"| e["ResumeThreadで走らせる"]
d -->|"失敗"| f["Resumeせず即Terminate"]
図4: 手順Aの骨格。「失敗したら走らせない」の分岐を省略した実装が、事故のときだけ野良プロセスを生む。
// C++: 手順Aの最小核(エラー処理は骨格のみ)
HANDLE job = CreateJobObjectW(nullptr, nullptr); // 無名でよい。継承させない
if (!job) return HRESULT_FROM_WIN32(GetLastError());
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation,
&limits, sizeof(limits))) {
DWORD err = GetLastError(); // CloseHandleで上書きされる前に退避
CloseHandle(job); // 制限なしのJobで子を走らせない
return HRESULT_FROM_WIN32(err);
}
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(exePath, cmdline, nullptr, nullptr, FALSE,
CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) {
DWORD err = GetLastError();
CloseHandle(job); // 起動リトライでJobハンドルを漏らさない
return HRESULT_FROM_WIN32(err);
}
if (!AssignProcessToJobObject(job, pi.hProcess)) {
DWORD err = GetLastError(); // Terminateで上書きされる前に退避
TerminateProcess(pi.hProcess, 1); // Jobの外では走らせない
// ハンドルを閉じ、errをエラーとして上げる
} else if (ResumeThread(pi.hThread) == (DWORD)-1) {
DWORD err = GetLastError();
TerminateProcess(pi.hProcess, 1); // サスペンドのまま置き去りにしない
// ハンドルを閉じ、errをエラーとして上げる
}
CloseHandle(pi.hThread);
// 成功したら job と pi.hProcess の所有権は呼び出し元の寿命管理オブジェクト
// (4章のC#ラッパー相当)へ移す。jobを閉じる=KillOnJobCloseの発火であり、
// pi.hProcess は6章で「誰が死んだか」の確定に使う
手順B: Windows 10以降では、Jobに入った状態で生成する
STARTUPINFOEX の属性リストに、PROC_THREAD_ATTRIBUTE_JOB_LIST でJobハンドルを載せます。そして、EXTENDED_STARTUPINFO_PRESENT フラグ付きで CreateProcess に渡します。フラグがないと拡張構造体として解釈されず、属性は無視されます。89
この方法では、初期スレッドが走る前にプロセスがJobへ所属します。「生成したが、まだ所属していない」という競合窓そのものがなくなるため、SUSPENDEDと、生成後にAssignして失敗を処理する分岐は不要です。
| 方法 | 所属を確定させるタイミング | 残る注意点 |
|---|---|---|
| 手順A: SUSPENDED → Assign → Resume | 子の生成後、初期スレッドを動かす前 | Assign前に親がクラッシュすると、停止した子が残る |
| 手順B: JOB_LISTで生成 | プロセスの生成時 | Windows 10以降が必要。属性リストと拡張起動フラグを指定する |
flowchart TB
accTitle: 手順Aと手順Bの競合窓の違い
accDescr: 手順Aは停止状態で生成してからAssignとResumeを行うため失敗時に終了させる分岐が必要だが、手順Bは属性リストにJobを載せて生成するため生まれた時点で所属済みで、競合窓も失敗分岐も存在しない
a1["手順A:停止状態で生成"] --> a2["Assignで所属"]
a2 --> a3["Resumeで始動"]
a2 -.-> a4["失敗分岐が必須"]
b1["手順B:属性リストで生成"] --> b2["生まれた時点で所属済み"]
b2 -.-> b3["競合窓も失敗分岐もない"]
図5: 手順Aは「入れてから走らせる」、手順Bは「入った状態で生まれる」。Windows 10以降を前提にできるなら、選ぶ理由は競合窓と失敗分岐の有無そのもの。
どちらの手順でも避けたい3つの実装
Process.Start()したあとでPIDを取って入れる(孫が先に出る)- Jobハンドルを子に継承させる(親が死んでも子がハンドルを握り続け、KillOnJobCloseが発火しなくなる ── 5章)
- Assignの失敗を握りつぶして運転を続ける(Jobの外の装置プロセスは、次の事故の主役になる)
.NETでは、Jobハンドルの所有者をコードに表す
System.Diagnostics.Process にJobの概念はなく、公式ラッパーもありません。P/InvokeかCsWin32で薄いラッパーを書きます。
ポイントは、JobハンドルをSafeHandleで包み、IDisposable にすることです。最後のJobハンドルを Dispose() で閉じるとKillOnJobCloseが発火します。「ラッパーの寿命が子ツリーの寿命」という設計意図を、所有権として表せます。
以下はハンドル所有を示す骨格です。生成処理は手順A/BのP/Invoke側で行い、実プロジェクトではCsWin32の設定やunsafe指定なども必要です。
// C#: Jobハンドルの所有だけを担う薄いラッパー(生成は手順A/BのP/Invoke経由で)
sealed class ChildProcessJob : IDisposable
{
private readonly SafeFileHandle _job; // フィールドで保持し続けること
public ChildProcessJob()
{
_job = PInvoke.CreateJobObject(default, null);
if (_job.IsInvalid) throw new Win32Exception();
var limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
limits.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!PInvoke.SetInformationJobObject(_job,
JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation,
&limits, (uint)sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
{
int err = Marshal.GetLastWin32Error(); // Dispose前に退避
_job.Dispose(); // KillOnJobCloseのないJobを渡さない
throw new Win32Exception(err);
}
}
public void Dispose() => _job.Dispose(); // ここで配下のツリーが終了する
}
逆に、閉じるつもりがないのにハンドルを閉じれば、子ツリーを終了させてしまいます。ラッパーへの参照は、親の生存期間を通して保持してください。保持しなければ、GCがSafeHandleを回収した瞬間に、子ツリーが理由なく全滅します。
5. KillOnJobClose ── 「飼う」と「一緒に死ぬ」
発火条件は「親の死」ではなく「最後のハンドルの閉鎖」
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE は、最後のJobハンドルが閉じたときに、配下の全プロセスを終了させる制限フラグです。3
親が例外で終了しても、Task Managerで殺されても、サービスが強制停止されても、カーネルはそのプロセスのハンドルをすべて閉じます。それが最後のJobハンドルなら、子も孫も終了します。親の終了処理が走ることを前提にしない点が、この仕組みの強さです。10
ただし、子にJobハンドルを継承させると、親の終了後もハンドルが残ります。その場合は「最後のハンドル」にならず、子ツリーは終了しません。
flowchart TB
accTitle: KillOnJobCloseのタイムライン
accDescr: 親が正常終了でもクラッシュでも強制終了でも、カーネルが親の全ハンドルを閉じ、それが最後のJobハンドルならJobが閉じて配下のプロセス木が一括終了する
die["親の消滅(クラッシュ含む)"] --> close["カーネルが全ハンドルを閉じる"]
close --> last{"最後のJobハンドル?"}
last -->|"はい"| killall["配下のツリーを一括終了"]
last -->|"いいえ(継承あり)"| stay["子は生き続ける"]
図6: 発火条件は「親の死」ではなく「最後のハンドルが閉じたこと」。だからハンドルを子に継承させてはいけない。
すぐ回収する方針と、診断材料を残す方針を選ぶ
強制終了で失うのは、装置の占有だけではありません。道連れで終了させられる子・孫のクラッシュダンプを取る機会、最後の有効フレーム、書きかけの計測ファイルをフラッシュする機会も失います。
親自身のクラッシュダンプは別です。WERは親の存命中に未処理例外を処理するため、親のダンプはハンドル閉鎖より前に書けます。ここで問題にしているのは、強制終了される側の子・孫の事後解析材料です。
| 方針 | 向く現場 | 失うもの |
|---|---|---|
| KillOnJobCloseあり | 装置の二重オープンが最悪の現場 | 事後解析の材料、最後のサンプル |
| なし(監視だけ) | ダンプとログが資産の現場 | 放置すると孤児プロセス、ポート占有 |
なし+監視プロセスが TerminateJobObject |
制御用サービスが別にあるとき | 実装が二重になる |
判断の材料として、「残してはいけないもの」と「残したいもの」を先に列挙してください。
残してはいけないもの: カメラ/デジタイザのオープン、シリアル・USBの排他、名前付きパイプのサーバ側、ライセンスドングルのセッション、共有メモリとロックファイル。
残したいもの: クラッシュダンプ、最後の有効フレーム/カウンタ、装置を安全側に戻すコマンドの送信機会(可能なら殺す前に送る)。
flowchart TB
accTitle: 殺すか、監視だけにするか
accDescr: 装置の二重オープンが最悪ならKillOnJobCloseを付け、クラッシュダンプや最終フレームが資産なら付けずに監視し、別の制御サービスがあるならそちらからTerminateJobObjectを呼ぶ
q{"落ちた瞬間に何を守る?"} -->|"装置の解放が最優先"| k["KillOnJobCloseあり"]
q -->|"ダンプと最終状態が資産"| m["監視のみ(殺さない)"]
q -->|"制御サービスが別にある"| t["監視側がTerminateJobObject"]
図7: 「殺すか」ではなく「落ちた瞬間に何を守るか」で選ぶ。両方欲しい場合は、監視側が先にダンプを採ってから畳む3行目の構成になる。
監視側には、親が生きているうちにJobハンドルを渡す
表の3行目、監視プロセスが TerminateJobObject で終了させる構成には準備が要ります。監視側は、親が死ぬ前にJobハンドルを手に入れておかなければなりません。
この記事の手順で作るJobは無名なので、親が消えた後に外から辿る方法はありません。受け渡し方は次の2つです。
| 方法 | 親が生きているうちに行うこと |
|---|---|
| 無名Jobのハンドルを複製する | DuplicateHandle で監視プロセスへハンドルを渡す |
| 名前付きJobを使う | 最初から名前付きで作り、監視側が OpenJobObject で開いて保持する |
名前はグローバルに衝突し得るため、固有のGUIDなどを含めます。この準備を忘れると、監視側は親の異常を見つけても、ツリーを終了させる手段を持てません。
ダンプ採取と装置の安全化は、強制終了より先に行う
KillOnJobCloseで終了させられる子には、TerminateProcess と同様、前触れがありません。未処理例外は発生しないため、子にWER(LocalDumps)を設定してあっても、その強制終了時のダンプは残りません。WERが拾えるのは、子が自分自身のクラッシュで終了するケースです。
「自動回収もダンプも」が要件なら、終了させる主体を監視側へ移します。対象が生きているうちにダンプを採取し、必要なら装置を安全側へ戻し、最後に TerminateJobObject で終了させる順序です。
flowchart TB
accTitle: 自動回収とダンプを両立させる畳み方
accDescr: 監視プロセスが異常を検知したら、先にダンプを採取し、必要なら装置を安全側に戻すコマンドを送り、最後にTerminateJobObjectで木を畳むことで、自動回収と事後解析を両立させる
det["監視側が異常を検知"] --> dmp["先にダンプを採取"]
dmp --> safe["装置を安全側へ戻す"]
safe --> term["TerminateJobObjectで畳む"]
図8: 「自動回収もダンプも」の唯一の解。順序が逆だと、採る対象がもう存在しない。
KillOnJobCloseで親の消失と同時に子を強制終了させる構成では、子が「装置を安全側に戻すコマンド」を送る機会もありません。それが必要な装置では、表の3行目の構成を選び、監視側が先に安全化コマンドを送ってから TerminateJobObject を呼びます。11
6. 完了ポートで「空になった」を待つ
Jobハンドルの待機だけでは、ツリーの終了を確認できない
Jobハンドルは、配下のプロセスが全部終了してもシグナル状態になりません。シグナルされるのは、ジョブ時間の上限超過で全プロセスが終了させられたときだけです。12
「子ツリーが空になったら次へ進む」ためには、I/O完了ポート(IOCP)をJobに関連付けます。1314 関連付けはJobが空のうち、プロセスを入れる前に済ませます。途中で関連付けると、その最中に状態が変わったプロセスの通知を取りこぼす可能性があります。15
4つのメッセージで、発生・終了・異常・ゼロ化を観測する
| メッセージ | 分かること |
|---|---|
JOB_OBJECT_MSG_NEW_PROCESS |
Jobにプロセスが加わったこと。孫の発生も検知する |
JOB_OBJECT_MSG_EXIT_PROCESS |
プロセスが終了したこと |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS |
アクセス違反など、異常終了コードで終了したこと。計測アプリでは特に重要13 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO |
アクティブプロセス数が0になったこと |
NEW_PROCESSのパケットに入るのは新しいPIDだけです。誰が生んだかという親子関係は分かりません。系譜が必要なら、ETWなど別の手段を足します。
sequenceDiagram
accTitle: 完了ポート通知の流れ
accDescr: Jobがプロセスの発生・終了・異常終了・ゼロ化のメッセージを完了ポートへ投げ、専用の監視スレッドがGetQueuedCompletionStatusで受け取り、UIスレッドには結果だけを渡す
participant J as Job Object
participant P as 完了ポート
participant W as 監視スレッド
participant U as UIスレッド
J->>P: NEW / EXIT_PROCESS
J->>P: ABNORMAL_EXIT / ZERO
W->>P: 完了パケットを待つ
P-->>W: メッセージとPID
W-->>U: 結果の通知だけ渡す
図9: GetQueuedCompletionStatusは専用スレッドで回す。UIスレッドで待つと、子の異常のたびにUIが「応答なし」になる。
専用スレッド・専用ポートで待ち、タイムアウト時には会計を照会する
この監視ループは、Jobの通知専用に作った完了ポートで回します。既存のI/Oと同じポートに相乗りすると、GetQueuedCompletionStatus が返った時点でパケットは取り出されています。キーが違うからと continue で捨てると、そのI/Oの持ち主は完了を永久に待ち続けます。失敗パケットの分岐も同じです。
共有する場合は、キーごとに持ち主へ配送する仕組みが別途必要です。この記事ではポートを分け、UIスレッドには結果だけを渡します。
また、1 Jobは1起動世代として、起動のたびに作り直す前提です。リトライで使い回すと TotalProcesses に前世代が含まれ、起動前の空Jobを区別するガードが効かなくなります。
// C++: 監視スレッドの骨格(通知の取りこぼしに備え、タイムアウト+会計で保険をかける)
DWORD msg; ULONG_PTR key; LPOVERLAPPED info;
bool treeEmpty = false;
while (!treeEmpty) {
if (!GetQueuedCompletionStatus(iocp, &msg, &key, &info, 5000)) {
if (info != nullptr) continue; // 失敗I/Oの完了パケット。監視は続ける
if (GetLastError() != WAIT_TIMEOUT) break; // ポート破棄などは終了
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct = {};
if (QueryInformationJobObject(job, JobObjectBasicAccountingInformation,
&acct, sizeof(acct), nullptr))
treeEmpty = (acct.TotalProcesses > 0 && // 起動前の空Jobを完了と誤認しない
acct.ActiveProcesses == 0); // ZERO通知が落ちたときの保険
// 前提: Jobは起動のたびに作り直す(1 Job=1起動世代)。リトライで
// Jobを使い回すとTotalProcessesは前世代を数えたままになり、
// このガードでは世代を区別できない
continue; // 照会失敗時は空と断定しない
}
if ((HANDLE)key != job) continue; // 関連付け時のCompletionKeyと突き合わせる。
// このポートはJob監視専用に作る前提
// (下の本文参照。共有ポートでこの捨て方を
// すると、他のI/Oの持ち主が永久に待つ)
DWORD pid = (DWORD)(UINT_PTR)info; // メッセージによってはPIDが入る
switch (msg) {
case JOB_OBJECT_MSG_NEW_PROCESS: /* 孫の発生をログ */ break;
case JOB_OBJECT_MSG_EXIT_PROCESS: /* 終了をログ */ break;
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:/* 異常終了: ダンプ確認へ */ break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: treeEmpty = true; break;
} // switchのbreakだけでは待機は終わらない
}
通知・会計・ハンドルは、確定できる情報が違う
通知は原則として到着保証がありません。保証されるのは、JobObjectNotificationLimitInformation で設定した上限の通知だけです。「通知が来ない=起きていない」とは判断できません。15
「空になったか」のような集計状態は、会計情報のポーリングを併用して確認します。ただし会計は集計カウンタなので、落ちたEXIT/ABNORMAL_EXITのPID・終了コード・異常かどうかまでは復元できません。そこはプロセスハンドルの保持とETWの領分です。
ACTIVE_PROCESS_ZEROも、正常終了の証拠ではありません。強制終了でゼロになったのかもしれず、メッセージ自体は区別しません。終了の質はEXIT/ABNORMAL_EXITと終了コードで判定します。
PIDだけで同じプロセスだと決めない
完了パケットのPIDは再利用されます。プロセスハンドルを保持していない限り、そのPIDがまだ同じプロセスを指す保証はありません。13
4章で保持した pi.hProcess が固定できるのは、直接起動した子のPIDだけです。SDKが生んだ孫については、NEW_PROCESSを受けた時点で OpenProcess してハンドルを取り、以後はそのハンドルを基準に突き合わせます。
それでも、通知からOpenまでの短い窓は残ります。その間にPIDが再利用されれば、別プロセスを掴みます。個体の同定まで厳密に必要な場合は、ETWのプロセス開始イベントなど、生成時刻を持つテレメトリでも裏を取ります。
flowchart TB
accTitle: 通知だけに頼らない監視
accDescr: 完了ポートのメッセージは通知目的で到着保証がないため、会計情報のポーリングとプロセスハンドルの保持を併用して、取りこぼしとPID再利用に備える
n["完了ポートの通知"] --> miss["到着保証なし(通知目的)"]
miss --> poll["会計情報のポーリング併用"]
n --> pid["PIDは再利用される"]
pid --> hold["プロセスハンドルを保持"]
図10: 通知は主経路、会計とハンドルは保険。両方そろって初めて「観測できている」と言える。
なお、かつては「Jobが空になるのを待つスレッドをTerminateThreadで殺す」ような実装が出回りましたが、完了ポートがあれば待ちスレッドの強制終了は不要です。Raymond Chenも、この古いパターンを完了ポート方式へ書き換える記事を出しています。16
7. 計測・装置連携で実際に起きる事故
ここまでの仕組みを、装置の現場で起きる6つの事故に当てはめます。各例では、原因と対処に加えて「何が残ったか」を確認します。
事故1: 親だけ死んでカメラが開いたまま
メーカーSDKがフレーム転送用のヘルパープロセスを持つ構成で、親UIをTask Managerで落とすとヘルパーだけが残ります。再起動した親はSDKの初期化で device busy。装置の電源を入れ直すまで復旧しない現場もあります。
残ったもの: ヘルパープロセスとカメラの排他オープン。
flowchart TB
accTitle: 親だけ死んでカメラが開いたまま
accDescr: UIの強制終了で親は消えるがSDKのヘルパープロセスは残り、カメラのハンドルを握ったままになるため、再起動した親はdevice busyで再オープンに失敗する
kill9["UIをTask Managerで終了"] --> dead["親は消える"]
dead --> helper["SDKヘルパーは残る"]
helper --> busy["カメラを握ったまま"]
busy --> fail["再起動後にdevice busy"]
図11: 「プロセスは消えたのに装置が開けない」の正体。犯人はTask Managerに表示された名前のプロセスではないことが多い。
事故2: 孫がJobの外に出る
経路は2つあり、対処を分ける必要があります。
(a) SDKが自分のJobを使っている場合。相手がすでに別のJobに入っているケースです。Windows 7では1プロセス1ジョブのため、こちらのAssignが失敗します。Windows 8以降では入れ子で拾えますが、こちらのJobにUI制限があると入れ子自体ができません(9章)。
(b) SDKが CREATE_BREAKAWAY_FROM_JOB で孫を生む場合。こちらのJobがBREAKAWAY_OKを許可している場合にだけ成立し、孫は最初から木の外に生まれます。入れ子でも拾えません。許可しなければSDK側の生成が失敗するため、監視を優先するなら、許可せず失敗として検知するのが基本です。
残ったもの: 監視の外で走る孫と、失敗を握りつぶしたAssign。
flowchart TB
accTitle: 孫がJobの外に出る2経路
accDescr: SDKが自分のJobを使っている経路はWindows 8以降なら入れ子で拾える余地があるがUI制限を付けていると失敗し、SDKがブレイクアウェイで孫を生む経路はこちらが許可した場合にだけ成立して孫は最初から木の外になる
g["孫がJobの外に出る"] --> ja["(a)SDKが自分のJob"]
g --> jb["(b)breakawayで生成"]
ja --> nest["Win8以降は入れ子で拾う"]
nest -.-> ui["UI制限があると失敗"]
jb --> allow["許可した場合のみ成立"]
allow -.-> out["孫は最初から木の外"]
図12: 同じ「外に出る」でも、(a)は入れ子で拾える余地が残り、(b)は許可した時点で拾えないことが確定する。対処は経路の特定から始まる。
事故3: サービスから起動した装置プロセス
サービス停止時にSCMが待つのは親だけで、セッション0で生んだ子・孫は停止処理の管轄外です。Job+KillOnJobCloseはこの管轄外まで面倒を見ます。サービスと対話セッションをまたぐ構成自体の設計はユーザー境界の記事に委ねます。
残ったもの: セッション0の残留プロセスと、次回起動時の二重起動判定の誤爆。
事故4: 1か月後に落ちる子
Jobの会計(PeakJobMemoryUsed、I/Oカウンタ、総プロセス数)を定期記録していないと、「どの世代のヘルパーがいつから膨らんだか」が事後に追えません。17 ハンドルリークで1か月後に落ちる構造は産業用カメラの長期障害記事で解剖したとおりで、Jobの会計はその調査の入口になります。
残ったもの: 原因確定に足りないログ。
事故5: 親の終了処理が子の終了を待つ
UIスレッドで WaitForExit やツリーの終了待ちをすると、子が固まった日に親が「応答なし」になります。終了待ちはIOCPスレッドに任せ、UIには進捗と打ち切りボタンだけを置きます。仕組みは「応答なし」記事のとおりです。
残ったもの: 道連れでハングした親。
flowchart TB
accTitle: 終了待ちを親のUIスレッドでやらない
accDescr: UIスレッドで子の終了を待つと子のハングが親の応答なしに伝播するため、終了待ちはIOCPの監視スレッドに任せ、UIスレッドには進捗表示と打ち切りボタンだけを置く
w2["UIスレッドで子の終了を待つ"] --> h2["子がハングした日"]
h2 --> f2["親も応答なし(道連れ)"]
ok2["IOCPスレッドで待つ"] --> u2["UIは進捗と打ち切りだけ"]
図13: 子の異常を観測する側が、子の異常で固まってはいけない。待つ場所を分けるだけで道連れは消える。
事故6: デバッガ配下でだけ失敗する
開発ツールやランチャーが、あなたの親プロセスをすでにどこかのJobに入れていることがあります。Windows 8以降は入れ子で大抵は救われますが、7以前の装置PCではAssignが ERROR_ACCESS_DENIED になり、「開発機でだけ動かない」「本番でだけ動かない」の差が生まれます。まず IsProcessInJob で自分の所属を確認するのが定石です。18
残ったもの: 環境差の原因を特定できない検証時間。
8. 制限は何を付けるか
制限には、目的と副作用がある
計測アプリで意味がある制限に絞って、使う理由と副作用を並べます。319
| 制限 | 使う理由 | やりすぎると |
|---|---|---|
| KILL_ON_JOB_CLOSE | 親消失で装置を解放 | 事後解析の材料が消える |
| ACTIVE_PROCESS | SDKの暴走的な子の増殖を止める | 正常なヘルパーまで生成拒否 |
| JOB_MEMORY / PROCESS_MEMORY | 長期稼働リークの上限 | 巨大な画像バッファの確保が失敗し始める |
| DIE_ON_UNHANDLED_EXCEPTION | 無人機でエラーダイアログを出さない | 対話デバッグが辛くなる |
| CPUレート制御 | 画像処理の子がUIを餓死させない | フレーム期限に間に合わない |
| BREAKAWAY_OK | 別ジョブが必要なSDKに脱出路を残す | 監視対象から消える |
| UI制限 | サンドボックス的な締め付け | 入れ子が壊れる(9章) |
通知上限は観測用、強制上限は拒否・終了用
「通知用の緩い上限」と「超えたら止める上限」は分けます。JobObjectNotificationLimitInformation は超過を通知するだけで、プロセスは走り続けます。15
Extended Limitの上限は強制されますが、強制の形は上限ごとに違います。3
| 上限 | 超過時に起きること |
|---|---|
| メモリ上限 | 超過するコミット操作が失敗する。プロセス自体は生きている |
| ACTIVE_PROCESS | 超過する生成・所属が失敗する。所属で超過したそのプロセスは終了させられる |
| プロセス時間(PROCESS_TIME) | 超過したそのプロセスだけが終了させられる |
| ジョブ時間(JOB_TIME) | 集計値に対する上限で、既定ではJob配下の全プロセスが終了させられる |
この違いを知らずに設定すると、「生成直後に消えるヘルパー」を別の障害と誤診します。長期稼働では、まず通知上限で観測し、傾向が分かってから強制上限を決める順番が安全です。
flowchart TB
accTitle: 通知用の上限と強制の上限
accDescr: JobObjectNotificationLimitInformationの上限は超過を通知するだけでプロセスは走り続け、Extended Limitの上限は強制で、メモリ上限は操作の失敗、ACTIVE_PROCESSは生成・所属の失敗、時間上限はプロセスの終了として効く
lim2{"上限の目的は?"} -->|"観測したい"| ntf["通知上限:超えても走る"]
lim2 -->|"止めたい"| enf["強制上限:拒否または終了"]
ntf --> log2["会計ログで世代を特定"]
enf --> die2["コミット失敗・生成拒否・終了"]
図14: 同じ「上限」でも通知と強制は別物で、強制の効き方も上限ごとに違う。観測なしにいきなり強制上限を張ると、正常動作の山で誤爆する。
CPUレート制御と周期処理の関係はソフトリアルタイム記事に委ね、ここでは「装置プロセスがUIを食う」対策程度に留めます。
9. 入れ子、Breakaway、すでにJobに入っている相手
入れ子は「プロセス集合の包含」と考える
Windows 8以降の入れ子のルールは4つで整理できます。5
- 親ジョブが広い集合、子ジョブはその部分集合(この包含が成立しない順序でAssignすると失敗する)
- 主要な資源制限は、チェーン上で最も厳しいものが実効になる
- UI制限が付いたジョブは入れ子にできない
- 通知はチェーン上のすべての親ジョブの完了ポートにも届く(子ジョブ側にポートがなくてもよい)
flowchart TB
accTitle: 入れ子ジョブの階層と実効制限
accDescr: 親ジョブは広い集合で子ジョブはその部分集合になり、主要な資源制限はチェーン上で最も厳しい値が実効になる。UI制限が付いたジョブは入れ子にできない
pj["親ジョブ(広い集合)"] --> cj["子ジョブ(部分集合)"]
cj --> pr["所属プロセス"]
pj -.-> eff["実効制限=最も厳しい値"]
cj -.-> eff
ui["UI制限付きジョブ"] -.-> no["入れ子にできない"]
図15: 入れ子は「集合の包含」で考える。UI制限は入れ子を壊すので、寿命管理のJobには付けないのが無難。
Breakawayは、最初からJobの外で生成する経路
breakawayは、CreateProcess で生まれる子孫が木から抜ける正規の経路です。3
| Job側の設定 | 子がJobの外で生まれる条件 |
|---|---|
JOB_OBJECT_LIMIT_BREAKAWAY_OK |
CREATE_BREAKAWAY_FROM_JOB を指定して生成する |
SILENT_BREAKAWAY_OK |
フラグ指定は不要。すべての子が外に生まれる |
SDKが自分でJobを使うために必要な場合もあります。ただし、その経路で生まれたプロセスは、一括終了からも監視からも外れます。許可するなら、抜けた先を誰が管理するかまで決めます。
flowchart TB
accTitle: Breakawayで木から抜ける経路
accDescr: JobにBREAKAWAY_OKが付いていると、CREATE_BREAKAWAY_FROM_JOBで生成された孫はJobの外に生まれ、一括終了と監視の対象から消える
j2["Job(BREAKAWAY_OKあり)"] --> c2["子プロセス"]
c2 -->|"通常の生成"| in3["孫もJobの中"]
c2 -->|"BREAKAWAY指定の生成"| out3["孫はJobの外へ"]
out3 --> lost["監視と一括終了の対象外"]
図16: breakawayは「必要なSDKへの脱出路」と「監視の穴」の両方の顔を持つ。付けるなら、抜けた先を誰が看取るかまで決める。
WMIの代理起動は、breakaway禁止でも防げない
WMIの Win32_Process.Create のように、第三者のプロセスが代理で起動する経路は別問題です。実際の親がWMIプロバイダーなので、生まれたプロセスは最初からJobの外にあります。breakawayを禁止しても、この穴は塞げません。1
SDKがこの経路を使っていないかは、NEW_PROCESSのログではなく、Process Explorerの親子関係で確認します。
既存Jobへの所属と、Windows 7以前の制約を先に確認する
相手がすでにJobに入っているとき(事故6)の手順は、IsProcessInJob で確認 → 入れ子が組めるならそのままAssign → 組めない(Windows 7、またはUI制限)なら設計を変える、の順です。18
Windows 7以前では、すでに別のJobに所属する相手へ二重にAssignすることはできません。BREAKAWAY_OKは、所属済みのプロセスを後から外すフラグではありません。SDK側が子の生成時にbreakawayを要求してくれる場合にだけ、その脱出路が成立します。それが望めなければ、起動前に「1プロセス1ジョブ」を前提として設計を変えます。12
親自身をJobに入れるかは、最後に判断する
最後に「自分のプロセスを自分のJobに入れる」設計にも触れておきます。親自身も配下に入れると、親のクラッシュ時にKillOnJobCloseの対象へ自分も含まれ、木全体の寿命が完全に一致します。ただしJobハンドルの持ち方を間違えると意図しない一斉終了になる諸刃の剣で、まずは「親は外、子ツリーだけ中」から始めるのが安全です。
10. 調査の仕方
調査は所属 → 会計と通知ログ → 装置の占有の順に進めます。プロセス名を目視するだけで、残留がないと結論しないための確認手順です。
- Process Explorer: プロセスのプロパティにJobタブがあり、所属Jobと制限が見えます。「このヘルパーはどのJobにいるのか」を最速で確認できます
IsProcessInJob: 自分や相手の所属をコードから確認する入口18QueryInformationJobObject: Basic Accounting(総プロセス数・CPU時間)とExtended Limit(PeakJobMemoryUsedなど)を定期記録する417- 完了ポートのログをファイルに残す: NEW_PROCESS / EXIT / ABNORMAL_EXITの時系列は、1か月後の調査で唯一の証拠になる
- 残留確認はPIDでやらない: 見るべきは装置ハンドルとパイプ名とロックファイル。「Task Managerにプロセスが見えない」は「装置が解放された」を意味しません
flowchart TB
accTitle: 残留調査の手順
accDescr: まずIsProcessInJobとProcess ExplorerのJobタブで所属を確認し、QueryInformationJobObjectで会計を読み、最後にプロセスの有無ではなく装置ハンドル・パイプ名・ロックファイルで残留を判定する
s1["IsProcessInJobで所属確認"] --> s2["Process ExplorerのJobタブ"]
s2 --> s3["QueryInformationJobObjectで会計"]
s3 --> s4["装置ハンドル・パイプ名で残留判定"]
図17: 調査は「所属→会計→占有」の順。プロセス名の目視だけで「残っていない」と結論しない。
11. ざっくり使い分け(判断表)
ここまでの選択を場面別にまとめます。終了方針は5章、監視の前提は6章、既存Jobとの関係は9章に戻って確認できます。
| 状況 | 推奨 |
|---|---|
| UI本体+装置SDKを別プロセスに出した | Jobに入れ、完了ポートで監視する |
| 親消失で装置が掴みっぱなしになるのが最悪 | KillOnJobCloseを付ける |
| 落ちた瞬間のフレームやダンプが資産 | KillOnJobCloseは付けず、監視側が安全化のうえ TerminateJobObject |
| メーカーSDKがヘルパーを生む | 生成をSUSPENDEDかJOB_LISTにし、NEW_PROCESSをログする |
Assignが ERROR_ACCESS_DENIED |
既存ジョブと入れ子可否を先に見る。UI制限を疑う |
| サービスから対話セッションの子を出す | UI制限を付けない。ユーザー境界記事の設計に戻る |
| 孫の終了をUIスレッドで待っている | やめる。IOCPスレッドに出す |
12. まとめ
子の寿命は、親の Process オブジェクトの寿命ではありません。親の終了も、子へは自動で伝わりません。Job Objectは、このプロセスツリーを一つの単位にして、制限・通知・一括終了を扱う仕組みです。
孫まで管理するには、子を走らせる前に所属を確定させます。手順AはSUSPENDED+Assign、Windows 10以降の手順BはJOB_LISTです。KillOnJobCloseは、親の終了理由にかかわらず最後のJobハンドルが閉じることで働きますが、道連れで終了させる子孫の事後解析材料も失います。
だからこそ、「落ちたあとに残してはいけないもの」と「残したいもの」を先に列挙し、即時回収か、監視側で採取・安全化してからの終了かを選ぶ。これが、この記事で伝えたい設計手順です。
次に書くならセッション0と対話セッションをまたぐプロセス起動の各論か、overlapped I/Oで装置を待つ話です。プロセスの「外側の寿命」を押さえたら、次はI/Oの寿命が待っています。
関連記事
- Windowsアプリが子プロセスを安全に扱うチェックリスト ── Job Object・終了伝播・標準入出力・watchdogのベストプラクティス
- Windowsアプリの「応答なし」はなぜ起きるか ── メッセージループとハングの仕組み
- 名前付きパイプの実務 ── Windowsプロセス間通信の定番を設計からセキュリティまで
- ハンドルリークで1か月後に落ちる ── 産業用カメラアプリ長期稼働障害の解剖(前編)
- Windowsでソフトリアルタイムをどこまでやれるか ── 実務ガイド
- 「同じPC」は同じ実行環境ではない ── AppData・HKCU・DPAPI・資格情報を隔てるユーザー境界
関連する相談領域
合同会社小村ソフトでは、カメラ・計測器・シリアル/USB装置と連携するWindowsアプリのプロセス分離設計、SDKヘルパーの残留や device busy といった装置占有トラブルの調査、長期稼働アプリの監視・自動回復の仕組みづくりを扱っています。「親を再起動すると装置が開けない」の一件からでもご相談ください。
参考リンク
-
Microsoft Learn, Job Objects. Job Objectがプロセスのグループを一つの単位として管理するカーネルオブジェクトであること、所属プロセスが作る子プロセスが既定で同じJobに関連付くこと(Win32_Process.Create経由は除く)、breakaway用の2つの制限フラグ、TerminateJobObjectによる一括終了、入れ子が使えない環境でのプロセス木の管理方法について。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). プロセスとJobの関連付けが解除できないこと、Windows 7以前は1プロセス1ジョブでWindows 8から複数所属(入れ子)が可能になったこと、入れ子での実効制限とbreakawayの伝播について。 ↩ ↩2
-
Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h). JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE(最後のJobハンドルが閉じたとき全プロセスを終了)、ACTIVE_PROCESS(同時アクティブプロセス数の上限)、JOB_MEMORY(ジョブ全体のコミット上限)、DIE_ON_UNHANDLED_EXCEPTION、BREAKAWAY_OK / SILENT_BREAKAWAY_OKの各制限フラグについて。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). Jobが終了したプロセスの分も含めて総プロセス数・CPU時間・ページフォールト数などの会計情報を保持し、QueryInformationJobObjectで取得できることについて。 ↩ ↩2
-
Microsoft Learn, Nested Jobs. 入れ子ジョブが親子の階層(子ジョブは親ジョブのプロセスの部分集合)を作ること、UI制限を設定したジョブは入れ子にできないこと、実効制限がチェーン上の最も厳しい値になること、通知が親ジョブチェーンの全完了ポートへ送られること、階層の終了が最下層から行われることについて。 ↩ ↩2
-
Microsoft Learn, Process Creation Flags. CREATE_SUSPENDED(初期スレッドをサスペンド状態で生成しResumeThreadまで実行しない)と、CREATE_BREAKAWAY_FROM_JOB(呼び出し元のJobにJOB_OBJECT_LIMIT_BREAKAWAY_OKが必要)について。 ↩
-
Raymond Chen, Closing the race window between creating a suspended process and putting it in a job (The Old New Thing). CREATE_SUSPENDEDで生成してからJobへ入れる古典手順と、その競合窓の閉じ方について。 ↩
-
Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). PROC_THREAD_ATTRIBUTE_JOB_LISTにより生成される子プロセスへ指定順にJobハンドルを割り当てられること、Windows 10 / Windows Server 2016以降でサポートされることについて。 ↩
-
Raymond Chen, A more direct and mistake-free way of creating a process in a job object (The Old New Thing). PROC_THREAD_ATTRIBUTE_JOB_LISTを使い、生成時点からプロセスをJobに所属させる方法について。 ↩
-
Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). KILL_ON_JOB_CLOSE付きのJobで親の消滅時に子孫をまとめて終了させる構成と、Jobハンドルを継承させないことの重要性について。 ↩
-
Microsoft Learn, TerminateJobObject function (jobapi2.h). Jobに関連付けられた全プロセスを、TerminateProcessを個々に呼んだのと同様に強制終了させることについて。 ↩
-
Microsoft Learn, Job Objects - Managing Job Objects. Jobオブジェクトがシグナル状態になるのはジョブ時間上限の超過で全プロセスが終了したときであること、最後のハンドルが閉じたときのJobの破棄と、KILL_ON_JOB_CLOSE指定時は閉鎖が全所属プロセスの終了を引き起こすことについて。 ↩
-
Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h). JOB_OBJECT_MSG_NEW_PROCESS / EXIT_PROCESS / ABNORMAL_EXIT_PROCESS / ACTIVE_PROCESS_ZEROなど完了ポートへ送られるメッセージの一覧、異常終了と判定される終了コード、PIDを返すメッセージではプロセスハンドルを保持していない限りPIDの再利用を否定できないこと、通知の配送が保証されないことについて。 ↩ ↩2 ↩3
-
Raymond Chen, How do I wait until all processes in a job have exited? (The Old New Thing). Jobハンドルの待機では「空になった」を検知できず、完了ポートのJOB_OBJECT_MSG_ACTIVE_PROCESS_ZEROで待つ必要があることについて。 ↩
-
Microsoft Learn, Job Objects - Job Limits and Notifications. 完了ポートの関連付けはJobが非アクティブなうちに行うのがよいこと(関連付け中に状態が変わったプロセスの通知を取りこぼす可能性の低減)、JobObjectNotificationLimitInformationで設定した上限を除きメッセージ配送が保証されないこと、通知上限では超過後もプロセスが走り続けることについて。 ↩ ↩2 ↩3
-
Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). 待ちスレッドをTerminateThreadで殺す古いパターンを、完了ポートベースの待機に書き換える方法について。 ↩
-
Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). プロセス単位・ジョブ単位のメモリ上限の設定と、PeakProcessMemoryUsed / PeakJobMemoryUsedによるピークメモリの取得について。 ↩ ↩2
-
Microsoft Learn, IsProcessInJob function (jobapi.h). プロセスが指定したJob(またはいずれかのJob)で実行されているかを判定できることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). Job単位でCPUレート(サイクルの割合や重み)を制御できることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
引数はなぜ壊れるか ── Windowsのコマンドライン引数の規則
Windowsでは引数の配列は存在せず、CreateProcessに渡るのは1本の文字列で、分割は受け取り側が行います。CommandLineToArgvW・CRT・.NETの分割規則と、.NETのArgumentList・C++での正しい組み立て方を解説します。
名前付きパイプの実務 ── Windowsプロセス間通信の定番を設計からセキュリティまで
Windowsのプロセス間通信の定番・名前付きパイプを実務目線で解説します。バイト/メッセージモードの選択、複数クライアントを捌くサーバー設計、ACLと偽装のセキュリティ、.NETのNamedPipeStreamまで、一次情報にもとづき整理します。
Win32スレッドプールAPI ── CreateThreadpoolWorkで「スレッドを作らない」並行処理
ネイティブコードでCreateThreadを乱立させていませんか。Vistaで刷新されたWin32スレッドプールAPIのwork・timer・wait・ioの4オブジェクト、クリーンアップグループ、コールバックでの禁止事項までを一次情報にもとづいて解説します。
DllMainとローダーロック ── 「DLLの初期化で何もするな」と言われる本当の理由
DllMainでLoadLibraryやスレッド同期をしてはいけないのはなぜか。全DLL通知を直列化するローダーロックの仕組みから、デッドロックが成立する典型シナリオ、遅延初期化などの正しい設計、ハング調査の手順までを一次情報で解説します。
スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方
条件変数のwaitは通知が来ていなくても目覚めることがあります(スプリアスウェイクアップ)。仕様がそれを許す理由をWindowsの実装から解き明かし、whileと述語で書く正しい待ち方をWin32・C++・C#のコードで示します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Job Objectはコンテナやサンドボックスですか?
- 違います。Job Objectは、プロセスのグループに「制限」「通知」「一括終了」を付けるカーネルオブジェクトです。メモリ・CPU・プロセス数の上限は課せられますが、ネットワークアクセスの制限はできず、アクセストークン(権限)も変わりません。UI制限はありますが、それだけでセキュリティ境界になるものではありません。分離やセキュリティが目的ならAppContainerやコンテナなど別の仕組みと組み合わせる必要があります。本記事で扱うのは「プロセスツリーの寿命と資源を一つの単位として扱う」用途です。
- Process.Kill()ではだめですか?
- Process.Kill()が終了させるのはそのプロセス1つだけで、孫プロセスには届きません。.NET Core 3.0以降のKill(entireProcessTree: true)は子孫をたどって終了させますが、プロセスツリーをその時点の親子関係から列挙する方式なので、列挙中に生まれたプロセスや、親が先に死んで系譜が切れたプロセスを取りこぼす余地があります。また、どちらの方法も「親自身がクラッシュしたとき」には呼ばれません。親の生死に関係なく子孫を回収したいなら、カーネルに寿命を預けるJob Object+KillOnJobCloseが確実です。
- 親がJobに入っていたら、子も自動でJobに入りますか?
- 既定では入ります。Jobに所属するプロセスがCreateProcessで作った子プロセスは、自動的に同じJobに所属します。例外はbreakawayです。JobにJOB_OBJECT_LIMIT_BREAKAWAY_OKが設定されていて子がCREATE_BREAKAWAY_FROM_JOBフラグ付きで生成された場合や、JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OKが設定されている場合、子はJobの外で生まれます。なお、WMIのWin32_Process.Create経由で作られたプロセスはJobに関連付きません。
- 一度入れたプロセスをJobから外せますか?
- 外せません。AssignProcessToJobObjectによる関連付けは不可逆で、プロセスが終了するまで所属は続きます。したがって設計の選択肢は「入れない」「breakawayで最初から外で生む」「別のJobを入れ子にする」の3つで、「あとから外す」はありません。この不可逆性が、生成前にJobを準備しておくべき理由でもあります。
- KillOnJobCloseは親のクラッシュでも効きますか?
- 効きます。JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEは「最後のJobハンドルが閉じたとき」に配下を終了させる仕組みで、親が正常終了しても、未処理例外で落ちても、Task Managerで殺されても、カーネルがプロセスの後始末としてハンドルを閉じるため発火します。ただしJobハンドルを子プロセスに継承させていると、親が死んでも子の持つハンドルが残るため「最後のハンドル」にならず、発火しません。Jobハンドルは継承させないでください。
- 完了ポートの通知は必ず届きますか?
- 届かないことがあります。公式ドキュメントは、JobObjectNotificationLimitInformationで設定した上限の通知を除き、完了ポートへのメッセージ配送は保証されないと明記しています。通知が来なかったことは事象が起きなかったことを意味しません。確実性が要る監視では、QueryInformationJobObjectによる会計情報のポーリングを併用し、プロセスハンドルを自分で保持して生死を確定させてください。
- .NETにJob Objectの公式APIはありますか?
- ありません。System.Diagnostics.ProcessにはJobの概念がなく、BCLにもラッパーはありません。P/Invokeで CreateJobObject / SetInformationJobObject / AssignProcessToJobObject を呼ぶか、Microsoft製のソースジェネレーターCsWin32で署名を生成して薄いラッパーを書くのが実務解です。JobハンドルをSafeHandleで包み、IDisposableのDisposeで閉じる設計にすると、「ラッパーの寿命=子プロセスツリーの寿命」というKillOnJobCloseの意味がコードにそのまま現れます。
- Windows 7の装置PCではどう設計すればよいですか?
- Windows 7以前は1プロセスが1つのJobにしか入れず、入れ子もできません。相手のSDKが自分でJobを使っていると、こちらのAssignProcessToJobObjectは失敗します。JOB_OBJECT_LIMIT_BREAKAWAY_OKは「すでに自分のJobに入っているプロセスが、CREATE_BREAKAWAY_FROM_JOB指定で子をJobの外に作ること」を許すフラグであり、入ってしまった後の二重Assignを通す魔法ではありません。つまり脱出路はSDK側が生成時にbreakawayを要求してくれる場合にだけ成立します。それが望めないなら、起動前に「自分のJobは1つだけ」という前提で設計を変えるしかありません。Microsoftのドキュメントも、入れ子が使えない環境ではbreakawayの2つの制限フラグで木を管理する方法を示しています。とはいえサポート切れのOSなので、可能なら移行が先です。