Historique des révisions (première version, publiée le 29 Aug 2026)
- Première publication
Vous avez terminé l’IU dans le Gestionnaire des tâches, et pourtant la caméra ne peut pas être rouverte. L’application de surveillance a disparu, et pourtant l’aide du SDK tient encore le port COM. Relancer le parent produit une seconde instance, et des problèmes apparaissent dans la mémoire partagée et les tubes nommés. Une fois que vous isolez un SDK de périphérique dans un processus séparé, vous rencontrez ces défaillances « après la mort du parent ».
Le point de départ pour penser la cause est que sous Windows, les processus enfants et petits-enfants ne se terminent pas automatiquement lorsque le processus parent se termine. La relation parent-enfant au lancement et la gestion de la durée de vie à la sortie sont deux choses différentes. L’outil qui comble cet écart est le Job Object.
Cet article confirme d’abord pourquoi le traitement de sortie du parent seul ne suffit pas, puis organise comment mettre des processus dans un Job, la politique de terminaison et la méthode de surveillance. Enfin, il relie cela aux défaillances qui surviennent dans l’intégration de périphériques et à la procédure d’investigation.
Le public visé est les développeurs WinForms / WPF / services qui isolent un SDK de périphérique dans un processus séparé. L’environnement requis est Windows 10/11 (pour les parties qui utilisent les Nested Jobs et PROC_THREAD_ATTRIBUTE_JOB_LIST), et le code est montré en C++ (Win32 API) et C# (.NET 6 ou plus récent). La difficulté est intermédiaire.
Cet article prolonge les articles « Ne répond pas », arrêt, veille/reprise et tubes nommés, et traite la durée de vie à l’extérieur du processus.
1. D’abord la conclusion
Le point de départ de la conception est de décider « ce qui ne doit pas rester, et ce que vous voulez conserver » avant de décider « comment terminer ».
Un Job Object est un mécanisme qui fait d’un arbre de processus une unité. Cependant, terminer de force les descendants au moment où le parent disparaît et conserver les vidages ou l’état final de ces descendants ne vont pas, tels quels, ensemble. Décidez si vous récupérez le périphérique que le processus détient ou si vous préservez d’abord le matériau de diagnostic.
- L’unité qui gère la durée de vie est le Job, pas la lignée parent-enfant. Il attache des limites, des notifications et une terminaison groupée à un groupe de processus. Une fois qu’un processus est associé, il ne peut pas partir jusqu’à sa sortie, et à partir de Windows 8 les Jobs peuvent être des Nested Jobs.12
- Réglez l’appartenance au Job avant que l’enfant ne s’exécute. Si vous faites Assign après le lancement, vous manquez les petits-enfants nés entre-temps.
CREATE_SUSPENDEDetJOB_LISTà la création ferment des fenêtres de course différentes (chapitre 4). - La récupération automatique et la conservation des informations de diagnostic se choisissent comme politique de terminaison. La condition de déclenchement de KillOnJobClose est « le dernier handle de Job se ferme ». C’est fort contre un plantage du parent mais faible pour l’analyse post-mortem des descendants terminés de force avec lui, donc si vous avez besoin des deux, le côté surveillance collecte d’abord et termine ensuite (chapitre 5).3
Ce qu’une application de mesure veut, ce n’est pas le meurtre lui-même. C’est ne pas laisser un processus qui détient le périphérique, et pouvoir observer les sorties anormales.
| Ce que vous voulez savoir | Chapitres à lire |
|---|---|
| Pourquoi le traitement de sortie du parent seul ne suffit pas | Chapitres 2–3 : la relation parent-enfant et le rôle du Job |
| Comment gérer les descendants sans en manquer | Chapitres 4–6 : création, politique de terminaison, surveillance |
| Ce qui devient un problème avec les SDK et les services | Chapitres 7–9 : cas de défaillance, limites de ressources, Nested Jobs et breakaway |
| Quoi investiguer sur le terrain, et comment choisir | Chapitres 10–11 : procédure d’investigation et tableau de décision |
La carte des connaissances ci-dessous sert à revoir comment les éléments se relient. Si vous préférez partir du mécanisme, passez au chapitre 2.
Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (21 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle
2. Pourquoi WaitForExit ne suffit pas
« Attendre » une sortie et lier les durées de vie sont des choses différentes
Process.WaitForExit() est une API pour « le parent attend que l’enfant se termine ». Ce que cet article considère est la direction opposée : que faire de l’enfant lorsque le parent meurt d’abord. Avec la seule relation parent-enfant de Windows, la sortie du parent n’est pas transmise à l’enfant.
Process.Kill() et CloseMainWindow() ne s’occupent pas non plus, par eux-mêmes, des processus petits-enfants ni des handles de périphérique que ces petits-enfants détiennent. Les handles du processus qui s’est terminé sont libérés, mais le problème est les handles détenus par les descendants qui ont survécu.
Kill(entireProcessTree: true) de .NET parcourt les descendants et les termine, mais il peut manquer des processus créés pendant l’énumération ou après que le parent est mort d’abord, et il n’est jamais appelé une fois que le parent lui-même a planté. Il vous faut un mécanisme qui ne dépend pas uniquement du code de nettoyage du parent.
Pour chacune des principales raisons pour lesquelles un parent se termine, voici ce qui reste sur le terrain.
| Pourquoi le parent meurt | Ce qui arrive à l’enfant | Ce qui reste sur le terrain |
|---|---|---|
| IU fermée avec le bouton X, traitement de sortie incomplet | Rien (l’objet Process est simplement disposé) |
Processus d’aide, verrou de caméra |
| Seul le parent tué dans le Gestionnaire des tâches | L’enfant continue de vivre | Port COM, USB, mémoire partagée |
| Plantage sur une exception non gérée | Aucune garantie que le finally du parent s’exécute |
Fichiers temporaires, verrous exclusifs |
| Délai d’expiration de l’arrêt du service | Le SCM ne s’occupe que du parent | Enfants restés en session 0 |
flowchart TB
accTitle: L'écart entre la durée de vie du parent et la durée d'occupation du périphérique
accDescr: Lorsque le processus parent se termine, l'attente du parent et son objet Process disparaissent, mais les processus enfants et petits-enfants continuent de vivre, et leur détention des handles de périphérique, des tubes nommés et des fichiers de verrou demeure aussi
parent["Le processus parent se termine"] --> gone["Parti : attente du parent, objet Process"]
parent --> live["Resté : processus enfants et petits-enfants"]
live --> dev["Détention des handles de périphérique"]
live --> pipe["Côté serveur des tubes nommés"]
live --> lock["Fichiers de verrou, mémoire partagée"]
Figure 1 : la durée de vie du parent et la durée d’occupation du périphérique sont décalées. Le code de nettoyage du parent a le moins de chances de s’exécuter précisément lorsque le parent meurt d’une mort anormale.
La cible est le cas où « l’OS tourne et seul le parent s’est terminé »
Les flux dans lesquels l’arrêt ou la veille arrête tout l’OS appartiennent à l’article sur l’arrêt et à l’article veille/reprise. Cet article traite le cas où l’OS reste en bonne santé et seul le parent se termine. Dans les environnements de mesure, c’est le cas le plus fréquent, et les processus restants sont plus difficiles à remarquer.
3. Ce qu’est un Job Object
Les opérations de base sont créer, associer, définir et interroger
Un Job Object est un objet noyau qui gère un groupe de processus comme une unité. Diviser les opérations de base par rôle donne les quatre suivantes.14
| API | Rôle |
|---|---|
CreateJobObject |
Créer un Job auquel aucun processus n’appartient encore |
AssignProcessToJobObject |
Associer un processus au Job |
SetInformationJobObject |
Définir des limites et d’autres réglages |
QueryInformationJobObject |
Lire les informations de comptabilité telles que le temps CPU, les défauts de page et le nombre de processus |
L’appartenance est irréversible ; un processus ne peut pas partir jusqu’à sa sortie. De plus, les informations de comptabilité incluent les totaux accumulés par les processus déjà terminés.
Un enfant qu’un processus membre crée avec CreateProcess appartient au même Job par défaut. Autrement dit, le cœur de la valeur d’un Job est que les petits-enfants et arrière-petits-enfants entrent automatiquement.1 Les chemins qui échappent à l’appartenance, tels que le breakaway et les lancements par procuration via WMI, sont examinés séparément au chapitre 9.
flowchart TB
accTitle: Structure de base d'un Job Object
accDescr: Un enfant et un petit-enfant appartiennent au Job Object que le processus parent a créé, et le Job applique des limites, envoie des notifications à un port d'achèvement et effectue une terminaison groupée par arbre de processus
parent["Processus parent"] --> job["Job Object"]
job --> child["Enfant (hôte du SDK de périphérique)"]
child --> gc1["Petit-enfant (aide du fournisseur)"]
job -.-> lim["Limites (mémoire, CPU)"]
job -.-> note["Notifications (port d'achèvement)"]
job -.-> kill["Terminaison groupée"]
Figure 2 : un Job est un conteneur qui fournit trois choses par arbre de processus : des limites, des notifications et une terminaison groupée.
Différences de génération d’OS, et en quoi cela diffère d’un bac à sable
Sous Windows 7 et plus tôt, un processus ne pouvait appartenir qu’à un job ; à partir de Windows 8, les Nested Jobs (appartenance multiple) sont devenus possibles.5 Le corps de cet article suppose Windows 10/11 ; les points à surveiller sous Windows 7 et plus tôt sont couverts au chapitre 9 et dans la FAQ.
Mettre un processus dans un Job n’en fait pas un conteneur ni un bac à sable. L’accès réseau ne peut pas être restreint, et le jeton d’accès (les privilèges) est un mécanisme séparé. Les restrictions d’interface seules ne peuvent pas non plus créer une frontière de sécurité. Le rôle dans cet article est strictement de faire de la durée de vie et des ressources d’un arbre de processus une unité.
4. La bonne façon d’entrer — La course entre création et association
Si vous faites Assign après le lancement, des petits-enfants naissent entre-temps
Un processus enfant peut engendrer des petits-enfants dans les premières millisecondes après qu’il commence à s’exécuter. Un SDK qui lance son aide est le cas typique. Si vous obtenez le PID après Process.Start() puis faites Assign, tout petit-enfant né avant l’Assign se retrouve hors du Job.
flowchart TB
accTitle: Associer après que l'enfant a commencé à s'exécuter manque les petits-enfants
accDescr: L'enfant s'exécute déjà juste après Process.Start, et tout petit-enfant qu'il engendre pendant la fenêtre de course avant l'appel à AssignProcessToJobObject se retrouve hors du Job
s["L'enfant démarre à Process.Start"] --> w["Fenêtre de course jusqu'à Assign"]
w --> g["Petits-enfants nés dans cette fenêtre"]
g --> out["Continuent hors du Job"]
s --> a2["AssignProcessToJobObject"]
a2 --> in2["Seuls les petits-enfants nés après y entrent"]
Figure 3 : la fenêtre de course n’est peut-être que de quelques millisecondes, mais le lancement de l’aide du SDK se produit exactement là.
Procédure A : créer suspendu, associer, puis exécuter
La méthode classique à la plus large compatibilité utilise CREATE_SUSPENDED. Elle ferme la fenêtre dans laquelle l’enfant s’exécute d’abord et engendre des petits-enfants avec l’ordre suivant.67
- Créer le Job avec
CreateJobObject - Définir les limites d’abord avec
SetInformationJobObject - Appeler
CreateProcessavecCREATE_SUSPENDED(le thread initial ne s’exécute pas) - L’y mettre avec
AssignProcessToJobObject - Si cela échoue, ne pas Resume ; appeler
TerminateProcesssur place (ne laisser s’exécuter aucune instruction hors du Job) - L’exécuter avec
ResumeThread
Ce que la procédure A ferme, c’est la fenêtre de course contre la création de petits-enfants. La fenêtre contre un plantage du parent lui-même demeure. Si le parent plante entre les étapes 3 et 4, un enfant suspendu qui n’est pas encore dans le Job reste. Il ne s’exécute pas, mais il ne disparaît pas tout seul non plus.
Si vous voulez fermer cette fenêtre y compris la résilience à un plantage du parent, utilisez la procédure B ci-dessous.
flowchart TB
accTitle: Procédure pour lancer avec SUSPENDED puis mettre l'enfant dans le Job
accDescr: Créer le Job et définir les limites, lancer l'enfant avec CREATE_SUSPENDED, l'associer avec AssignProcessToJobObject, l'arrêter avec TerminateProcess sans reprendre en cas d'échec, et l'exécuter avec ResumeThread en cas de succès
a["Créer le Job avec CreateJobObject"] --> b["Limites avec SetInformationJobObject"]
b --> c["Lancer l'enfant avec CREATE_SUSPENDED"]
c --> d["AssignProcessToJobObject"]
d -->|"succès"| e["Exécuter avec ResumeThread"]
d -->|"échec"| f["Terminer tout de suite, pas de Resume"]
Figure 4 : le squelette de la procédure A. Une implémentation qui omet la branche « s’il échoue, ne pas l’exécuter » ne produit un processus errant que lorsque les choses vont mal.
// C++ : le noyau minimal de la procédure A (la gestion d'erreur n'est qu'un squelette)
HANDLE job = CreateJobObjectW(nullptr, nullptr); // Sans nom convient. Ne pas le rendre héritable
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(); // Le sauver avant que CloseHandle ne l'écrase
CloseHandle(job); // Ne pas exécuter l'enfant dans un Job sans limites
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); // Ne pas fuir le handle de Job aux relances
return HRESULT_FROM_WIN32(err);
}
if (!AssignProcessToJobObject(job, pi.hProcess)) {
DWORD err = GetLastError(); // Le sauver avant que Terminate ne l'écrase
TerminateProcess(pi.hProcess, 1); // Ne pas le laisser s'exécuter hors du Job
// Fermer les handles et remonter err comme erreur
} else if (ResumeThread(pi.hThread) == (DWORD)-1) {
DWORD err = GetLastError();
TerminateProcess(pi.hProcess, 1); // Ne pas le laisser suspendu
// Fermer les handles et remonter err comme erreur
}
CloseHandle(pi.hThread);
// En cas de succès, la propriété de job et de pi.hProcess passe à l'objet de
// gestion de durée de vie de l'appelant (l'équivalent du wrapper C# du chapitre 4).
// Fermer job déclenche KillOnJobClose, et pi.hProcess sert au chapitre 6
// à trancher « qui est mort »
Procédure B : sous Windows 10 et plus tard, créer le processus déjà dans le Job
Placez le handle de Job sur la liste d’attributs de STARTUPINFOEX avec PROC_THREAD_ATTRIBUTE_JOB_LIST. Passez-la ensuite à CreateProcess avec le drapeau EXTENDED_STARTUPINFO_PRESENT. Sans le drapeau, la structure n’est pas interprétée comme l’étendue, et les attributs sont ignorés.89
Avec cette méthode, le processus appartient au Job avant que son thread initial ne s’exécute. La fenêtre de course « créé, mais pas encore membre » n’existe plus du tout, donc ni SUSPENDED ni la branche qui fait Assign après la création et gère l’échec ne sont nécessaires.
| Méthode | Quand l’appartenance est réglée | Points d’attention restants |
|---|---|---|
| Procédure A : SUSPENDED → Assign → Resume | Après la création de l’enfant, avant que son thread initial ne s’exécute | Si le parent plante avant l’Assign, un enfant arrêté reste |
| Procédure B : créer avec JOB_LIST | À la création du processus | Exige Windows 10 ou plus tard. Spécifier la liste d’attributs et le drapeau de démarrage étendu |
flowchart TB
accTitle: En quoi les fenêtres de course des procédures A et B diffèrent
accDescr: La procédure A crée le processus suspendu puis fait Assign et Resume, donc elle a besoin d'une branche qui termine en cas d'échec, alors que la procédure B crée le processus avec le Job sur la liste d'attributs, donc il est déjà membre dès sa naissance et n'a ni fenêtre de course ni branche d'échec
a1["Procédure A : créer suspendu"] --> a2["Rejoindre avec Assign"]
a2 --> a3["Démarrer avec Resume"]
a2 -.-> a4["Branche d'échec requise"]
b1["Procédure B : créer avec liste d'attributs"] --> b2["Membre dès la naissance"]
b2 -.-> b3["Pas de fenêtre de course, pas de branche d'échec"]
Figure 5 : la procédure A est « le mettre dedans, puis l’exécuter » ; la procédure B est « né déjà dedans ». Si vous pouvez supposer Windows 10 ou plus tard, la raison de la choisir est précisément la présence ou l’absence de la fenêtre de course et de la branche d’échec.
Trois implémentations à éviter dans l’une ou l’autre procédure
- Obtenir le PID après
Process.Start()puis l’y mettre (les petits-enfants sortent d’abord) - Laisser l’enfant hériter du handle de Job (l’enfant continue de détenir le handle même après la mort du parent, donc KillOnJobClose ne se déclenche plus — chapitre 5)
- Avaler un échec d’Assign et continuer l’exploitation (un processus de périphérique hors du Job est ce qui entraîne la défaillance suivante)
En .NET, exprimer le propriétaire du handle de Job dans le code
System.Diagnostics.Process n’a pas de notion de Job, et il n’y a pas de wrapper officiel. Écrivez un wrapper mince avec P/Invoke ou CsWin32.
Le point est d’envelopper le handle de Job dans un SafeHandle et d’en faire un IDisposable. Fermer le dernier handle de Job dans Dispose() déclenche KillOnJobClose. L’intention de conception « la durée de vie du wrapper est la durée de vie de l’arbre enfant » peut s’exprimer comme une propriété.
Ci-dessous un squelette qui montre la propriété du handle. La création se fait du côté P/Invoke des procédures A/B, et un vrai projet a aussi besoin de la configuration CsWin32, des désignations unsafe, etc.
// C# : un wrapper mince responsable seulement de posséder le handle de Job (la création passe par le P/Invoke des procédures A/B)
sealed class ChildProcessJob : IDisposable
{
private readonly SafeFileHandle _job; // Continuer à le tenir dans un champ
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(); // Le sauver avant Dispose
_job.Dispose(); // Ne pas remettre un Job sans KillOnJobClose
throw new Win32Exception(err);
}
}
public void Dispose() => _job.Dispose(); // L'arbre en dessous se termine ici
}
À l’inverse, si vous fermez le handle sans avoir l’intention de le fermer, vous terminez l’arbre enfant. Conservez une référence au wrapper pendant toute la durée de vie du parent. Si vous ne le faites pas, l’arbre enfant est anéanti sans raison dès que le GC collecte le SafeHandle.
5. KillOnJobClose — « garder » et « mourir ensemble »
Le déclencheur est « la fermeture du dernier handle », pas « la mort du parent »
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE est un drapeau de limite qui termine chaque processus sous le Job lorsque le dernier handle de Job est fermé.3
Que le parent se termine sur une exception, soit tué dans le Gestionnaire des tâches, ou soit arrêté de force en tant que service, le noyau ferme tous les handles de ce processus. Si c’était le dernier handle de Job, les enfants et petits-enfants se terminent. La force de ce mécanisme est qu’il ne suppose pas que le traitement de sortie du parent s’exécute.10
Cependant, si vous laissez l’enfant hériter du handle de Job, le handle demeure après la sortie du parent. Dans ce cas ce n’est pas « le dernier handle », et l’arbre enfant ne se termine pas.
flowchart TB
accTitle: Chronologie de KillOnJobClose
accDescr: Que le parent se termine normalement, plante ou soit terminé de force, le noyau ferme tous les handles du parent, et si c'était le dernier handle de Job le Job se ferme et l'arbre de processus en dessous est terminé d'un coup
die["Le parent disparaît (y compris un plantage)"] --> close["Le noyau ferme tous les handles"]
close --> last{"Dernier handle de Job ?"}
last -->|"Oui"| killall["Terminer tout l'arbre en dessous"]
last -->|"Non (hérité)"| stay["Les enfants continuent de vivre"]
Figure 6 : le déclencheur est « le dernier handle s’est fermé », pas « le parent est mort ». C’est pourquoi le handle ne doit pas être hérité par l’enfant.
Choisir entre récupérer immédiatement et préserver le matériau de diagnostic
Ce que vous perdez avec une terminaison forcée n’est pas seulement la détention du périphérique. Vous perdez aussi la chance de prendre un vidage sur incident des enfants et petits-enfants terminés avec le parent, la dernière image valide, et la chance de vider un fichier de mesure à moitié écrit.
Le vidage du parent lui-même est une autre affaire. WER traite l’exception non gérée pendant que le parent est encore vivant, donc le vidage du parent peut être écrit avant la fermeture des handles. Ce dont il s’agit ici, c’est le matériau post-mortem des enfants et petits-enfants du côté qui est terminé de force.
| Politique | Convient à | Ce que vous perdez |
|---|---|---|
| Avec KillOnJobClose | Les sites où une double ouverture du périphérique est le pire résultat | Matériau post-mortem, les derniers échantillons |
| Sans (surveillance seulement) | Les sites où les vidages et journaux sont des actifs | Processus orphelins et ports détenus si on néglige |
Sans, plus un processus de surveillance qui appelle TerminateJobObject |
Lorsqu’un service de contrôle séparé existe | L’implémentation est dupliquée |
Comme entrée de la décision, listez d’abord « ce qui ne doit pas rester » et « ce que vous voulez conserver ».
Ce qui ne doit pas rester : une caméra ou un numériseur ouvert, l’usage exclusif d’un port série ou USB, le côté serveur d’un tube nommé, une session de dongle de licence, la mémoire partagée et les fichiers de verrou.
Ce que vous voulez conserver : les vidages sur incident, la dernière image ou les derniers compteurs valides, la chance d’envoyer une commande qui ramène le périphérique dans un état sûr (si possible, l’envoyer avant de tuer).
flowchart TB
accTitle: Tuer, ou seulement surveiller ?
accDescr: Si une double ouverture du périphérique est le pire résultat, définir KillOnJobClose ; si les vidages et la dernière image sont des actifs, ne pas le définir et surveiller à la place ; si un service de contrôle séparé existe, appeler TerminateJobObject depuis là
q{"Que protégez-vous au moment où ça meurt ?"} -->|"Libérer le périphérique passe d'abord"| k["Avec KillOnJobClose"]
q -->|"Les vidages et l'état final sont des actifs"| m["Surveiller seulement (ne pas tuer)"]
q -->|"Un service de contrôle séparé existe"| t["Le côté surveillance appelle TerminateJobObject"]
Figure 7 : choisissez selon « que protégez-vous au moment où ça meurt », pas selon « tuer ou non ». Si vous voulez les deux, vous aboutissez à l’arrangement de la troisième ligne, dans lequel le côté surveillance prend d’abord le vidage puis démonte l’arbre.
Remettre le handle de Job au côté surveillance pendant que le parent est vivant
La troisième ligne du tableau, l’arrangement dans lequel un processus de surveillance termine l’arbre avec TerminateJobObject, a besoin de préparation. Le côté surveillance doit obtenir le handle de Job avant que le parent ne meure.
Le Job créé par la procédure de cet article est sans nom, donc il n’y a aucun moyen de l’atteindre de l’extérieur une fois le parent parti. Il y a deux façons de le remettre.
| Méthode | Que faire pendant que le parent est vivant |
|---|---|
| Dupliquer le handle du Job sans nom | Passer le handle au processus de surveillance avec DuplicateHandle |
| Utiliser un Job nommé | Le créer nommé dès le départ ; le côté surveillance l’ouvre avec OpenJobObject et le tient |
Parce que les noms peuvent entrer en collision globalement, incluez un GUID unique ou équivalent. Si vous oubliez cette préparation, le côté surveillance n’a aucun moyen de terminer l’arbre même lorsqu’il détecte l’anomalie du parent.
Prendre les vidages et mettre le périphérique en sécurité avant la terminaison forcée
Un enfant terminé par KillOnJobClose n’a aucun avertissement, comme avec TerminateProcess. Aucune exception non gérée ne se produit, donc même si WER (LocalDumps) est configuré sur l’enfant, aucun vidage de cette terminaison forcée ne reste. Ce que WER peut attraper, c’est le cas où l’enfant se termine à cause de son propre plantage.
Si l’exigence est « à la fois récupération automatique et vidages », déplacez la responsabilité de terminer vers le côté surveillance. L’ordre est : prendre le vidage pendant que la cible est vivante, ramener le périphérique dans un état sûr si nécessaire, et enfin terminer avec TerminateJobObject.
flowchart TB
accTitle: Démonter de sorte que la récupération automatique et les vidages coexistent
accDescr: Lorsque le processus de surveillance détecte une anomalie, il prend d'abord un vidage, envoie si nécessaire une commande qui ramène le périphérique dans un état sûr, et enfin démonte l'arbre avec TerminateJobObject, de sorte que la récupération automatique et l'analyse post-mortem coexistent
det["Le côté surveillance détecte une anomalie"] --> dmp["Prendre d'abord le vidage"]
dmp --> safe["Ramener le périphérique dans un état sûr"]
safe --> term["Démonter avec TerminateJobObject"]
Figure 8 : la seule réponse à « à la fois récupération automatique et vidages ». Inversez l’ordre et la cible à vider n’existe plus.
Dans un arrangement où KillOnJobClose termine de force l’enfant au moment où le parent disparaît, l’enfant n’a pas non plus la chance d’envoyer une commande « ramener le périphérique dans un état sûr ». Pour les périphériques qui en ont besoin, choisissez l’arrangement de la troisième ligne, et faites envoyer d’abord la commande de mise en sécurité par le côté surveillance puis appeler TerminateJobObject.11
6. Attendre « vide » avec un port d’achèvement
Attendre uniquement sur le handle de Job ne peut pas confirmer que l’arbre s’est terminé
Le handle de Job ne passe pas à l’état signalé lorsque tous les processus en dessous se sont terminés. Il n’est signalé que lorsque tous les processus ont été terminés parce que la limite de temps du job a été dépassée.12
Pour « passer à la suite une fois l’arbre enfant vide », associez un port d’achèvement d’E/S (IOCP) au Job.1314 Faites l’association pendant que le Job est vide, avant d’y mettre aucun processus. Si vous l’associez en cours de route, vous pouvez manquer les notifications des processus dont l’état a changé pendant l’association.15
Observer la création, la sortie, l’anomalie et le zéro avec quatre messages
| Message | Ce qu’il vous dit |
|---|---|
JOB_OBJECT_MSG_NEW_PROCESS |
Un processus a rejoint le Job. Détecte aussi la création de petits-enfants |
JOB_OBJECT_MSG_EXIT_PROCESS |
Un processus s’est terminé |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS |
Un processus s’est terminé avec un code de sortie anormal tel qu’une violation d’accès. Particulièrement important dans les applications de mesure13 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO |
Le nombre de processus actifs a atteint 0 |
Tout ce qu’un paquet NEW_PROCESS transporte, c’est le nouveau PID. Il ne vous dit pas la relation parent-enfant, c’est-à-dire qui a engendré le processus. Si vous avez besoin de la lignée, ajoutez un autre moyen tel qu’ETW.
sequenceDiagram
accTitle: Flux des notifications du port d'achèvement
accDescr: Le Job dépose les messages de création, de sortie, de sortie anormale et de zéro sur le port d'achèvement, un thread de surveillance dédié les reçoit avec GetQueuedCompletionStatus, et seuls les résultats sont passés au thread d'IU
participant J as Job Object
participant P as Port d'achèvement
participant W as Thread de surveillance
participant U as Thread d'IU
J->>P: NEW / EXIT_PROCESS
J->>P: ABNORMAL_EXIT / ZERO
W->>P: Attendre le paquet d'achèvement
P-->>W: Message et PID
W-->>U: Ne passer que la notification de résultat
Figure 9 : exécutez GetQueuedCompletionStatus sur un thread dédié. Attendez sur le thread d’IU, et l’IU passe à « Ne répond pas » chaque fois qu’un enfant se comporte mal.
Attendre sur un thread dédié et un port dédié, et interroger la comptabilité au délai d’expiration
Exécutez cette boucle de surveillance sur un port d’achèvement créé uniquement pour les notifications du Job. Si vous vous greffez sur le même port que des E/S existantes, le paquet a déjà été retiré au moment où GetQueuedCompletionStatus revient. Si vous le jetez avec continue parce que la clé diffère, le propriétaire de cette E/S attend sa completion pour toujours. Il en va de même pour la branche des paquets en échec.
Si vous partagez, vous avez besoin d’un mécanisme séparé qui livre à chaque propriétaire par clé. Cet article sépare les ports et ne passe que des résultats au thread d’IU.
Il suppose aussi qu’un Job est une génération de lancement, recréé à chaque lancement. Si vous le réutilisez d’une tentative à l’autre, TotalProcesses inclut la génération précédente, et la garde qui distingue un Job vide avant le lancement cesse de fonctionner.
// C++ : squelette du thread de surveillance (délai d'expiration + comptabilité comme assurance contre les notifications manquées)
DWORD msg; ULONG_PTR key; LPOVERLAPPED info;
bool treeEmpty = false;
while (!treeEmpty) {
if (!GetQueuedCompletionStatus(iocp, &msg, &key, &info, 5000)) {
if (info != nullptr) continue; // Paquet d'achèvement d'une E/S en échec. Continuer à surveiller
if (GetLastError() != WAIT_TIMEOUT) break; // Port détruit et assimilés : arrêter
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct = {};
if (QueryInformationJobObject(job, JobObjectBasicAccountingInformation,
&acct, sizeof(acct), nullptr))
treeEmpty = (acct.TotalProcesses > 0 && // Ne pas prendre un Job vide avant lancement pour une completion
acct.ActiveProcesses == 0); // Assurance pour une notification ZERO perdue
// Hypothèse : le Job est recréé à chaque lancement (1 Job = 1 génération de lancement).
// Si le Job est réutilisé d'une tentative à l'autre, TotalProcesses compte encore
// la génération précédente, et cette garde ne peut pas séparer les générations
continue; // En cas d'échec de l'interrogation, ne pas conclure qu'il est vide
}
if ((HANDLE)key != job) continue; // Apparier avec le CompletionKey utilisé à l'association.
// Ce port est supposé créé uniquement pour la surveillance du Job
// (voir le texte ci-dessous. Jeter ainsi sur un port partagé
// fait attendre pour toujours les propriétaires des autres E/S)
DWORD pid = (DWORD)(UINT_PTR)info; // Certains messages portent un PID
switch (msg) {
case JOB_OBJECT_MSG_NEW_PROCESS: /* journaliser la création de petit-enfant */ break;
case JOB_OBJECT_MSG_EXIT_PROCESS: /* journaliser la sortie */ break;
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:/* sortie anormale : aller vérifier un vidage */ break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: treeEmpty = true; break;
} // Le break du switch seul ne termine pas l'attente
}
Les notifications, la comptabilité et les handles tranchent chacun des informations différentes
Les notifications n’ont par principe aucune garantie de livraison. Les seules garanties sont les notifications des limites définies avec JobObjectNotificationLimitInformation. Vous ne pouvez pas conclure « pas de notification = cela n’est pas arrivé ».15
L’état agrégé tel que « est-il devenu vide » se confirme aussi en interrogeant les informations de comptabilité. Mais la comptabilité est un ensemble de compteurs agrégés, donc elle ne peut pas reconstruire le PID, le code de sortie, ni si la sortie était anormale pour un EXIT/ABNORMAL_EXIT perdu. C’est le domaine de la conservation des handles de processus et d’ETW.
ACTIVE_PROCESS_ZERO n’est pas non plus une preuve d’une sortie normale. Le compte peut être arrivé à zéro par terminaison forcée, et le message lui-même ne distingue pas. Jugez la qualité de la sortie par EXIT/ABNORMAL_EXIT et le code de sortie.
Ne décidez pas que c’est le même processus par le seul PID
Le PID d’un paquet d’achèvement est réutilisé. À moins de détenir un handle de processus, il n’y a aucune garantie que le PID désigne encore le même processus.13
Ce que le pi.hProcess tenu au chapitre 4 peut figer, c’est seulement le PID de l’enfant que vous avez lancé directement. Pour les petits-enfants engendrés par le SDK, appelez OpenProcess au moment où NEW_PROCESS arrive pour obtenir un handle, et à partir de là appariez contre ce handle.
Même ainsi, une courte fenêtre demeure entre la notification et l’Open. Si le PID est réutilisé dans cet intervalle, vous saisissez un autre processus. Lorsque l’identification stricte du processus individuel est requise, recoupez avec une télémétrie qui porte une heure de création, telle que les événements de démarrage de processus ETW.
flowchart TB
accTitle: Une surveillance qui ne s'appuie pas sur les seules notifications
accDescr: Parce que les messages du port d'achèvement servent à notifier et n'ont pas de garantie de livraison, combinez-les avec l'interrogation des informations de comptabilité et la conservation des handles de processus pour vous préparer aux manques et à la réutilisation de PID
n["Notifications du port d'achèvement"] --> miss["Pas de garantie de livraison (but de notification)"]
miss --> poll["Interroger aussi les informations de comptabilité"]
n --> pid["Les PID sont réutilisés"]
pid --> hold["Conserver les handles de processus"]
Figure 10 : les notifications sont le chemin principal ; la comptabilité et les handles sont l’assurance. Ce n’est qu’avec les deux que vous pouvez dire que vous « observez ».
Soit dit en passant, des implémentations qui « tuent avec TerminateThread le thread qui attend que le Job se vide » ont circulé, mais avec un port d’achèvement il n’est pas besoin de terminer de force le thread d’attente. Raymond Chen a aussi publié un article qui réécrit ce vieux schéma vers l’approche du port d’achèvement.16
7. Défaillances qui se produisent vraiment en mesure et intégration de périphériques
Ici les mécanismes couverts jusqu’ici sont appliqués à six défaillances qui se produisent dans les environnements de périphériques. Pour chaque cas, outre la cause et le remède, nous confirmons « ce qui est resté ».
Défaillance 1 : seul le parent meurt, et la caméra reste ouverte
Dans une configuration où le SDK du fournisseur a un processus d’aide pour le transfert de trames, tuer l’IU parent dans le Gestionnaire des tâches ne laisse que l’aide. Le parent relancé obtient device busy pendant l’initialisation du SDK. Sur certains sites, rien ne récupère tant que le périphérique n’est pas mis hors tension.
Ce qui est resté : le processus d’aide et l’ouverture exclusive de la caméra.
flowchart TB
accTitle: Seul le parent meurt et la caméra reste ouverte
accDescr: Terminer de force l'IU retire le parent, mais le processus d'aide du SDK reste en détenant le handle de caméra, donc le parent relancé échoue à rouvrir avec device busy
kill9["Terminer l'IU dans le Gestionnaire des tâches"] --> dead["Le parent disparaît"]
dead --> helper["L'aide du SDK reste"]
helper --> busy["Détient encore la caméra"]
busy --> fail["device busy après le relancement"]
Figure 11 : le vrai visage de « le processus est parti, et pourtant le périphérique ne peut pas être ouvert ». Le coupable n’est souvent pas le processus dont le nom était affiché dans le Gestionnaire des tâches.
Défaillance 2 : un petit-enfant se retrouve hors du Job
Il y a deux chemins, et les remèdes doivent être séparés.
(a) Le SDK utilise son propre Job. C’est le cas où la contrepartie est déjà dans un Job différent. Sous Windows 7 c’est un job par processus, donc votre Assign échoue. Sous Windows 8 et plus tard un Nested Job peut le ramasser, mais si votre Job a des restrictions d’interface, le Nested Job lui-même est impossible (chapitre 9).
(b) Le SDK engendre le petit-enfant avec CREATE_BREAKAWAY_FROM_JOB. Cela ne fonctionne que lorsque votre Job autorise BREAKAWAY_OK, et le petit-enfant naît hors de l’arbre dès le départ. Un Nested Job ne peut pas le ramasser non plus. Si vous ne l’autorisez pas, la création du SDK échoue, donc si la surveillance est la priorité, l’approche de base est de ne pas l’autoriser et de le détecter comme un échec.
Ce qui est resté : un petit-enfant qui s’exécute hors de la surveillance, et un Assign dont l’échec a été avalé.
flowchart TB
accTitle: Deux chemins par lesquels un petit-enfant se retrouve hors du Job
accDescr: Sur le chemin où le SDK utilise son propre Job, un Nested Job peut le ramasser sous Windows 8 et plus tard mais échoue si des restrictions d'interface sont définies ; sur le chemin où le SDK engendre le petit-enfant avec breakaway, cela ne fonctionne que si vous l'avez autorisé et le petit-enfant est hors de l'arbre dès le départ
g["Le petit-enfant se retrouve hors du Job"] --> ja["(a) Le SDK utilise son propre Job"]
g --> jb["(b) Créé avec breakaway"]
ja --> nest["Win8+ le ramasse par Nested Job"]
nest -.-> ui["Échoue avec des restrictions d'interface"]
jb --> allow["Ne fonctionne que si autorisé"]
allow -.-> out["Petit-enfant hors de l'arbre dès le départ"]
Figure 12 : le même « sortir », mais (a) laisse de la place pour le ramasser par Nested Job, tandis que (b) est réglé comme irrécupérable dès que vous l’autorisez. Le remède commence par identifier le chemin.
Défaillance 3 : un processus de périphérique lancé depuis un service
Lorsqu’un service s’arrête, le SCM n’attend que le parent ; les enfants et petits-enfants engendrés en session 0 sont hors du champ du traitement d’arrêt. Job + KillOnJobClose couvre même cette zone hors champ. La conception d’un arrangement qui enjambe un service et la session interactive est laissée à l’article sur la frontière utilisateur.
Ce qui est resté : des processus restants en session 0, et un faux positif dans le contrôle d’instance dupliquée au lancement suivant.
Défaillance 4 : un enfant qui meurt un mois plus tard
Si vous n’enregistrez pas périodiquement la comptabilité du Job (PeakJobMemoryUsed, compteurs d’E/S, nombre total de processus), vous ne pouvez pas retracer ensuite « quelle génération de l’aide a commencé à gonfler, et quand ».17 La structure de mourir un mois plus tard d’une fuite de handles est telle que disséquée dans l’article sur la défaillance de longue durée d’une caméra industrielle, et la comptabilité du Job est le point d’entrée de cette investigation.
Ce qui est resté : des journaux insuffisants pour trancher la cause.
Défaillance 5 : le traitement de sortie du parent attend que l’enfant se termine
Si vous appelez WaitForExit ou attendez que l’arbre se termine sur le thread d’IU, le parent passe à « Ne répond pas » le jour où l’enfant se fige. Laissez l’attente de sortie au thread IOCP, et ne mettez sur l’IU que la progression et un bouton d’abandon. Le mécanisme est tel que décrit dans l’article « Ne répond pas ».
Ce qui est resté : un parent bloqué avec l’enfant.
flowchart TB
accTitle: Ne pas attendre la sortie sur le thread d'IU du parent
accDescr: Attendre la sortie de l'enfant sur le thread d'IU propage un blocage de l'enfant dans le Ne répond pas du parent, donc laissez l'attente de sortie au thread de surveillance IOCP et ne mettez sur le thread d'IU qu'un affichage de progression et un bouton d'abandon
w2["Attendre la sortie de l'enfant sur le thread d'IU"] --> h2["Le jour où l'enfant se bloque"]
h2 --> f2["Le parent aussi Ne répond pas (entraîné)"]
ok2["Attendre sur le thread IOCP"] --> u2["L'IU n'a que la progression et l'abandon"]
Figure 13 : le côté qui observe l’anomalie de l’enfant ne doit pas se figer à cause de l’anomalie de l’enfant. Séparer seulement où vous attendez retire le blocage collatéral.
Défaillance 6 : n’échoue que sous un débogueur
Les outils de développement et les lanceurs peuvent déjà avoir mis votre processus parent dans un Job. Sous Windows 8 et plus tard un Nested Job vous sauve généralement, mais sur les PC d’équipement sous 7 ou plus tôt l’Assign devient ERROR_ACCESS_DENIED, produisant des différences telles que « ça ne marche que sur la machine de développement » ou « seulement en production ». Le premier pas standard est de vérifier votre propre appartenance avec IsProcessInJob.18
Ce qui est resté : du temps de vérification passé sans pouvoir figer la cause de la différence d’environnement.
8. Quelles limites attacher
Les limites ont un but et des effets secondaires
En restreignant la liste aux limites qui comptent dans une application de mesure, voici les raisons d’utiliser chacune et les effets secondaires.319
| Limite | Pourquoi vous l’utilisez | Si vous en abusez |
|---|---|---|
| KILL_ON_JOB_CLOSE | Libérer le périphérique quand le parent disparaît | Le matériau post-mortem disparaît |
| ACTIVE_PROCESS | Arrêter une multiplication incontrôlée des enfants du SDK | Même les aides légitimes se voient refuser la création |
| JOB_MEMORY / PROCESS_MEMORY | Un plafond sur les fuites en longue durée | L’allocation d’énormes tampons d’image commence à échouer |
| DIE_ON_UNHANDLED_EXCEPTION | Pas de boîtes de dialogue d’erreur sur les machines sans surveillance | Le débogage interactif devient pénible |
| Contrôle du taux CPU | Empêcher un enfant de traitement d’image d’affamer l’IU | Les échéances de trame sont manquées |
| BREAKAWAY_OK | Laisser une voie de sortie à un SDK qui a besoin de son propre job | Les processus disparaissent de la surveillance |
| Restrictions d’interface | Serrage de type bac à sable | Les Nested Jobs cassent (chapitre 9) |
Les limites de notification servent à observer ; les limites imposées servent à refuser et terminer
Séparez « des limites souples pour la notification » de « des limites qui arrêtent les choses lorsqu’elles sont dépassées ». JobObjectNotificationLimitInformation ne fait que vous notifier l’excès ; le processus continue de s’exécuter.15
Les limites Extended Limit sont imposées, mais la forme de l’imposition diffère par limite.3
| Limite | Ce qui se passe lorsqu’elle est dépassée |
|---|---|
| Limite de mémoire | L’opération de commit qui la dépasserait échoue. Le processus lui-même reste vivant |
| ACTIVE_PROCESS | La création ou l’association qui la dépasserait échoue. Un processus qui l’a dépassée par association est terminé |
| Temps de processus (PROCESS_TIME) | Seul le processus qui l’a dépassée est terminé |
| Temps de job (JOB_TIME) | Une limite sur la valeur agrégée ; par défaut tous les processus sous le Job sont terminés |
Définissez cela sans connaître la différence et vous diagnostiquerez à tort « une aide qui disparaît juste après la création » comme une autre panne. Pour un fonctionnement de longue durée, l’ordre sûr est d’observer d’abord avec des limites de notification, et de décider les limites imposées une fois la tendance connue.
flowchart TB
accTitle: Limites pour la notification et limites pour l'imposition
accDescr: Une limite JobObjectNotificationLimitInformation ne notifie que l'excès et le processus continue, alors que les limites Extended Limit sont imposées : une limite mémoire fait échouer l'opération, ACTIVE_PROCESS fait échouer création et association, et les limites de temps terminent le processus
lim2{"But de la limite ?"} -->|"Observer"| ntf["Limite de notification : continue si dépassée"]
lim2 -->|"Arrêter"| enf["Limite imposée : refuser ou terminer"]
ntf --> log2["Identifier la génération d'après les journaux de comptabilité"]
enf --> die2["Échec de commit, création refusée, terminaison"]
Figure 14 : la même « limite », mais notification et imposition sont des choses différentes, et l’imposition fonctionne différemment par limite. Posez une limite imposée sans observer d’abord, et elle se déclenche à tort sur un pic de fonctionnement normal.
La relation entre le contrôle du taux CPU et le traitement périodique est laissée à l’article sur le temps réel souple ; ici nous n’allons pas plus loin qu’une contre-mesure pour « le processus de périphérique mange l’IU ».
9. Nested Jobs, breakaway, et une contrepartie déjà dans un Job
Pensez les Nested Jobs comme « confinement d’ensembles de processus »
Les règles des Nested Jobs sous Windows 8 et plus tard s’organisent en quatre.5
- Le job parent est l’ensemble plus large, et le job enfant en est un sous-ensemble (associer dans un ordre qui casse ce confinement échoue)
- Pour les principales limites de ressources, la plus restrictive le long de la chaîne prend effet
- Un job avec des restrictions d’interface ne peut pas être un Nested Job
- Les notifications sont aussi livrées aux ports d’achèvement de chaque job parent le long de la chaîne (le job enfant n’a pas besoin d’un port)
flowchart TB
accTitle: Hiérarchie des Nested Jobs et limites effectives
accDescr: Le job parent est l'ensemble plus large et le job enfant en est un sous-ensemble, et pour les principales limites de ressources la valeur la plus restrictive le long de la chaîne prend effet. Un job avec des restrictions d'interface ne peut pas être un Nested Job
pj["Job parent (ensemble plus large)"] --> cj["Job enfant (sous-ensemble)"]
cj --> pr["Processus membres"]
pj -.-> eff["Limite effective = valeur la plus restrictive"]
cj -.-> eff
ui["Job avec restrictions d'interface"] -.-> no["Ne peut pas être un Nested Job"]
Figure 15 : pensez les Nested Jobs comme « confinement d’ensembles ». Les restrictions d’interface cassent les Nested Jobs, donc il est plus sûr de ne pas les attacher à un Job utilisé pour la gestion de durée de vie.
Le breakaway est le chemin pour créer un processus hors du Job dès le départ
Le breakaway est le chemin légitime par lequel les descendants créés avec CreateProcess quittent l’arbre.3
| Réglage côté Job | Condition pour que l’enfant naisse hors du Job |
|---|---|
JOB_OBJECT_LIMIT_BREAKAWAY_OK |
Le créer avec CREATE_BREAKAWAY_FROM_JOB spécifié |
SILENT_BREAKAWAY_OK |
Aucun drapeau nécessaire. Chaque enfant naît dehors |
Parfois c’est nécessaire parce que le SDK utilise un Job à lui. Cependant, un processus né par ce chemin est exclu à la fois de la terminaison groupée et de la surveillance. Si vous l’autorisez, décidez aussi qui gère ce qui s’est échappé.
flowchart TB
accTitle: Le chemin hors de l'arbre par breakaway
accDescr: Lorsque BREAKAWAY_OK est défini sur le Job, un petit-enfant créé avec CREATE_BREAKAWAY_FROM_JOB naît hors du Job et disparaît du champ de la terminaison groupée et de la surveillance
j2["Job (avec BREAKAWAY_OK)"] --> c2["Processus enfant"]
c2 -->|"Création normale"| in3["Petit-enfant aussi dans le Job"]
c2 -->|"Création avec BREAKAWAY"| out3["Le petit-enfant sort du Job"]
out3 --> lost["Hors surveillance et terminaison groupée"]
Figure 16 : le breakaway a deux visages : « une voie de sortie pour un SDK qui en a besoin » et « un trou dans la surveillance ». Si vous l’attachez, décidez qui s’occupe de ce qui s’est échappé.
Un lancement par procuration via WMI ne peut pas être empêché même avec le breakaway interdit
Les chemins sur lesquels un processus tiers lance en votre nom, tels que Win32_Process.Create de WMI, sont une autre affaire. Le parent réel est le fournisseur WMI, donc le processus né est hors du Job dès le départ. Interdire le breakaway ne ferme pas ce trou.1
Si le SDK utilise ce chemin se vérifie non pas d’après le journal NEW_PROCESS mais d’après les relations parent-enfant dans Process Explorer.
Vérifier d’abord l’appartenance à un Job existant et les contraintes de Windows 7 et plus tôt
Lorsque la contrepartie est déjà dans un Job (défaillance 6), la procédure est : vérifier avec IsProcessInJob → si un Nested Job peut être mis en place, Assign tel quel → s’il ne peut pas (Windows 7, ou restrictions d’interface), changer la conception.18
Sous Windows 7 et plus tôt, vous ne pouvez pas Assign une seconde fois une contrepartie qui appartient déjà à un autre Job. BREAKAWAY_OK n’est pas un drapeau qui retire ensuite un processus déjà associé. Cette voie de sortie ne fonctionne que lorsque le côté SDK demande le breakaway quand il crée l’enfant. Si cela ne peut pas être attendu, changez la conception avant le lancement en partant de « un processus, un job ».12
Décider en dernier si le parent lui-même entre dans le Job
Enfin, un mot sur la conception « mettre votre propre processus dans votre propre Job ». Si le parent lui-même est aussi placé en dessous, alors lorsque le parent plante il est lui aussi inclus dans les cibles de KillOnJobClose, et la durée de vie de tout l’arbre coïncide complètement. Mais c’est une épée à double tranchant qui se transforme en une terminaison de masse involontaire si vous vous trompez dans la gestion du handle de Job, donc il est plus sûr de commencer par « parent dehors, seulement l’arbre enfant dedans ».
10. Comment investiguer
Investiguez dans l’ordre appartenance → comptabilité et journaux de notification → occupation du périphérique. C’est la procédure de confirmation pour ne pas conclure « rien n’est resté » simplement en regardant les noms de processus.
- Process Explorer : les propriétés du processus ont un onglet Job, qui montre le Job auquel le processus appartient et ses limites. C’est le moyen le plus rapide de confirmer « dans quel Job est cette aide »
IsProcessInJob: le point d’entrée pour vérifier votre propre appartenance ou celle de la contrepartie depuis le code18QueryInformationJobObject: enregistrer périodiquement Basic Accounting (nombre total de processus, temps CPU) et Extended Limit (PeakJobMemoryUsedet ainsi de suite)417- Conserver le journal du port d’achèvement dans un fichier : la chronologie NEW_PROCESS / EXIT / ABNORMAL_EXIT devient la seule preuve dans une investigation un mois plus tard
- Ne pas vérifier les restes par PID : ce qu’il faut regarder, ce sont les handles de périphérique, les noms de tubes et les fichiers de verrou. « Aucun processus visible dans le Gestionnaire des tâches » ne signifie pas « le périphérique a été libéré »
flowchart TB
accTitle: Procédure pour investiguer les restes
accDescr: D'abord confirmer l'appartenance avec IsProcessInJob et l'onglet Job de Process Explorer, lire la comptabilité avec QueryInformationJobObject, et enfin juger les restes d'après les handles de périphérique, les noms de tubes et les fichiers de verrou plutôt que d'après l'existence d'un processus
s1["Confirmer l'appartenance avec IsProcessInJob"] --> s2["Onglet Job de Process Explorer"]
s2 --> s3["Comptabilité avec QueryInformationJobObject"]
s3 --> s4["Juger les restes d'après les handles de périphérique et les noms de tubes"]
Figure 17 : investiguez dans l’ordre « appartenance → comptabilité → occupation ». Ne concluez pas « rien n’est resté » simplement en regardant les noms de processus.
11. Un guide approximatif pour choisir (tableau de décision)
Ici les choix jusqu’ici sont résumés par situation. Revenez au chapitre 5 pour la politique de terminaison, au chapitre 6 pour les hypothèses de surveillance, et au chapitre 9 pour la relation avec les Jobs existants.
| Situation | Recommandation |
|---|---|
| Le corps d’IU et le SDK de périphérique ont été séparés en processus distincts | Les mettre dans un Job, et surveiller avec un port d’achèvement |
| Le pire cas est que le périphérique soit détenu après la disparition du parent | Définir KillOnJobClose |
| La trame ou le vidage au moment du plantage est un actif | Ne pas définir KillOnJobClose ; le côté surveillance met en sécurité puis appelle TerminateJobObject |
| Le SDK du fournisseur engendre des aides | Créer avec SUSPENDED ou JOB_LIST, et journaliser NEW_PROCESS |
Assign renvoie ERROR_ACCESS_DENIED |
Vérifier d’abord le job existant et si un Nested Job est possible. Soupçonner les restrictions d’interface |
| Engendrer des enfants dans la session interactive depuis un service | Ne pas attacher de restrictions d’interface. Revenir à la conception de l’article sur la frontière utilisateur |
| Attendre que les petits-enfants se terminent sur le thread d’IU | Arrêter. Le déplacer vers le thread IOCP |
12. Résumé
La durée de vie d’un enfant n’est pas la durée de vie de l’objet Process du parent. La sortie du parent n’est pas non plus transmise automatiquement à l’enfant. Un Job Object est le mécanisme qui fait de cet arbre de processus une unité et gère les limites, les notifications et la terminaison groupée.
Pour gérer jusqu’aux petits-enfants, réglez l’appartenance avant que l’enfant ne s’exécute. La procédure A est SUSPENDED + Assign ; la procédure B, sous Windows 10 et plus tard, est JOB_LIST. KillOnJobClose fonctionne par la fermeture du dernier handle de Job quelle que soit la raison de la sortie du parent, mais il perd aussi le matériau post-mortem des descendants terminés avec lui.
C’est précisément pourquoi vous devez lister d’abord « ce qui ne doit pas rester après le plantage » et « ce que vous voulez conserver », et choisir entre récupération immédiate et terminaison après que le côté surveillance a collecté et mis en sécurité. C’est la procédure de conception que cet article veut transmettre.
La prochaine chose à écrire serait le détail du lancement de processus à travers la session 0 et la session interactive, ou l’attente sur des périphériques avec des E/S superposées. Une fois que vous tenez la « durée de vie extérieure » d’un processus, la durée de vie des E/S attend ensuite.
Articles connexes
- Checklist pour gérer les processus enfants en toute sécurité dans une application Windows — bonnes pratiques pour Job Object, propagation de la sortie, E/S standard et watchdogs
- Ce qu’est vraiment « Ne répond pas » — comment Windows décide qu’une application est bloquée, et comment concevoir des applications qui ne le sont pas
- Tubes nommés en pratique — l’IPC standard de Windows, de la conception à la sécurité
- Enquête sur les plantages après un fonctionnement de longue durée d’une caméra industrielle - La fuite de handles (partie 1)
- Guide pratique pour se rapprocher autant que possible du temps réel souple sur un Windows ordinaire
- « Le même PC » n’est pas le même environnement d’exécution — La frontière utilisateur qui sépare AppData, HKCU, DPAPI et les informations d’identification
Domaines de conseil associés
KomuraSoft LLC prend en charge la conception d’isolation de processus des applications Windows qui travaillent avec des caméras, des instruments de mesure et des périphériques série/USB, l’investigation des troubles d’occupation de périphérique tels que les aides SDK restantes et device busy, et la construction de mécanismes de surveillance et de récupération automatique pour les applications de longue durée. N’hésitez pas à nous consulter même à partir d’un seul cas de « relancer le parent et le périphérique ne peut pas être ouvert ».
- Développement d’applications Windows
- Conseil technique et revue de conception
- Investigation de bugs et analyse des causes
- Nous contacter
Références
-
Microsoft Learn, Job Objects. Sur le fait qu’un Job Object est un objet noyau qui gère un groupe de processus comme une unité, que les processus enfants créés par un processus membre sont associés au même Job par défaut (sauf via Win32_Process.Create), les deux drapeaux de limite pour le breakaway, la terminaison groupée avec TerminateJobObject, et comment gérer un arbre de processus dans les environnements où les Nested Jobs ne sont pas disponibles. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). Sur le fait que l’association entre un processus et un Job est irréversible, un job par processus sous Windows 7 et plus tôt avec appartenance multiple (Nested Jobs) possible à partir de Windows 8, et les limites effectives et la propagation du breakaway sous Nested Jobs. ↩ ↩2
-
Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h). Sur les drapeaux de limite JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (terminer tous les processus lorsque le dernier handle de Job se ferme), ACTIVE_PROCESS (un plafond de processus actifs simultanément), JOB_MEMORY (un plafond de commit pour tout le job), DIE_ON_UNHANDLED_EXCEPTION, et BREAKAWAY_OK / SILENT_BREAKAWAY_OK. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). Sur le fait que le Job conserve des informations de comptabilité telles que le nombre total de processus, le temps CPU et le nombre de défauts de page, y compris les totaux accumulés par les processus terminés, et qu’on les obtient avec QueryInformationJobObject. ↩ ↩2
-
Microsoft Learn, Nested Jobs. Sur le fait que les Nested Jobs forment une hiérarchie parent-enfant (le job enfant est un sous-ensemble des processus du job parent), qu’un job avec des restrictions d’interface ne peut pas être un Nested Job, que la limite effective est la valeur la plus restrictive le long de la chaîne, que les notifications sont envoyées à tous les ports d’achèvement de la chaîne de jobs parents, et que la hiérarchie est terminée depuis le niveau le plus bas. ↩ ↩2
-
Microsoft Learn, Process Creation Flags. Sur CREATE_SUSPENDED (créer le thread initial suspendu et ne pas l’exécuter jusqu’à ResumeThread) et CREATE_BREAKAWAY_FROM_JOB (le Job de l’appelant doit avoir 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). Sur la procédure classique de créer avec CREATE_SUSPENDED puis de mettre le processus dans un Job, et comment fermer sa fenêtre de course. ↩
-
Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). Sur le fait que PROC_THREAD_ATTRIBUTE_JOB_LIST associe des handles de Job au processus enfant créé dans l’ordre spécifié, et qu’il est pris en charge sous Windows 10 / Windows Server 2016 et plus tard. ↩
-
Raymond Chen, A more direct and mistake-free way of creating a process in a job object (The Old New Thing). Sur l’utilisation de PROC_THREAD_ATTRIBUTE_JOB_LIST pour faire appartenir un processus à un Job dès le moment de la création. ↩
-
Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). Sur l’arrangement qui termine les descendants ensemble lorsque le parent disparaît en utilisant un Job avec KILL_ON_JOB_CLOSE, et l’importance de ne pas laisser le handle de Job être hérité. ↩
-
Microsoft Learn, TerminateJobObject function (jobapi2.h). Sur le fait de terminer de force tous les processus associés au Job, comme si TerminateProcess avait été appelé sur chacun individuellement. ↩
-
Microsoft Learn, Job Objects - Managing Job Objects. Sur le fait que l’objet Job passe à l’état signalé lorsque tous les processus sont terminés pour dépassement de la limite de temps du job, que le Job est détruit lorsque le dernier handle se ferme, et que la fermeture provoque la terminaison de tous les processus membres lorsque KILL_ON_JOB_CLOSE est spécifié. ↩
-
Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h). Sur la liste des messages envoyés au port d’achèvement tels que JOB_OBJECT_MSG_NEW_PROCESS / EXIT_PROCESS / ABNORMAL_EXIT_PROCESS / ACTIVE_PROCESS_ZERO, les codes de sortie jugés sorties anormales, le fait que la réutilisation de PID est impossible à écarter pour les messages qui renvoient un PID à moins de détenir un handle de processus, et le fait que la livraison des notifications n’est pas garantie. ↩ ↩2 ↩3
-
Raymond Chen, How do I wait until all processes in a job have exited? (The Old New Thing). Sur le fait qu’attendre sur le handle de Job ne peut pas détecter « est devenu vide », et le besoin d’attendre JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO sur le port d’achèvement. ↩
-
Microsoft Learn, Job Objects - Job Limits and Notifications. Sur le fait qu’associer le port d’achèvement pendant que le Job est inactif est préférable (réduire la possibilité de manquer des notifications pour des processus dont l’état change pendant l’association), que la livraison des messages n’est pas garantie sauf pour les limites définies avec JobObjectNotificationLimitInformation, et que les processus continuent de s’exécuter après avoir dépassé une limite de notification. ↩ ↩2 ↩3
-
Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). Sur la réécriture du vieux schéma qui tue le thread d’attente avec TerminateThread en une attente basée sur le port d’achèvement. ↩
-
Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). Sur le réglage des limites de mémoire par processus et par job, et l’obtention de la mémoire de pointe avec PeakProcessMemoryUsed / PeakJobMemoryUsed. ↩ ↩2
-
Microsoft Learn, IsProcessInJob function (jobapi.h). Sur le fait de déterminer si un processus s’exécute dans le Job spécifié (ou dans n’importe quel Job). ↩ ↩2 ↩3
-
Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). Sur le contrôle du taux CPU (part des cycles ou poids) par Job. ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Pourquoi les arguments se cassent — Les règles des arguments de ligne de commande Windows
Windows passe à CreateProcess une seule chaîne que le destinataire découpe. Traite les règles de CommandLineToArgvW, du CRT et de .NET, A...
Tubes nommés en pratique — l'IPC standard de Windows, de la conception à la sécurité
Guide pratique des tubes nommés, mécanisme standard de communication inter-processus sous Windows. Cet article organise, à partir des sou...
API du pool de threads Win32 — de la concurrence sans créer de threads, avec CreateThreadpoolWork
Vous multipliez les CreateThread dans le code natif ? Cet article explique, à partir des sources primaires, l'API du pool de threads Win3...
DllMain et le verrou du chargeur — la vraie raison pour laquelle on vous dit de « ne rien faire dans l'initialisation d'une DLL »
Pourquoi il ne faut pas appeler LoadLibrary ni synchroniser avec d'autres threads depuis DllMain. À partir des sources primaires, cet art...
Réveils parasites — pourquoi une variable de condition se réveille « sans notification » et comment attendre correctement sous Windows
Le wait d'une variable de condition peut revenir sans notification (réveil parasite). L'article explique, d'après l'implémentation Window...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Un Job Object est-il un conteneur ou un bac à sable ?
- Non. Un Job Object est un objet noyau qui attache des limites, des notifications et une terminaison groupée à un groupe de processus. Il peut imposer des plafonds de mémoire, de CPU et de nombre de processus, mais il ne peut pas restreindre l'accès réseau, et le jeton d'accès (les privilèges) ne change pas. Il a des restrictions d'interface, mais celles-ci seules n'en font pas une frontière de sécurité. Si l'isolation ou la sécurité est le but, il faut le combiner avec un autre mécanisme tel qu'AppContainer ou les conteneurs. Cet article couvre l'usage qui consiste à traiter la durée de vie et les ressources d'un arbre de processus comme une unité.
- Process.Kill() ne suffit-il pas ?
- Process.Kill() ne termine que ce seul processus ; il n'atteint pas les processus petits-enfants. Kill(entireProcessTree: true) dans .NET Core 3.0 et plus tard parcourt les descendants et les termine, mais comme il énumère l'arbre de processus à partir des relations parent-enfant de ce moment, il peut manquer des processus nés pendant l'énumération et des processus dont la lignée a été coupée parce que le parent est mort d'abord. De plus, aucune des deux méthodes n'est appelée lorsque le parent lui-même plante. Si vous voulez que les descendants soient récupérés que le parent vive ou meure, un Job Object plus KillOnJobClose, qui confie la durée de vie au noyau, est le choix fiable.
- Si le parent est dans un Job, l'enfant entre-t-il automatiquement dans le Job ?
- Par défaut, oui. Un processus enfant créé avec CreateProcess par un processus qui appartient à un Job appartient automatiquement au même Job. L'exception est le breakaway. Si JOB_OBJECT_LIMIT_BREAKAWAY_OK est défini sur le Job et que l'enfant est créé avec le drapeau CREATE_BREAKAWAY_FROM_JOB, ou si JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK est défini, l'enfant naît hors du Job. Notez aussi que les processus créés via Win32_Process.Create de WMI ne sont pas associés au Job.
- Peut-on retirer un processus d'un Job une fois qu'il y a été mis ?
- Non. L'association faite par AssignProcessToJobObject est irréversible, et l'appartenance continue jusqu'à la sortie du processus. Les options de conception sont donc trois : ne pas l'y mettre, le créer dehors dès le départ avec le breakaway, ou utiliser un Job distinct en Nested Job. Il n'y a pas de "le retirer plus tard". Cette irréversibilité est aussi la raison de préparer le Job avant la création.
- KillOnJobClose fonctionne-t-il même lorsque le parent plante ?
- Oui. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE est un mécanisme qui termine les processus en dessous lorsque le dernier handle de Job est fermé, et que le parent sorte normalement, meure sur une exception non gérée, ou soit tué dans le Gestionnaire des tâches, le noyau ferme les handles dans le cadre du nettoyage du processus, donc cela se déclenche. Cependant, si vous laissez le processus enfant hériter du handle de Job, le handle que l'enfant détient survit à la mort du parent, ce n'est donc pas le dernier handle et cela ne se déclenche pas. Ne laissez pas le handle de Job être hérité.
- Les notifications du port d'achèvement sont-elles toujours livrées ?
- Pas toujours. La documentation officielle indique explicitement que, sauf pour les notifications des limites définies avec JobObjectNotificationLimitInformation, la livraison des messages au port d'achèvement n'est pas garantie. Une notification qui n'est pas arrivée ne signifie pas que l'événement n'a pas eu lieu. Pour une surveillance qui a besoin de certitude, combinez-la avec l'interrogation des informations de comptabilité via QueryInformationJobObject, et conservez vous-même les handles de processus pour trancher si un processus est vivant ou mort.
- .NET a-t-il une API officielle de Job Object ?
- Non. System.Diagnostics.Process n'a pas de notion de Job, et la BCL n'a pas de wrapper non plus. La réponse pratique est d'appeler CreateJobObject / SetInformationJobObject / AssignProcessToJobObject par P/Invoke, ou de générer les signatures avec CsWin32, le générateur de source de Microsoft, et d'écrire un wrapper mince. Si vous enveloppez le handle de Job dans un SafeHandle et le fermez dans Dispose de IDisposable, le sens de KillOnJobClose, "durée de vie du wrapper = durée de vie de l'arbre de processus enfants", apparaît directement dans le code.
- Comment concevoir pour un PC d'équipement sous Windows 7 ?
- Sous Windows 7 et plus tôt, un processus ne peut appartenir qu'à un seul Job, et les Nested Jobs ne sont pas possibles. Si le SDK de la contrepartie utilise un Job à lui, votre AssignProcessToJobObject échoue. JOB_OBJECT_LIMIT_BREAKAWAY_OK est un drapeau qui permet à un processus déjà dans votre Job de créer un enfant hors du Job avec CREATE_BREAKAWAY_FROM_JOB ; ce n'est pas une magie qui laisse passer un second Assign une fois le processus déjà dedans. Autrement dit, la voie de sortie n'existe que lorsque le côté SDK demande le breakaway à la création. Si cela ne peut pas être attendu, la seule option est de changer la conception avant le lancement en partant du principe qu'il n'y a qu'un Job, le vôtre. La documentation de Microsoft montre aussi comment gérer l'arbre avec les deux drapeaux de limite de breakaway dans les environnements où les Nested Jobs ne sont pas disponibles. Cela dit, c'est un OS hors support, donc si c'est possible, la migration vient d'abord.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.