aujourd’hui on va voir un mécanisme avancé mais vraiment très important du développement système Windows : les Job Objects.
Beaucoup de développeurs savent créer un processus avec CreateProcessW, attendre sa fin, récupérer son code de sortie, voire ouvrir un processus existant avec OpenProcess… mais s’arretent la. Le problème, c’est qu’un programme un peu sérieux ne lance pas toujours un seul processus isolé. Il peut lancer plusieurs enfants, sous-processus, outils auxiliaires, workers, navigateurs embarqués, helpers, etc. Et a partir de la, si on ne possède aucun conteneur de supervision, le nettoyage devient vite pénible, le contrôle des ressources devient flou, et le comportement du groupe de processus devient difficile à raisonner.
C’est précisément la qu’interviennent les Job Objects.
Pourquoi les Job Objects sont importants
Comprendre les Job Objects permet de mieux voir :
- comment regrouper plusieurs processus dans un meme conteneur noyau
- comment imposer des limites communes
- comment superviser un groupe de processus au lieu de les gérer un par un
- comment tuer proprement tout un ensemble de processus
- comment mettre en place certaines formes de confinement, de sandboxing léger ou de contrôle d’exécution
- comment recevoir des notifications quand quelque chose se passe dans le job
Ce qu’est réellement un Job Object
Un Job Object est un objet noyau qui sert de conteneur de processus. Il ne représente pas un processus en particulier, mais un groupe de processus sur lequel Windows peut appliquer des règles communes.
Un job peut servir à :
- regrouper plusieurs processus
- imposer des limites de ressources
- surveiller l’activité du groupe
- recevoir certaines notifications système
- contrôler la durée de vie de tout l’ensemble
- terminer tous les processus du groupe en une seule opération
Un Job Object :
- ne contient pas de threads comme entités gérées directement
- n’est pas un parent de processus au sens Unix fort
- n’agit pas sur toute la mémoire du système
- n’affecte que les processus explicitement placés dedans
Le principe général
Le modèle des Job Objects est simple dans l’idée :
- on crée un job vide
- on configure éventuellement ses limites ou politiques
- on assigne un ou plusieurs processus à ce job
- Windows applique alors les règles du job à ces processus
Créer un Job Object
La fonction de base est :
Code: Select all
HANDLE hJob = CreateJobObjectW(NULL, L"MyJob");
if (!hJob)
return 1;
- le nom du job est optionnel
- la fonction retourne un HANDLE, donc un objet noyau à fermer correctement
- au moment de sa création, le job est vide
- si un nom est utilisé, il s’inscrit dans l’espace de noms des objets noyau
Un Job Object vide ne change rien
C’est une chose qui mérite d’etre répétée, parce que beaucoup de débutants créent un job puis pensent que le système “surveille” déjà leurs processus. Ce n’est pas le cas.
Tant qu’aucun processus n’a été assigné :
- aucune limite ne s’applique
- aucune supervision réelle n’existe
- aucune politique n’a d’effet
Assigner un processus à un Job
L’API centrale est :
Code: Select all
if (!AssignProcessToJobObject(hJob, hProcess))
return 1;
- il faut disposer d’un handle valide vers le processus
- le processus doit etre compatible avec l’assignation demandée
- un processus n’est pas libre de “sortir” du job une fois placé dedans par un mécanisme simple user-mode
- les règles du job s’appliquent ensuite à ce processus
Vérifier si un processus est déjà dans un Job
Une API très utile à connaitre est :
- IsProcessInJob
Code: Select all
BOOL inJob = FALSE;
if (!IsProcessInJob(hProcess, NULL, &inJob))
return 1;
Créer le processus suspendu puis l’assigner
Une bonne pratique classique consiste à créer d’abord le processus en mode suspendu, puis à l’assigner au job avant de reprendre son thread principal. Cela évite qu’il commence à s’exécuter librement avant son rattachement.
Exemple :
Code: Select all
STARTUPINFOW si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
if (!CreateProcessW(
L"C:\\Windows\\System32\\notepad.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi))
{
return 1;
}
if (!AssignProcessToJobObject(hJob, pi.hProcess))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(hJob);
return 1;
}
ResumeThread(pi.hThread);
Les structures centrales des Job Objects
Pour travailler sérieusement avec les jobs, il faut connaitre plusieurs structures, pas uniquement une seule.
Les plus importantes à connaitre au début sont :
- JOBOBJECT_BASIC_LIMIT_INFORMATION
- JOBOBJECT_EXTENDED_LIMIT_INFORMATION
- JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
- JOBOBJECT_BASIC_PROCESS_ID_LIST
- JOBOBJECT_ASSOCIATE_COMPLETION_PORT
- JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
- JOBOBJECT_END_OF_JOB_TIME_INFORMATION
- JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION
- JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2
Les classes d’information de SetInformationJobObject et QueryInformationJobObject
Les deux grandes API de configuration et d’interrogation sont :
- SetInformationJobObject
- QueryInformationJobObject
Parmi les classes à connaitre, on retrouve notamment :
- JobObjectBasicLimitInformation
- JobObjectExtendedLimitInformation
- JobObjectBasicAccountingInformation
- JobObjectBasicProcessIdList
- JobObjectAssociateCompletionPortInformation
- JobObjectEndOfJobTimeInformation
- JobObjectCpuRateControlInformation
- JobObjectNotificationLimitInformation
- JobObjectNotificationLimitInformation2
- JobObjectBasicUIRestrictions
- JobObjectSecurityLimitInformation
Les limites de base : JOBOBJECT_BASIC_LIMIT_INFORMATION
La première vraie structure à connaitre est JOBOBJECT_BASIC_LIMIT_INFORMATION.
Exemple :
Code: Select all
JOBOBJECT_BASIC_LIMIT_INFORMATION info = { 0 };
info.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
info.ActiveProcessLimit = 2;
Parmi les flags importants :
- JOB_OBJECT_LIMIT_ACTIVE_PROCESS
- JOB_OBJECT_LIMIT_PROCESS_TIME
- JOB_OBJECT_LIMIT_JOB_TIME
- JOB_OBJECT_LIMIT_WORKINGSET
- JOB_OBJECT_LIMIT_PRIORITY_CLASS
- JOB_OBJECT_LIMIT_AFFINITY
- JOB_OBJECT_LIMIT_SCHEDULING_CLASS
- JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
- JOB_OBJECT_LIMIT_BREAKAWAY_OK
- JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
- JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
Appliquer une structure de limite au Job
Une fois la structure remplie, on l’applique avec SetInformationJobObject :
Code: Select all
if (!SetInformationJobObject(
hJob,
JobObjectBasicLimitInformation,
&info,
sizeof(info)))
{
return 1;
}
- créer le job
- configurer ses limites
- puis seulement assigner les processus
JOBOBJECT_EXTENDED_LIMIT_INFORMATION
Dès que l’on veut aller un peu plus loin, il faut utiliser JOBOBJECT_EXTENDED_LIMIT_INFORMATION. Cette structure étend les limites de base avec des champs supplémentaires, notamment autour de la mémoire.
Exemple :
Code: Select all
JOBOBJECT_EXTENDED_LIMIT_INFORMATION ext = { 0 };
ext.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_PROCESS_MEMORY |
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
ext.ProcessMemoryLimit = 50 * 1024 * 1024;
if (!SetInformationJobObject(
hJob,
JobObjectExtendedLimitInformation,
&ext,
sizeof(ext)))
{
return 1;
}
- BasicLimitInformation
- ProcessMemoryLimit
- JobMemoryLimit
- PeakProcessMemoryUsed
- PeakJobMemoryUsed
- IoInfo
KILL_ON_JOB_CLOSE : le flag à connaitre absolument
S’il y a un seul flag que beaucoup de développeurs devraient retenir immédiatement, c’est :
Code: Select all
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
Cela donne un mécanisme de nettoyage extrêmement pratique :
- pas besoin d’énumérer chaque processus un par un pour les tuer
- pas de fuite de sous-processus oubliés
- fin du superviseur = fin du groupe de processus
Exemple minimal :
Code: Select all
JOBOBJECT_BASIC_LIMIT_INFORMATION info = { 0 };
info.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(
hJob,
JobObjectBasicLimitInformation,
&info,
sizeof(info)))
{
return 1;
}
Avec JOB_OBJECT_LIMIT_ACTIVE_PROCESS, on peut limiter le nombre de processus simultanément actifs dans le job.
Exemple :
Code: Select all
JOBOBJECT_BASIC_LIMIT_INFORMATION info = { 0 };
info.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
info.ActiveProcessLimit = 2;
Limiter la mémoire
Les jobs permettent aussi de contrôler certaines consommations mémoire, notamment via :
- JOB_OBJECT_LIMIT_PROCESS_MEMORY
- JOB_OBJECT_LIMIT_JOB_MEMORY
Exemple conceptuel :
- ProcessMemoryLimit : limite mémoire pour un processus individuel du job
- JobMemoryLimit : limite mémoire globale pour le job entier
Temps CPU et fin du Job
Les jobs savent aussi gérer certaines notions temporelles.
Flags à connaitre :
- JOB_OBJECT_LIMIT_PROCESS_TIME
- JOB_OBJECT_LIMIT_JOB_TIME
- JOBOBJECT_END_OF_JOB_TIME_INFORMATION
Priorité, affinité, scheduling class
Le job peut aussi exprimer certaines politiques d’exécution plus générales via :
- JOB_OBJECT_LIMIT_PRIORITY_CLASS
- JOB_OBJECT_LIMIT_AFFINITY
- JOB_OBJECT_LIMIT_SCHEDULING_CLASS
- de forcer une classe de priorité
- de limiter l’affinité processeur
- de cadrer le comportement d’ordonnancement
Breakaway et processus qui échappent au Job
Deux flags importants à connaitre existent autour du breakaway :
- JOB_OBJECT_LIMIT_BREAKAWAY_OK
- JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
C’est un sujet un peu plus subtil, mais il faut au moins connaitre ces drapeaux, car ils apparaissent vite dans les codes de sandbox, d’encadrement de processus ou de supervision.
Interroger les infos d’un Job : QueryInformationJobObject
Pour lire les informations d’un job, l’API centrale est :
- QueryInformationJobObject
Code: Select all
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acc = { 0 };
if (!QueryInformationJobObject(
hJob,
JobObjectBasicAccountingInformation,
&acc,
sizeof(acc),
NULL))
{
return 1;
}
- TotalUserTime
- TotalKernelTime
- ThisPeriodTotalUserTime
- ThisPeriodTotalKernelTime
- TotalPageFaultCount
- TotalProcesses
- ActiveProcesses
- TotalTerminatedProcesses
Récupérer la liste des PID du Job
Pour connaitre les processus présents dans un job, une structure très utile est :
- JOBOBJECT_BASIC_PROCESS_ID_LIST
- NumberOfAssignedProcesses
- NumberOfProcessIdsInList
- ProcessIdList
Notifications de Job via Completion Port
Un autre aspect très important des Job Objects, souvent ignoré au début, est leur capacité à envoyer des notifications vers un I/O completion port.
La structure de liaison est :
- JOBOBJECT_ASSOCIATE_COMPLETION_PORT
- JobObjectAssociateCompletionPortInformation
- CreateIoCompletionPort
- GetQueuedCompletionStatus
- PostQueuedCompletionStatus
- création ou fin de processus dans le job
- dépassement de certaines limites
- changement d’état significatif du job
Notification limits
Windows propose aussi des limites “notifiantes”, c’est-à-dire des seuils qui ne servent pas forcément à tuer immédiatement un processus, mais à signaler qu’une condition a été atteinte.
Structures importantes :
- JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION
- JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2
Contrôle du CPU d’un Job
Pour des scénarios plus avancés, il faut connaitre :
- JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
- JobObjectCpuRateControlInformation
- JOB_OBJECT_CPU_RATE_CONTROL_ENABLE
- JOB_OBJECT_CPU_RATE_CONTROL_WEIGHT_BASED
- JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP
- JOB_OBJECT_CPU_RATE_CONTROL_MIN_MAX_RATE
Restrictions UI et sécurité
Il existe aussi des classes et structures pour certaines restrictions d’interface ou de sécurité.
Par exemple :
- JobObjectBasicUIRestrictions
- JobObjectSecurityLimitInformation
Terminer explicitement un Job
En plus de KILL_ON_JOB_CLOSE, il existe une API directe pour tuer tous les processus du job :
- TerminateJobObject
Code: Select all
if (!TerminateJobObject(hJob, 0))
return 1;
Exemple complet minimal propre
Voici un exemple minimal dans un style plus propre, avec création suspendue, assignation, reprise et nettoyage :
Code: Select all
STARTUPINFOW si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
HANDLE hJob = CreateJobObjectW(NULL, NULL);
if (!hJob)
return 1;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION ext = { 0 };
ext.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(
hJob,
JobObjectExtendedLimitInformation,
&ext,
sizeof(ext)))
{
CloseHandle(hJob);
return 1;
}
if (!CreateProcessW(
L"C:\\Windows\\System32\\notepad.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi))
{
CloseHandle(hJob);
return 1;
}
if (!AssignProcessToJobObject(hJob, pi.hProcess))
{
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(hJob);
return 1;
}
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
/* A la fin, CloseHandle(hJob) provoquera la fin des processus
du job grâce à JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE */
Il faut résister à une confusion classique : un job n’est pas un “parent” de processus au sens traditionnel. Ce n’est pas une hiérarchie familiale stricte, mais un mécanisme de regroupement et de contrôle.
Autrement dit :
- le job n’est pas une pile de threads
- le job n’est pas l’arbres des parents/enfants
- le job est une relation de contrôle explicite entre le noyau et un groupe de processus
Comme souvent en Win32, les plus grosses erreurs viennent d’une mauvaise compréhension des bases.
Parmi les fautes fréquentes :
- confondre job et processus
- croire qu’un job contient des threads comme objets gérés directement
- oublier de configurer les limites avant l’assignation quand la logique l’exige
- oublier JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE quand on veut un vrai nettoyage automatique
- fermer le handle du job trop tot
- croire qu’un processus peut etre déplacé librement d’un job à un autre
- ne pas vérifier les retours de SetInformationJobObject, AssignProcessToJobObject ou QueryInformationJobObject
- oublier CloseHandle sur le job lui-meme
Le Job Object est un conteneur noyau de processus. Il permet de regrouper plusieurs processus sous une meme politique de contrôle, de supervision ou de limitation. Il n’exécute rien lui-meme, n’est pas une hiérarchie forte, et n’agit que sur les processus qui lui sont explicitement rattachés.
Les idées importantes à garder sont les suivantes :
- un job ne fait rien tant qu’aucun processus n’y est assigné
- AssignProcessToJobObject est l’API centrale de rattachement
- SetInformationJobObject et QueryInformationJobObject sont les deux grandes API de configuration et d’interrogation
- JOBOBJECT_BASIC_LIMIT_INFORMATION et JOBOBJECT_EXTENDED_LIMIT_INFORMATION sont les structures de base à connaitre
- JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE est l’un des flags les plus utiles en pratique
- les notifications via completion port rendent les jobs très puissants pour la supervision
Pour avoir une bonne base, il faut au minimum reconnaitre :
- JOBOBJECT_BASIC_LIMIT_INFORMATION
- JOBOBJECT_EXTENDED_LIMIT_INFORMATION
- JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
- JOBOBJECT_BASIC_PROCESS_ID_LIST
- JOBOBJECT_ASSOCIATE_COMPLETION_PORT
- JOBOBJECT_CPU_RATE_CONTROL_INFORMATION
- JOBOBJECT_END_OF_JOB_TIME_INFORMATION
- JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION
- JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2
- STARTUPINFOW
- PROCESS_INFORMATION
Pour la base des jobs :
- CreateJobObjectW
- OpenJobObjectW
- AssignProcessToJobObject
- IsProcessInJob
- SetInformationJobObject
- QueryInformationJobObject
- TerminateJobObject
- CloseHandle
- CreateProcessW
- ResumeThread
- TerminateProcess
- CreateIoCompletionPort
- GetQueuedCompletionStatus
- PostQueuedCompletionStatus
- InitializeProcThreadAttributeList
- UpdateProcThreadAttribute
- DeleteProcThreadAttributeList
Comprendre les Job Objects, c’est comprendre comment Windows sait encadrer non pas un processus isolé, mais un groupe entier de processus avec des règles communes. C’est une pièce essentielle dès qu’on commence à écrire des superviseurs, des services robustes, des outils de sandbox, des environnements de test, ou tout simplement des programmes qui veulent lancer des enfants sans en perdre le contrôle.
Si tu maitrises déjà correctement :
- CreateJobObjectW
- AssignProcessToJobObject
- SetInformationJobObject
- QueryInformationJobObject
- JOBOBJECT_BASIC_LIMIT_INFORMATION
- JOBOBJECT_EXTENDED_LIMIT_INFORMATION
- JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
- TerminateJobObject
Et surtout, tu commences à voir qu’un Job Object n’est pas juste une curiosité de plus dans Win32, mais un vrai mécanisme de contrôle de groupe, extrêmement utile en développement système.
A bientot pour le prochain cours
