Ce qui reste après la mort du parent — garder les processus enfants dans un Job Object

· Mis à jour le: · · Windows, Développement Windows, C#, C++, Win32 API, Mesure et contrôle

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_SUSPENDED et JOB_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
L'écart entre la durée de vie du parent et la durée d'occupation du périphériqueLorsque 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 aussiLe processus parent se termineParti : attente du parent, objet ProcessResté : processus enfants et petits-enfantsDétention des handles de périphériqueCôté serveur des tubes nommésFichiers 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.

Structure de base d'un Job ObjectUn 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 processusProcessus parentJob ObjectEnfant (hôte du SDK de périphérique)Petit-enfant (aide du fournisseur)Limites (mémoire, CPU)Notifications (port d'achèvement)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.

Associer après que l'enfant a commencé à s'exécuter manque les petits-enfantsL'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 JobL'enfant démarre à Process.StartFenêtre de course jusqu'à AssignPetits-enfants nés dans cette fenêtreContinuent hors du JobAssignProcessToJobObjectSeuls 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

  1. Créer le Job avec CreateJobObject
  2. Définir les limites d’abord avec SetInformationJobObject
  3. Appeler CreateProcess avec CREATE_SUSPENDED (le thread initial ne s’exécute pas)
  4. L’y mettre avec AssignProcessToJobObject
  5. Si cela échoue, ne pas Resume ; appeler TerminateProcess sur place (ne laisser s’exécuter aucune instruction hors du Job)
  6. 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.

Procédure pour lancer avec SUSPENDED puis mettre l'enfant dans le JobCré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èssuccèséchecCréer le Job avec CreateJobObjectLimites avec SetInformationJobObjectLancer l'enfant avec CREATE_SUSPENDEDAssignProcessToJobObjectExécuter avec ResumeThreadTerminer 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
En quoi les fenêtres de course des procédures A et B diffèrentLa 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'échecProcédure A : créer suspenduRejoindre avec AssignDémarrer avec ResumeBranche d'échec requiseProcédure B : créer avec liste d'attributsMembre dès la naissancePas 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.

Chronologie de KillOnJobCloseQue 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 coupOuiNon (hérité)Le parent disparaît (y compris un plantage)Le noyau ferme tous les handlesDernier handle de Job ?Terminer tout l'arbre en dessousLes 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).

Tuer, ou seulement surveiller ?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àLibérer le périphérique passe d'abordLes vidages et l'état final sont des actifsUn service de contrôle séparé existeQue protégez-vous au moment où ça meurt ?Avec KillOnJobCloseSurveiller seulement (ne pas tuer)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.

Démonter de sorte que la récupération automatique et les vidages coexistentLorsque 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 coexistentLe côté surveillance détecte une anomaliePrendre d'abord le vidageRamener le périphérique dans un état sûrDé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.

Flux des notifications du port d'achèvementLe 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'IUThread d'IUThread de surveillancePort d'achèvementJob ObjectThread d'IUThread de surveillancePort d'achèvementJob ObjectNEW / EXIT_PROCESSABNORMAL_EXIT / ZEROAttendre le paquet d'achèvementMessage et PIDNe 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.

Une surveillance qui ne s'appuie pas sur les seules notificationsParce 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 PIDNotifications du port d'achèvementPas de garantie de livraison (but de notification)Interroger aussi les informations de comptabilitéLes PID sont réutilisésConserver 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.

Seul le parent meurt et la caméra reste ouverteTerminer 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 busyTerminer l'IU dans le Gestionnaire des tâchesLe parent disparaîtL'aide du SDK resteDétient encore la caméradevice 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é.

Deux chemins par lesquels un petit-enfant se retrouve hors du JobSur 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épartLe petit-enfant se retrouve hors du Job(a) Le SDK utilise son propre Job(b) Créé avec breakawayWin8+ le ramasse par Nested JobÉchoue avec des restrictions d'interfaceNe fonctionne que si autorisé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.

Ne pas attendre la sortie sur le thread d'IU du parentAttendre 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'abandonAttendre la sortie de l'enfant sur le thread d'IULe jour où l'enfant se bloqueLe parent aussi Ne répond pas (entraîné)Attendre sur le thread IOCPL'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.

Limites pour la notification et limites pour l'impositionUne 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 processusObserverArrêterBut de la limite ?Limite de notification : continue si dépasséeLimite imposée : refuser ou terminerIdentifier la génération d'après les journaux de comptabilitéÉ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)
Hiérarchie des Nested Jobs et limites effectivesLe 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 JobJob parent (ensemble plus large)Job enfant (sous-ensemble)Processus membresLimite effective = valeur la plus restrictiveJob avec restrictions d'interfaceNe 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é.

Le chemin hors de l'arbre par breakawayLorsque 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 surveillanceCréation normaleCréation avec BREAKAWAYJob (avec BREAKAWAY_OK)Processus enfantPetit-enfant aussi dans le JobLe petit-enfant sort du JobHors 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 code18
  • QueryInformationJobObject : enregistrer périodiquement Basic Accounting (nombre total de processus, temps CPU) et Extended Limit (PeakJobMemoryUsed et 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é »
Procédure pour investiguer les restesD'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 processusConfirmer l'appartenance avec IsProcessInJobOnglet Job de Process ExplorerComptabilité avec QueryInformationJobObjectJuger 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

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 ».

Références

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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

  6. 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). 

  7. 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. 

  8. 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. 

  9. 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. 

  10. 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é. 

  11. 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. 

  12. 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é. 

  13. 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

  14. 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. 

  15. 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

  16. 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. 

  17. 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

  18. 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

  19. 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 récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

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.

Retour au blog