Télécharger la checklist Excel avec feuilles en japonais et en anglais
Outils de conversion, updaters, workers d’analyse, CLI externes, PowerShell, ffmpeg, utilitaires internes. Une application Windows en vient à dépendre de processus enfants bien plus facilement qu’on ne le pense.
Mais les accidents ne portent pas sur « est-ce que le lancement a réussi ».
- Le parent tombe, mais l’enfant reste
- Seul un processus petit-enfant survit
stdout/stderrse bloque etWaitForExitne revient jamais- Le watchdog meurt avec ce qu’il surveille
- Vous pensiez que
Kill(entireProcessTree: true)avait terminé le travail, mais seule l’observation s’est terminée en premier
La clé pour gérer les processus enfants en toute sécurité sous Windows n’est pas de choisir l’API de lancement, mais de décider qui possède l’arbre des processus et de concevoir la procédure de fin ainsi que les E/S.
Dans cet article, nous organisons le Job Object, la propagation de fin, les entrées-sorties standard et le watchdog en une seule conception cohérente.
1. D’abord la conclusion
Voici d’abord, listés directement, les points qui comptent le plus en pratique.
- Si vous voulez lier la durée de vie de l’arbre des processus enfants à celle du parent, le point de référence est le Job Object
- Demander la fin à la console et récupérer l’arbre des processus sont deux choses différentes
- La première relève du process group et de
GenerateConsoleCtrlEvent - La seconde relève du Job Object
- La première relève du process group et de
- Si vous voulez intégrer les processus au Job dès le lancement, la conception naturelle consiste à utiliser
STARTUPINFOEXetPROC_THREAD_ATTRIBUTE_JOB_LIST - La règle de base est de drainer la sortie standard et l’erreur standard en parallèle
- Si vous utilisez
stdin, concevez jusqu’à fermer le flux après l’écriture pour transmettre l’EOF - Il est plus sûr de placer le watchdog en dehors du Job qu’il surveille
- Le
Kill(entireProcessTree: true)de.NETest pratique comme API d’arrêt explicite, mais il ne remplace pas une conception incluant la récupération automatique en cas de crash du parent et un arrêt gracieux (graceful shutdown)
2. Ce qui pose vraiment problème
L’implémentation du lancement d’un processus enfant tient généralement en une dizaine de lignes au départ. Mais les accidents se produisent en dehors de ces dix lignes.
- Après la mort du parent, les enfants et petits-enfants continuent de tourner
- Un helper lance un autre helper, et l’on se contente d’attendre seulement l’enfant direct en pensant que c’est terminé
- Un côté de
stdout/stderrse bloque, et le parent comme l’enfant finissent par s’attendre mutuellement - On attend sur le thread UI, et à la fois l’écran et COM se figent
- Le watchdog partage le même destin que ce qu’il surveille, et tombe avec lui en cas d’anomalie
Ce qu’il faut retenir ici, c’est que « la gestion des processus enfants » n’est pas l’affaire d’une seule API.
Il est plus clair de distinguer au minimum ces quatre aspects.
- Qui possède l’arbre des processus
- Comment demander l’arrêt coopératif
- Comment faire circuler les entrées-sorties standard
- Comment surveiller les fins anormales et les blocages
3. Ne pas confondre le rôle de chaque mécanisme
Le process handle, le process group et le Job Object se ressemblent mais jouent des rôles différents.
| Mécanisme | Rôle principal | Adapté à | Ce qu’il ne couvre pas à lui seul |
|---|---|---|---|
| Process handle | Attendre la fin d’un processus, récupérer l’exit code | Attendre la fin d’un outil ponctuel | La récupération des processus petits-enfants |
| Process group | Propager Ctrl+Break vers une console | L’arrêt coopératif d’un enfant console | Le nettoyage lors d’un crash du parent, les processus enfants GUI |
| Job Object | Regrouper l’arbre de processus, appliquer des limites, tout terminer d’un coup | Les arbres de workers, les updaters, les chaînes de helpers | Le comportement spécifique à l’appli « sauvegarder avant de fermer » |
Un process group est un mécanisme qui décide où envoyer un signal à la console, pas un mécanisme pour nettoyer tout l’arbre quand le parent meurt. Le Job Object, lui, est le mécanisme propre à Windows pour gérer un groupe de processus comme une seule unité.
4. Faire du Job Object le point de référence
Le plus grand atout du Job Object est de pouvoir regrouper le process tree selon « à quel Job il appartient », et non « de qui il est l’enfant ». Les enfants créés via CreateProcess par un processus appartenant à un Job rejoignent ce Job par défaut.
De plus, en ajoutant JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, tous les processus associés au Job se terminent lorsque le dernier job handle est fermé.
4.1 Les quatre points à retenir en premier
1. Si vous voulez nettoyer tout l’arbre à la fin du parent, utilisez KILL_ON_JOB_CLOSE
C’est la base pour gérer des helpers/workers dans une application Windows. Une conception qui appelle explicitement TerminateJobObject fonctionne aussi, mais si vous voulez rattacher le cleanup à la durée de vie du parent, y compris en cas de fin anormale, KILL_ON_JOB_CLOSE est la solution la plus claire.
2. Ne pas ajouter BREAKAWAY à la légère
JOB_OBJECT_LIMIT_BREAKAWAY_OK et JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK semblent pratiques, mais ils peuvent aussi faire qu’une partie de l’arbre que vous pensiez pouvoir nettoyer s’en échappe. Sauf intention délibérée, laisser breakaway désactivé réduit le taux d’incidents.
3. Si vous voulez intégrer le Job dès le lancement, utilisez PROC_THREAD_ATTRIBUTE_JOB_LIST
Vous pouvez aussi rattacher un processus après coup avec AssignProcessToJobObject.
Mais dans les cas où vous voulez supposer l’appartenance au Job dès l’instant du lancement, il est plus judicieux de spécifier le Job à la création via STARTUPINFOEX et PROC_THREAD_ATTRIBUTE_JOB_LIST.
4. Ne pas laisser flou le propriétaire du job handle
KILL_ON_JOB_CLOSE s’active quand le dernier handle est fermé.
Autrement dit, si le job handle est dupliqué vers un autre processus ou hérité sans intention, le cleanup ne se produira pas comme prévu même si le parent meurt. Qui est le propriétaire final du job handle doit être décidé à l’avance.
4.2 Le Job Object sert aussi à l’observabilité, mais les notifications ne sont pas infaillibles
Le Job Object peut être associé à un I/O completion port pour recevoir des notifications. Toutefois, il est plus sûr de ne pas considérer les notifications de completion port comme totalement garanties dans tous les cas.
Les completion ports sont donc pratiques pour :
- la surveillance
- l’agrégation
- la journalisation
- les métriques
mais il vaut mieux ne pas construire la correction (correctness) sur elles seules.
5. Concevoir la propagation de fin comme un protocole assorti d’un timeout
La fin d’un processus enfant n’est pas une affaire réglée par un seul appel à une API de kill. Le schéma le moins sujet aux incidents suit ces trois étapes.
- Demander l’arrêt coopératif
- Attendre avec un court timeout
- Enfin, terminer le Job entier de force
En respectant cet ordre, vous préservez le chemin de fin normal tout en récupérant l’arbre en cas de blocage.
5.1 Enfant GUI
Pour un processus enfant doté d’une GUI, en .NET, CloseMainWindow envoie le message de fermeture.
Mais il s’agit d’une demande de fin, pas d’un arrêt forcé. Le déroulement naturel est donc :
CloseMainWindow- attendre un certain temps
- si cela échoue, tuer tout le Job
5.2 Enfant console
Pour un enfant console, le close message de la GUI n’est pas disponible. Dans ce cas, on utilise le process group et les signaux de console.
Le déroulement consiste à lancer avec CREATE_NEW_PROCESS_GROUP, puis à envoyer CTRL_BREAK_EVENT via GenerateConsoleCtrlEvent.
Les points importants ici sont :
CTRL_C_EVENTne se prête pas bien au ciblage d’un groupe spécifique- seuls les processus qui partagent la console peuvent recevoir le signal
- utiliser
CREATE_NEW_PROCESS_GROUPchange aussi la signification deCTRL+C
5.3 Worker / enfant headless
Les workers et les enfants headless ne sont souvent ni GUI ni console. Dans ce cas, il est plus sûr de disposer d’un protocole de fin dédié au processus enfant.
- envoyer
quitsurstdin - envoyer une commande d’arrêt via un named pipe / socket / RPC
- transmettre la demande d’arrêt via un event object
Côté Windows, le Job Object prend en charge le nettoyage de l’arbre ; côté application, le pipe ou stdin prend en charge l’arrêt gracieux (graceful shutdown). Cette séparation limite les incidents.
6. Ne pas laisser les entrées-sorties standard se bloquer
6.1 Drainer stdout / stderr en parallèle
La première règle de base est celle-ci.
Drainez stdout et stderr en parallèle. Lire entièrement un côté avant l’autre se bloque facilement.
Les pipes Windows n’ont pas de buffer infini. Si l’enfant produit une grosse quantité de sortie sur stderr alors que le parent ne lit que stdout, il est courant que l’enfant se bloque sur l’écriture et que le parent se bloque en attendant la fin du processus.
6.2 Si vous utilisez stdin, concevez jusqu’à l’EOF
Pouvoir écrire dans stdin et le fait que l’enfant puisse se terminer ne sont pas la même chose.
- Vous écrivez l’entrée puis ne fermez pas le flux
- Le parent pense « j’ai déjà tout transmis »
- L’enfant pense « la suite va encore arriver » et continue d’attendre
C’est une situation qui se produit. Si vous utilisez stdin, la conception doit inclure la fermeture du flux après l’écriture, afin que l’EOF soit transmis.
6.3 Toujours fermer les extrémités de pipe inutilisées
Si les extrémités inutilisées, côté parent comme côté enfant, ne sont pas fermées, l’EOF ne se propage pas et les conditions de fin s’effondrent. C’est simple, mais c’est un incident étonnamment fréquent en pratique.
6.4 Ne pas rester flou sur UseShellExecute=false et l’héritage des handles
Si vous utilisez la redirection des entrées-sorties standard, .NET exige UseShellExecute=false.
En Win32 aussi, il est plus sûr de limiter autant que possible ce qui est hérité. Laisser bInheritHandles=TRUE et tout hériter est une source de fuites de handles (handle leak) inattendues.
7. Placer le watchdog « à l’extérieur »
Le point le plus important lorsqu’on ajoute un watchdog est de ne pas le placer dans le même Job que ce qu’il surveille. Si vous voulez redémarrer un worker lorsqu’il tombe, cela n’a aucun sens que l’agent de redémarrage meure lui aussi avec lui.
7.1 Baser la surveillance de fin sur des wait handles
Un processus passe à l’état signalé (signaled) lorsqu’il se termine.
La surveillance de la fin n’a donc en réalité pas besoin d’une boucle de polling vérifiant HasExited toutes les 100 ms.
En Win32, la bonne approche consiste à utiliser :
WaitForSingleObjectWaitForMultipleObjectsRegisterWaitForSingleObjectSetThreadpoolWait
Si vous gérez plusieurs enfants, une surveillance basée sur des wait handles est plus naturelle qu’un polling par timer.
7.2 Ne pas attendre indéfiniment sur le thread UI
WaitForSingleObject(INFINITE) est pratique, mais utilisé sur un thread possédant une fenêtre, il bloque facilement le message pump.
Sur les threads UI, les threads d’appartement COM (COM apartment thread) et les threads dotés d’un message pump, il est plus sûr de réfléchir d’abord à où placer l’attente.
7.3 Un watchdog de blocage a besoin d’un heartbeat
Pour un watchdog de fin de processus (exit watchdog), le process handle suffit. Mais un watchdog de blocage (hang watchdog) est différent.
- Bloqué à 100 % de CPU
- En deadlock
- La event loop est vivante mais ne progresse pas
- Arrêté en attente d’une entrée
Ces états ne peuvent pas être détectés par le seul critère « le processus est-il vivant ». Si vous voulez aussi détecter les blocages, il faut des vérifications de vivacité au niveau applicatif, comme :
- un heartbeat
- une séquence de progression
- un horodatage du dernier travail réussi
- une health probe
7.4 Placer l’agent de redémarrage en dehors de ce qu’il surveille
En pratique, on rencontre couramment ces deux schémas.
- L’application parente lance un helper seulement de manière temporaire
- Le parent possède le Job ; sa fin récupère l’arbre du helper
- Un worker reste résident longtemps, et on veut le redémarrer s’il tombe
- Un processus ou service watchdog externe crée un Job par génération de worker
Dans le second cas, séparer l’arbre du worker de l’autorité de redémarrage rend la conception plus stable.
7.5 Gérer la politique de redémarrage sous forme de budget
Une fois le watchdog en place, le risque suivant est le démarrage d’une crash loop.
- Redémarrage immédiat
- Nouveau crash immédiat
- Seuls les logs s’accumulent en masse
Pour éviter cela, il vaut mieux disposer d’un budget de redémarrage (restart budget) comprenant :
- un backoff
- un plafond du nombre de redémarrages sur une fenêtre de temps donnée
- un arrêt avec notification en cas d’échecs consécutifs
8. Configurations recommandées par scénario type
| Scénario | Configuration recommandée |
|---|---|
| Une application de bureau lance un helper CLI ponctuel | 1 lancement = 1 Job. Ajouter KILL_ON_JOB_CLOSE et drainer stdout / stderr en parallèle. À l’annulation : arrêt coopératif → timeout → Job kill |
| Le helper lance à son tour des processus petits-enfants | Partir du principe du Job Object et ne pas autoriser le breakaway. Pour fixer l’appartenance dès le lancement, utiliser PROC_THREAD_ATTRIBUTE_JOB_LIST |
| Un service / watchdog surveille un arbre de workers de longue durée | Le watchdog est un processus / service externe. Créer un Job par génération de worker et surveiller via exit handle + heartbeat |
| Vous voulez arrêter un outil console proprement | Lancer avec CREATE_NEW_PROCESS_GROUP, arrêt coopératif via CTRL_BREAK_EVENT, puis Job kill après timeout |
| Vous voulez fermer un helper GUI | CloseMainWindow / l’équivalent de WM_CLOSE → timeout → Job kill |
| Vous voulez surveiller de nombreux processus enfants | Plutôt que d’ajouter des blocking threads, utiliser RegisterWaitForSingleObject / SetThreadpoolWait |
Le plus important ici est de séparer le mécanisme d’arrêt gracieux (graceful shutdown) du mécanisme de nettoyage (cleanup).
9. Ce qu’il ne faut pas faire
- Penser que
Kill(entireProcessTree: true)seul résout l’arrêt gracieux et la récupération lors d’un crash du parent - Hériter de tout en laissant
bInheritHandles=TRUE - Lire tout
stdoutavant de lirestderr - Ne pas fermer les extrémités de pipe inutilisées
- Appeler
WaitForSingleObject(INFINITE)sur le thread UI - Placer le watchdog dans le même Job que ce qu’il surveille
- Utiliser 259 comme exit code ordinaire
- Faire des notifications du Job completion port l’unique source de vérité
10. Conclusion
Pour gérer les processus enfants en toute sécurité dans une application Windows, la structuration qui aide le plus est celle-ci :
Qui possède le process tree ? Comment la demande de fin est-elle transmise ? Comment les entrées-sorties standard sont-elles drainées jusqu’au bout ? Où le watchdog se trouve-t-il ?
Décidez d’abord ces quatre points.
Ensuite, pour le dire sans détour :
- Le point de référence du nettoyage de l’arbre est le Job Object
- L’arrêt gracieux se répartit selon GUI / console / worker
- Les E/S standard se conçoivent en incluant le drainage parallèle et l’EOF
- Le watchdog se place en dehors de ce qu’il surveille, et se surveille via des wait handles et des heartbeats plutôt que par polling
CreateProcess et Process.Start eux-mêmes ne sont qu’un point d’entrée.
Ce qui influe réellement sur le taux d’incidents, c’est la localisation de la responsabilité de la fin et le drainage complet des E/S.
11. Références
- Microsoft Learn, Job Objects
- Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION
- Microsoft Learn, UpdateProcThreadAttribute
- Microsoft Learn, InitializeProcThreadAttributeList
- Microsoft Learn, Inheritance (Processes and Threads)
- Microsoft Learn, CreateProcessW
- Microsoft Learn, Creating a Child Process with Redirected Input and Output
- Microsoft Learn, Pipe Handle Inheritance
- Microsoft Learn, Process.Kill
- Microsoft Learn, Process.CloseMainWindow
- Microsoft Learn, GenerateConsoleCtrlEvent
- Microsoft Learn, WaitForSingleObject
- Microsoft Learn, RegisterWaitForSingleObject
- Microsoft Learn, GetExitCodeProcess
- Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Compatibilité descendante des interfaces DLL et COM — Tableau de décision : quels changements cassent les appelants
Quels changements apportés à une DLL ou à un composant COM cassent réellement leurs appelants ? Nous détaillons les trois niveaux de comp...
Comment comprendre l'isolation des sessions Windows — Session 0, RDP et exécution simultanée de plusieurs utilisateurs
Cet article démêle le concept de « session » Windows, un sujet qui déroute régulièrement les développeurs d'applications Windows. Il expl...
Empêcher les lancements multiples d'une application Windows — Mutex nommé et activation de la fenêtre existante lors d'un second lancement
Cet article détaille comment implémenter la prévention des lancements multiples d'une application Windows métier à l'aide d'un Mutex nomm...
Intégrer l'authentification Entra ID dans une application WinForms/WPF — Architecture pratique avec MSAL.NET et le broker WAM
Guide pratique pour intégrer l'authentification Entra ID (ex Azure AD) dans une application de bureau WinForms/WPF : la logique du client...
Date, heure et fuseaux horaires dans les applications métier — des pièges de DateTime au principe de stockage en UTC et à la conception des tests
Un déplacement de serveur fait dériver les horaires de neuf heures : cet article remonte à la source des incidents de date/heure, la prop...
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
Dans les applications Windows qui pilotent des CLI externes, des outils de conversion, des workers et des updaters, la stabilité dépend davantage de la gestion de l'arbre des processus et de la conception de la fin de processus que de la méthode de lancement.
Analyse des bugs et des causes
Les incidents opérationnels difficiles à reproduire — un enfant qui survit après le crash du parent, un stdout qui se bloque, un watchdog qui tombe avec ce qu'il surveille — s'améliorent souvent en revoyant la conception de la gestion des processus.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Pourquoi le processus enfant reste-t-il actif après le crash du processus parent ?
- Parce qu'un process handle ou un process group seul ne dispose d'aucun mécanisme pour récupérer l'arbre de processus lors d'un crash du parent. Si vous voulez lier la durée de vie de l'arbre des processus enfants à celle du parent, le point de référence est le Job Object. En ajoutant JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, tous les processus appartenant au Job se terminent lorsque le dernier job handle est fermé, ce qui permet de rattacher le nettoyage (cleanup) à la durée de vie du parent, y compris en cas de fin anormale de ce dernier.
- Pourquoi WaitForExit ne retourne-t-il jamais ?
- Il est très probable que le pipe de la sortie standard ou de l'erreur standard soit bloqué. Les pipes Windows n'ont pas de buffer infini : si l'enfant écrit massivement sur stderr alors que le parent ne lit que stdout, l'enfant se bloque sur l'écriture et le parent se bloque en attendant la fin du processus. La règle de base est de drainer stdout et stderr en parallèle ; une implémentation qui lit entièrement un côté avant l'autre se bloque facilement. Par ailleurs, si vous ne fermez pas les extrémités inutilisées des pipes, l'EOF ne se propage pas et les conditions de fin de processus s'effondrent.
- Le Kill(entireProcessTree: true) de .NET ne suffit-il pas à lui seul ?
- Non, ce n'est pas suffisant. C'est pratique comme API d'arrêt explicite, mais cela ne remplace pas une conception incluant la récupération automatique en cas de crash du parent et un arrêt gracieux (graceful shutdown). L'approche la moins sujette aux incidents comporte trois étapes : demander un arrêt coopératif, attendre avec un court timeout, puis terminer le Job entier de force en dernier recours. Le moyen de demander l'arrêt coopératif dépend du type d'enfant : CloseMainWindow pour un enfant GUI, CREATE_NEW_PROCESS_GROUP associé à CTRL_BREAK_EVENT pour un enfant console, et un protocole de fin dédié via stdin ou un pipe pour un worker.
- Où faut-il placer le processus watchdog ?
- Le plus important est de ne pas le placer dans le même Job que ce qu'il surveille. Si vous voulez redémarrer un worker lorsqu'il tombe, cela n'a aucun sens que l'agent de redémarrage meure en même temps que lui. Pour un worker qui reste résident longtemps, une configuration stable consiste à faire créer un Job par génération de worker par un processus ou service watchdog externe. La surveillance de la fin de processus doit reposer sur des wait handles plutôt que sur du polling, et si vous avez besoin de détecter les blocages (hang), combinez-la avec une vérification de vivacité au niveau applicatif, comme un heartbeat.
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.
Liens publics