Gestion des processus
Ce chapitre explore les mécanismes sophistiqués qui permettent au système d’exploitation de gérer efficacement les processus en cours d’exécution, depuis les opérations de commutation de contexte jusqu’à la gestion fine de la mémoire virtuelle. Ces concepts constituent les fondements de la performance système moderne et déterminent largement la réactivité perçue des applications.
Dans le cadre de ce cours, nous nous concentrerons sur l’architecture x86-64 avec des références spécifiques au noyau Linux. Les mécanismes présentés sont universels mais leur implémentation peut varier selon l’architecture processeur et le système d’exploitation.
Context Switch
Le context switch (commutation de contexte) représente l’opération fondamentale qui permet au système d’exploitation de créer l’illusion d’une exécution simultanée de multiples processus sur un processeur unique ou multicore. Cette opération consiste à sauvegarder l’état complet d’un processus en cours d’exécution et à restaurer l’état d’un autre processus pour lui permettre de reprendre son exécution là où elle s’était arrêtée.

La commutation de contexte s’avère indispensable dans plusieurs situations critiques : lorsqu’un processus épuise son quantum de temps alloué par l’ordonnanceur, lorsqu’il effectue une opération bloquante (entrée/sortie, attente de ressource), lorsqu’une interruption matérielle nécessite un traitement prioritaire, ou encore lorsqu’un processus de priorité supérieure devient prêt à s’exécuter.
Mécanisme de commutation
Le processus de commutation implique plusieurs étapes critiques orchestrées conjointement par le matériel et le système d’exploitation. D’abord, le système sauvegarde l’état complet du processus actuel dans son Process Control Block (PCB), incluant tous les registres du processeur, le compteur ordinal, le pointeur de pile, et les informations de gestion mémoire. L’ordonnanceur sélectionne ensuite le prochain processus à exécuter selon sa politique de planification. Enfin, le système restaure l’état du nouveau processus depuis son PCB et transfert le contrôle à ce processus.
Coût des commutations
Les commutations de contexte génèrent deux types de coûts distincts mais interconnectés. Les coûts directs correspondent au temps processeur nécessaire pour effectuer la sauvegarde et la restauration des états, typiquement quelques microsecondes sur les architectures modernes. Les coûts indirects, souvent plus significatifs, résultent de la perte de localité dans les caches processeur, le vidage de la TLB, la perturbation du pipeline d’instructions et la perte d’historique du prédicteur de branchement.
Process Control Block (PCB)
Le Process Control Block constitue la structure de données centrale qui permet au noyau de gérer chaque processus du système. Dans le noyau Linux, cette structure s’appelle task_struct et contient une quantité considérable d’informations nécessaires à la gestion complète du processus.
La structure task_struct regroupe plusieurs catégories d’informations essentielles : l’identification du processus (PID, PPID, utilisateur propriétaire), l’état actuel du processus (en cours, prêt, bloqué, zombie), les informations de planification (priorité, politique d’ordonnancement, temps CPU consommé), les registres sauvegardés du processeur, les descripteurs de mémoire virtuelle, les descripteurs de fichiers ouverts, et les informations de signalisation.
struct task_struct {
volatile long state; // État du processus
void *stack; // Pointeur vers la pile noyau
struct mm_struct *mm; // Espace d'adressage virtuel
struct files_struct *files; // Fichiers ouverts
struct thread_struct thread; // Contexte processeur
pid_t pid; // Identifiant processus
struct task_struct *parent; // Processus parent
struct list_head children; // Liste des processus enfants
u64 utime, stime; // Temps utilisateur/système
struct signal_struct *signal; // Gestionnaire de signaux
// ... centaines d'autres champs
};
La task_struct représente la structure interne du noyau.
La vue /proc/[pid]/ est la projection observable du PCB, accessible à l’espace utilisateur.
PCB du processus PID 1234:
├── PID: 1234
├── Parent PID (PPID): 1
├── État: S (Sleeping)
├── Compteur ordonnancement: vruntime = 201239ns
├── Registres CPU sauvegardés:
│ ├── RIP: 0x00007f1a8048054
│ ├── RSP: 0x00007ffc7a3b1234
│ └── RAX: 0x0000000000000001
├── Mémoire:
│ ├── Code segment: 0x00400000 - 0x00600000
│ ├── Heap (brk): 0x00600000 - 0x00800000
│ ├── Stack: 0x7ffc7a3b0000
│ └── Mappings: [libc.so, ld.so, ...]
├── Fichiers ouverts:
│ ├── fd 0: /dev/pts/0 (stdin)
│ ├── fd 1: /dev/pts/0 (stdout)
│ ├── fd 2: /dev/pts/0 (stderr)
│ └── fd 3: /var/log/syslog
├── Signaux en attente: [SIGINT, SIGTERM]
└── Temps CPU:
├── Utilisateur: 120ms
└── Système: 5ms
Le PCB joue un rôle central lors des commutations de contexte puisqu’il sert de zone de sauvegarde pour tous les éléments d’état du processus. Le noyau maintient une table de processus qui référence l’ensemble des PCB actifs, permettant un accès rapide aux informations de n’importe quel processus.
Compteur ordinal et registres de gestion
Le compteur ordinal (Program Counter ou Instruction Pointer) représente l’un des registres les plus critiques du processeur car il contient l’adresse de la prochaine instruction à exécuter. Sur architecture x86-64, ce registre s’appelle RIP (64-bit Instruction Pointer) et sa sauvegarde/restauration précise constitue un élément essentiel du context switch.
Parallèlement, le pointeur de pile (Stack Pointer) maintient l’adresse du sommet de la pile d’exécution du processus. Le processeur utilise également un pointeur de cadre (Frame Pointer ou EBP/RBP en x86) qui pointe vers la base du cadre de pile de la fonction actuellement exécutée, facilitant l’accès aux variables locales et aux paramètres de fonction.
Call Stack et gestion des appels
La pile d’appels (call stack) constitue une structure de données LIFO (Last In, First Out) qui gère les appels de fonctions et leurs contextes d’exécution. Chaque appel de fonction crée un nouveau cadre de pile (stack frame) contenant les paramètres de la fonction, les variables locales, l’adresse de retour, et le pointeur de cadre précédent.
public static void main(String[] args) {
// Variables locales de main
int x = 10;
int y = 20;
// Appel de funcA (adresse retour stockée dans la pile)
funcA(x, y);
}
static void funcA(int a, int b) {
// Variables locales de funcA
int sum = a + b;
// Appel de funcB (adresse retour stockée dans la pile)
funcB(sum, b);
}
static void funcB(int c, int d) {
// Variables locales de funcB
int product = c * d;
// À la fin, retour vers funcA
System.out.println("Résultat : " + product);
}
L’organisation des cadres de pile suit une convention d’appel standardisée qui définit comment les paramètres sont passés, comment les registres sont sauvegardés, et comment les valeurs de retour sont gérées. Sur x86-64, la convention System V ABI spécifie que les six premiers paramètres entiers sont passés dans les registres RDI, RSI, RDX, RCX, R8, R9, les paramètres supplémentaires étant placés sur la pile.
Pourquoi SP et FP coïncident parfois ? Au moment exact où une nouvelle fonction commence (prologue), on a : push ebp (sauvegarde de l’ancien FP). mov ebp, esp (on fait pointer FP sur l’actuel SP). sub esp, N (on réserve de l’espace pour les variables locales). Pendant un court instant, juste après mov ebp, esp, le SP et le FP pointent bien au même endroit. Mais dès que tu alloues des variables locales (par sub esp, N), le SP descend alors que le FP reste fixe.
Adressage virtuel et physique
L’un des concepts fondamentaux des systèmes d’exploitation modernes réside dans la distinction entre adresses virtuelles et adresses physiques. Les adresses virtuelles constituent un espace d’adressage abstrait que chaque processus perçoit comme sa mémoire dédiée, commençant généralement à l’adresse 0x0000000000000000 et s’étendant sur l’intégralité de l’espace adressable.
Les adresses physiques correspondent aux emplacements réels dans la mémoire vive (RAM) du système. La Memory Management Unit (MMU) du processeur effectue la traduction transparente entre ces deux espaces d’adressage en utilisant des tables de pages qui établissent la correspondance entre pages virtuelles et cadres physiques. Cette abstraction procure plusieurs avantages cruciaux : chaque processus dispose d’un espace mémoire uniforme et prévisible et les processus sont isolés les uns des autres (un processus ne peut accéder à la mémoire d’un autre).
Translation Lookaside Buffer (TLB) et cache d’adresses
La Translation Lookaside Buffer (TLB) représente un cache matériel haute performance qui stocke les traductions d’adresses récemment utilisées pour éviter l’accès coûteux aux tables de pages en mémoire principale. Chaque entrée TLB contient une adresse de page virtuelle associée à son adresse de page physique correspondante.
Lorsque le processeur génère une adresse virtuelle, la MMU consulte d’abord la TLB. En cas de TLB hit, la traduction s’effectue en un cycle d’horloge. En cas de TLB miss, le processeur doit accéder à la table de pages en mémoire, opération qui peut nécessiter plusieurs centaines de cycles, puis met à jour la TLB avec la nouvelle traduction.
La TLB moderne utilise des identificateurs d’espace d’adressage (ASID - Address Space Identifier) qui permettent de conserver simultanément les traductions de plusieurs processus sans nécessiter un vidage complet lors des commutations de contexte. Cette optimisation réduit significativement le coût indirect des context switches.
Page Fault : gestion des défauts de page
Un page fault (défaut de page) survient lorsque le processeur tente d’accéder à une page virtuelle qui n’est pas actuellement présente en mémoire physique. Cette situation déclenche une exception matérielle qui transfère le contrôle au gestionnaire de page fault du noyau.
Le noyau doit alors déterminer si l’accès est légitime en consultant les structures de données de gestion mémoire du processus. Si l’accès est valide, le système charge la page demandée depuis le stockage secondaire (support de stockage non volatil) vers un cadre de page libre en mémoire physique, met à jour la table de pages, et reprend l’exécution du processus. Si l’accès est illégitime (tentative d’accès à une zone non mappée), le noyau génère un signal SIGSEGV (segmentation fault).
Cache Hit et Cache Miss
Les cache hits et cache misses influencent dramatiquement les performances du système car l’écart de latence entre les différents niveaux de mémoire est considérable. Un accès au cache L1 prend typiquement 1-2 cycles, le cache L2 nécessite 10-20 cycles, le cache L3 demande 50-100 cycles, tandis qu’un accès à la RAM principale peut requérir 200-400 cycles.
Les commutations de contexte perturbent la localité des caches car le nouveau processus accède à des données différentes, provoquant l’éviction des données du processus précédent. Cette “pollution” des caches constitue l’un des coûts indirects majeurs du context switching et explique pourquoi une fréquence excessive de commutations dégrade les performances globales du système.
Heap vs Stack : deux modèles d’allocation
L’espace d’adressage virtuel d’un processus se divise en plusieurs régions distinctes, dont deux revêtent une importance particulière : le heap et la stack.
La stack (pile) constitue une zone mémoire gérée automatiquement qui stocke les variables locales, les paramètres de fonctions, les adresses de retour et les cadres de pile. Elle croît vers les adresses décroissantes et sa gestion suit rigoureusement le principe LIFO. L’allocation et la libération y sont extrêmement rapides car elles consistent simplement à ajuster le pointeur de pile.
Le heap (tas) représente une zone de mémoire dédiée à l’allocation dynamique où les programmes peuvent réserver et libérer de la mémoire à l’exécution via des fonctions comme malloc() et free(). Il croît vers les adresses croissantes et permet une gestion flexible mais plus complexe de la mémoire.
Les différences entre stack et heap impactent significativement les performances et la sécurité. La stack offre des accès ultra-rapides et une gestion automatique de la mémoire, mais sa taille est limitée et son débordement provoque un crash du processus. Le heap permet une allocation flexible de grandes quantités de mémoire, mais l’allocation/libération est plus lente et sujette aux fuites mémoire en l’absence de ramasse-miettes.
Optimisations et considérations de performance
La compréhension fine de ces mécanismes permet d’optimiser les performances système. L’affinité processeur (CPU affinity) maintient les processus sur le même cœur pour préserver la localité des caches (en tout cas sous Linux). Les techniques de batching regroupent les opérations pour réduire la fréquence des commutations. L’utilisation de threads légers au niveau utilisateur permet d’éviter le coût des context switches noyau et seras l’objet du chapitre suivant.
L’analyse des métriques système comme le nombre de commutations de contexte par seconde (/proc/stat), les statistiques TLB (perf stat), et les page faults (/proc/vmstat) fournit des indicateurs précieux pour identifier les goulots d’étranglement de performance et ajuster la configuration du système.
Ces mécanismes, bien que largement transparents pour les applications, constituent les fondements invisibles qui permettent aux systèmes d’exploitation modernes de gérer efficacement des milliers de processus simultanés tout en maintenant des performances élevées et une isolation robuste entre les applications.