Chapitre 1 · 6 h
Notion de processus
Programme contre processus, états et transitions, bloc de contrôle ; création et terminaison par fork, exec et wait ; première approche des threads.
Sur une machine de bureau qui « ne fait rien », ps aux | wc -l répond typiquement autour de
trois cents. Trois cents processus, pour un utilisateur qui regarde un fond d'écran. Aucun
n'occupe le processeur en permanence : la quasi-totalité attend quelque chose — une frappe, un
paquet réseau, une échéance.
Ce chapitre définit cet objet, le plus important du cours. Le chapitre 1 a dit que le système abstrait le matériel ; le processus est l'abstraction du processeur. Et le chapitre 4 en tirera la question suivante : quand plusieurs processus sont prêts, lequel élire ?
Un programme n'est pas un processus
La distinction paraît scolaire jusqu'à ce qu'on la manque, après quoi rien du chapitre ne tient.
Un programme est un fichier sur le disque : une suite d'octets, passive, qui ne fait rien. Un processus est une exécution de ce programme : un objet vivant, doté d'un état, de mémoire, de fichiers ouverts, d'un utilisateur propriétaire.
La relation n'est pas de un à un. Un même programme lancé trois fois donne trois processus
indépendants, avec chacun ses variables — trois fenêtres d'éditeur qui ne partagent rien.
Inversement, un processus peut au cours de sa vie exécuter successivement plusieurs programmes,
et c'est précisément ce que fait exec.
L'analogie qui fonctionne : la recette et le plat. La recette se recopie et se range dans un tiroir ; le plat est en train de cuire, occupe une casserole, et deux personnes qui suivent la même recette produisent deux plats distincts.
Ce qu'un processus possède
Un processus, c'est d'abord une image mémoire, découpée en quatre régions dont la disposition est presque universelle :
adresses hautes ┌────────────────────┐ │ pile (stack) │ variables locales, paramètres, adresses de retour │ ↓ │ croît vers le bas à chaque appel de fonction │ │ │ ↑ │ │ tas (heap) │ allocation dynamique (malloc), croît vers le haut ├────────────────────┤ │ données │ variables globales et statiques ├────────────────────┤ │ code (text) │ les instructions, en lecture seule └────────────────────┘ adresses bassesLe code est en lecture seule, ce qui permet à trois exécutions du même programme d'en partager une seule copie en mémoire — économie considérable, et l'une des raisons d'être de la pagination du chapitre 6. Pile et tas croissent l'un vers l'autre : c'est ce qui permet d'ajuster l'espace entre les deux sans le fixer à l'avance, et un débordement de pile est la collision de ces deux frontières.
Le système, lui, décrit chaque processus par un bloc de contrôle de processus (BCP), une structure du noyau qui contient tout ce qu'il faut pour le suspendre et le reprendre :
| Contenu du BCP | Pourquoi |
|---|---|
| Identifiant (PID), propriétaire | l'identifier, décider de ses droits |
| État | prêt, élu, bloqué (section suivante) |
| Compteur ordinal et registres | reprendre exactement où il en était |
| Informations mémoire | tables de pages du chapitre 6 |
| Fichiers ouverts | les descripteurs 0, 1, 2 du chapitre 2 |
| Priorité, temps consommé | l'ordonnancement du chapitre 4 |
Le changement de contexte est l'opération qui sauvegarde le BCP du processus sortant et restaure celui de l'entrant. C'est un pur surcoût — aucun travail utile n'est fait pendant ce temps — et il coûte de l'ordre de la microseconde, sans compter l'effet indirect le plus cher : le cache du chapitre 7 d'architecture est rempli des données de l'ancien processus, et le nouveau recommence à froid.
Cinq états, et une transition qui compte
élection ┌──────┐ ──────────► ┌──────┐ │ PRÊT │ │ ÉLU │ ──── exit ───► terminé └──────┘ ◄────────── └──────┘ ▲ préemption │ │ │ appel bloquant │ l'événement │ (lecture disque, attente…) │ attendu survient ▼ │ ┌────────┐ └───────────────────│ BLOQUÉ │ └────────┘Le point à ne jamais confondre : prêt et bloqué ne sont pas la même attente.
Un processus prêt attend seulement le processeur. Il a tout ce qu'il lui faut ; donnez-lui le processeur, il repart immédiatement. C'est parmi ces processus, et seulement parmi eux, que l'ordonnanceur choisira au chapitre 4.
Un processus bloqué attend autre chose : la fin d'une lecture disque, un paquet réseau, une saisie. Lui donner le processeur ne servirait à rien — il ne pourrait rien en faire. Il ne redevient prêt que lorsque l'événement attendu survient, ce qui est signalé par une interruption, exactement au sens du chapitre 8 d'architecture.
Deux transitions distinctes mènent donc hors de l'état élu, et il faut les nommer correctement. L'appel bloquant est volontaire : le processus demande une opération lente et se met de côté de lui-même. La préemption est subie : l'interruption d'horloge rend la main au noyau, qui décide de retirer le processeur à un processus pourtant capable de continuer. Sans préemption, un programme en boucle infinie gèlerait la machine — c'est exactement ce qui se passait sur les systèmes coopératifs des années 1990.
Quiz · 1 question
Un processus effectue une lecture de fichier de 5 ms. Pendant ce temps, l'ordonnanceur doit-il le considérer comme candidat au processeur ?
- Oui : il est prêt à reprendre dès que la lecture se termine, autant lui laisser le processeur — candidat quand même
- Non : il est BLOQUÉ, pas prêt. Lui donner le processeur ne servirait à rien puisqu'il ne peut rien faire tant que la donnée n'est pas arrivée ; il ne redeviendra prêt qu'à l'interruption du contrôleur disque — bloqué, donc hors course
- Oui, mais avec une priorité réduite pendant la durée de la lecture — priorité réduite
Réponse : C'est la distinction centrale du chapitre. PRÊT signifie « il ne me manque que le processeur » ; BLOQUÉ signifie « il me manque autre chose, et le processeur n'y changerait rien ». Un processus qui attend un disque n'a aucune instruction à exécuter avant que la donnée soit là : l'élire ferait perdre un changement de contexte pour rien. L'ordonnanceur ne choisit donc QUE parmi les prêts. Le passage de bloqué à prêt est déclenché par l'interruption que lève le contrôleur en fin de transfert — c'est le mécanisme du chapitre 8 d'architecture, et c'est ce qui rend l'attente d'entrée/sortie « gratuite » pour le reste du système : pendant ces 5 ms, d'autres processus travaillent.
Créer un processus : fork, exec, wait
Unix crée les processus d'une façon qui déroute au premier abord et qui se révèle très économique en concepts.
fork duplique le processus appelant. L'enfant reçoit une copie de l'image mémoire du
parent, de ses variables, de ses descripteurs de fichier ouverts, de son environnement. Les
deux processus reprennent à la même instruction, celle qui suit le fork.
D'où la particularité qui surprend tout le monde : fork rend deux fois, une fois dans
chaque processus, et sa valeur de retour est le seul moyen de savoir où l'on est.
pid = fork();if (pid < 0) // échec : pas de nouveau processus perror("fork");else if (pid == 0) // ici, JE SUIS L'ENFANT ...else // ici, JE SUIS LE PARENT ; pid est le PID de l'enfant ...Le choix est délibéré : l'enfant n'a pas besoin qu'on lui dise son PID, il peut le demander par
getpid. C'est le parent qui a besoin de connaître celui de l'enfant, pour l'attendre ou le
tuer plus tard.
exec remplace l'image mémoire du processus courant par celle d'un autre programme. Le PID
ne change pas, les descripteurs ouverts restent ouverts — mais le code, les données et la pile
sont écrasés, et on ne revient jamais d'un exec réussi.
La combinaison des deux donne le schéma que le shell du chapitre 2 exécute à chaque commande :
fork pour se dupliquer, puis exec dans l'enfant pour devenir le programme demandé, pendant
que le parent attend. Séparer les deux opérations peut sembler tortueux ; c'est en réalité ce
qui rend les redirections possibles. Entre le fork et le exec, l'enfant est encore le
shell : il peut refermer son descripteur 1 et le rouvrir sur un fichier, puis lancer le
programme, qui écrira dans le fichier sans jamais savoir qu'il ne s'adresse pas à l'écran.
wait suspend le parent jusqu'à la terminaison d'un enfant et récupère son code de
retour — celui que exit a fourni, et que le shell teste dans un if. Deux situations
anormales portent des noms qu'il faut connaître :
Un processus zombie est terminé mais dont le parent n'a pas encore appelé wait. Son BCP
subsiste, uniquement pour conserver le code de retour. C'est normal et bref ; cela devient une
fuite si le parent n'attend jamais ses enfants.
Un processus orphelin a perdu son parent avant sa propre fin. Il est adopté par le
processus d'initialisation, de PID 1, qui appelle wait en permanence — ce qui garantit qu'un
orphelin ne deviendra pas un zombie éternel. Tous les processus forment ainsi un arbre
enraciné en PID 1.
Les fils d'exécution
Un processus a un seul fil d'exécution : un compteur ordinal, une pile. Un fil (thread) en ajoute d'autres à l'intérieur du même processus.
Ce qui est partagé entre les fils d'un processus : le code, les données globales, le tas, les fichiers ouverts. Ce qui reste propre à chacun : la pile, les registres, le compteur ordinal.
Deux conséquences, exactement opposées. La bonne : la communication est immédiate, puisqu'une
variable globale est visible de tous, et la création d'un fil coûte bien moins qu'un fork,
qui doit dupliquer une image mémoire. La mauvaise : cette même mémoire partagée est le terrain
des conditions de concurrence du chapitre 5. Deux processus séparés ne peuvent pas se
corrompre mutuellement ; deux fils, si, et ils le font.
Il en découle une règle de choix : des tâches indépendantes s'écrivent en processus, des tâches qui doivent échanger beaucoup de données s'écrivent en fils — et acceptent alors la synchronisation qui va avec.
Quiz · 1 question
Pourquoi Unix sépare-t-il fork et exec en deux appels système, plutôt que de fournir une seule primitive « lancer un programme » ?
- Pour des raisons historiques : la fusion des deux serait aujourd'hui préférable — héritage historique
- Parce que l'intervalle entre les deux appartient encore à l'enfant tel qu'il était : c'est là que le shell redirige les descripteurs, change de répertoire ou abaisse ses droits, avant que le nouveau programme prenne la place — l'intervalle est utile
- Parce que fork est réservé aux processus et exec aux fils d'exécution — deux objets différents
Réponse : La séparation est le point de génie du modèle. Après fork, l'enfant est encore une copie du shell : il exécute du code du shell, avec les structures du shell. Il peut donc préparer l'environnement du futur programme — refermer le descripteur 1 et le rouvrir sur un fichier pour une redirection, brancher un tube, changer de répertoire courant, abandonner des privilèges. Puis exec écrase le code sans toucher aux descripteurs, et le programme lancé écrit dans le fichier sans avoir la moindre idée qu'il ne s'adresse pas à l'écran. Une primitive unique « lancer un programme » devrait recevoir en paramètres toutes les personnalisations imaginables ; c'est le choix de Windows avec CreateProcess et sa dizaine d'arguments.
À vous
L'exercice simule fork, exec et wait : chaque processus est un objet avec son PID, son
parent, son état et son code de retour, et l'appel à fork doit produire deux exécutions à
partir d'un seul appel.
Trois choses à obtenir. L'arbre des processus, affiché en indentant les enfants. Le zombie : un
enfant terminé dont le parent n'a pas appelé wait reste dans la table. Et l'adoption : un
enfant dont le parent meurt d'abord doit être rattaché au PID 1.
Exercice de code
Complétez fork, exit et wait : arbre des processus, zombie et adoption par le PID 1.
Point de départ
// Table des processus du noyau. Chaque entrée est un BCP simplifié.
let PROCHAIN_PID = 1;
const TABLE = [];
function creer(parent, nom) {
const p = { pid: PROCHAIN_PID++, parent, nom, etat: "prêt", code: null, attend: false };
TABLE.push(p);
return p;
}
const init = creer(null, "init"); // PID 1, la racine de l'arbre
const trouver = (pid) => TABLE.find((p) => p.pid === pid);
// fork : duplique le processus courant. Rend le PID de l'enfant au parent,
// et 0 à l'enfant — ici on rend les deux, à l'appelant de simuler.
function fork(courant, nomEnfant) {
const enfant = creer(courant.pid, nomEnfant ?? courant.nom);
return { auParent: enfant.pid, aLEnfant: 0, enfant };
}
// exec : remplace le programme. Le PID ne change PAS.
function exec(processus, programme) {
processus.nom = programme;
return processus;
}
// exit : le processus se termine. Son BCP survit tant que le parent n'a pas
// récupéré son code de retour.
function sortir(processus, code) {
processus.etat = "zombie";
processus.code = code;
// ← à écrire : les enfants de ce processus deviennent orphelins
}
// wait : le parent récupère un enfant zombie et libère son BCP.
function attendre(parent) {
const zombie = TABLE.find((p) => p.parent === parent.pid && p.etat === "zombie");
if (!zombie) return null;
TABLE.splice(TABLE.indexOf(zombie), 1); // le BCP disparaît enfin
return { pid: zombie.pid, code: zombie.code };
}
function arbre(pid = 1, profondeur = 0) {
const p = trouver(pid);
if (!p) return;
console.log(" ".repeat(profondeur) + "PID " + p.pid + " " + p.nom +
" [" + p.etat + (p.code !== null ? ", code " + p.code : "") + "]");
for (const e of TABLE.filter((x) => x.parent === pid)) arbre(e.pid, profondeur + 1);
}
// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Dans sortir(), rattachez les enfants du mourant au PID 1 (adoption).
// 2. Écrivez le scénario du shell : fork, exec dans l'enfant, wait dans le
// parent — et vérifiez qu'aucun zombie ne subsiste à la fin.
const shell = creer(1, "bash");
const f = fork(shell, "bash");
exec(f.enfant, "grep");
sortir(f.enfant, 0);
console.log("— avant wait —"); arbre();
console.log("wait rend :", attendre(shell));
console.log("— après wait —"); arbre();
Solution
let PROCHAIN_PID = 1;
const TABLE = [];
function creer(parent, nom) {
const p = { pid: PROCHAIN_PID++, parent, nom, etat: "prêt", code: null, attend: false };
TABLE.push(p);
return p;
}
const init = creer(null, "init");
const trouver = (pid) => TABLE.find((p) => p.pid === pid);
function fork(courant, nomEnfant) {
const enfant = creer(courant.pid, nomEnfant ?? courant.nom);
return { auParent: enfant.pid, aLEnfant: 0, enfant };
}
function exec(processus, programme) {
processus.nom = programme;
return processus;
}
function sortir(processus, code) {
processus.etat = "zombie";
processus.code = code;
// Adoption : un enfant ne doit jamais rester sans parent, sinon personne
// n'appellerait jamais wait pour lui et son BCP fuirait définitivement.
// Le processus 1 attend en permanence, c'est son unique travail.
for (const enfant of TABLE.filter((p) => p.parent === processus.pid)) {
enfant.parent = 1;
}
}
function attendre(parent) {
const zombie = TABLE.find((p) => p.parent === parent.pid && p.etat === "zombie");
if (!zombie) return null;
TABLE.splice(TABLE.indexOf(zombie), 1);
return { pid: zombie.pid, code: zombie.code };
}
function arbre(pid = 1, profondeur = 0) {
const p = trouver(pid);
if (!p) return;
console.log(" ".repeat(profondeur) + "PID " + p.pid + " " + p.nom +
" [" + p.etat + (p.code !== null ? ", code " + p.code : "") + "]");
for (const e of TABLE.filter((x) => x.parent === pid)) arbre(e.pid, profondeur + 1);
}
console.log("— 1. le shell lance une commande : fork, exec, wait —");
const shell = creer(1, "bash");
const a = fork(shell, "bash");
exec(a.enfant, "grep");
sortir(a.enfant, 0);
arbre();
console.log("wait rend :", attendre(shell), " (le BCP est libéré)");
console.log("");
console.log("— 2. un parent qui n'attend pas : le zombie reste —");
const b = fork(shell, "ls");
sortir(b.enfant, 2);
arbre();
console.log("");
console.log("— 3. adoption : le parent meurt avant son enfant —");
const c = fork(shell, "bash");
const petitFils = fork(c.enfant, "sleep");
sortir(c.enfant, 0); // le parent intermédiaire disparaît…
arbre(); // …et son enfant est passé sous le PID 1
console.log("le processus " + petitFils.enfant.pid +
" a désormais pour parent le PID " + petitFils.enfant.parent);
En travaux pratiques
Travaux pratiques 3 · 3 h
Premier noyau du mini-shell
Créer des processus, les remplacer, les attendre — et écrire les cinquante lignes qui font l'essentiel d'un interpréteur de commandes.
Avant de commencer
- Les TP 1 et 2
- gcc, et la page de manuel de fork au chapitre 2
Énoncé
- Le premier fork — Écrivez un programme qui appelle fork une fois et affiche, dans les deux branches, son identifiant et celui de son père. Exécutez-le dix fois et notez si l'ordre des lignes varie.
- Compter les processus — Écrivez un programme avec trois appels à fork successifs, sans aucun test. Prédisez le nombre de lignes affichées AVANT d'exécuter, puis vérifiez. Indice : Chaque appel double la population.
- Remplacer le programme — Faites exécuter la commande ls par le fils, avec execvp. Vérifiez que la ligne écrite après l'appel n'est jamais atteinte, et expliquez pourquoi.
- Attendre — Faites attendre le père avec wait, récupérez le code de sortie du fils, et affichez-le. Testez avec un fils qui réussit, un qui échoue, un qui est tué par un signal.
- Fabriquer un zombie — Écrivez un programme dont le père n'attend pas et dort trente secondes. Observez le fils dans ps pendant ce temps, et notez son état.
- Fabriquer un orphelin — Inversez : le fils survit au père. Affichez l'identifiant de son père avant et après la mort de celui-ci.
- Le mini-shell — Assemblez : une boucle qui lit une ligne, la découpe en mots, crée un processus, exécute la commande et attend. Faites-le fonctionner avec ls, echo et une commande inexistante.
- L'arrière-plan — Ajoutez la reconnaissance d'une esperluette finale : la commande s'exécute sans que le shell attende. Vérifiez ensuite ce qui arrive à vos processus terminés.
C'est réussi quand
- Trois fork affichent huit lignes, et vous saviez le prédire
- Votre shell exécute ls, signale proprement une commande inconnue, et ne quitte pas
- Vous savez expliquer ce qu'est un zombie sans employer le mot « planté »
Correction
pid_t p = fork();
if (p < 0) perror("fork");
else if (p == 0) printf("fils : pid=%d ppid=%d\n", getpid(), getppid());
else printf("père : pid=%d fils=%d\n", getpid(), p);fork est le seul appel qui revient DEUX fois : une fois dans chaque processus. La valeur de retour est la seule différence entre les deux — 0 chez le fils, l'identifiant du fils chez le père. Tout le reste est identique : mêmes variables, mêmes valeurs, même point d'exécution. Et l'ordre d'affichage varie d'une exécution à l'autre, parce que c'est l'ordonnanceur qui décide.
fork(); fork(); fork();
printf("bonjour\n"); → 8 lignes
1 → 2 → 4 → 8 (chaque appel double)
n appels sans test → 2 puissance n processusL'erreur classique est de compter 3 ou 4 : on oublie que le fils exécute LUI AUSSI les fork suivants. C'est le même raisonnement qu'une bombe à fork, et c'est aussi pourquoi tout système sérieux limite par utilisateur le nombre de processus.
if (fork() == 0) {
execvp("ls", (char*[]){"ls", "-l", NULL});
perror("execvp"); /* atteint SEULEMENT si exec a échoué */
_exit(127);
}exec ne crée pas un processus : il REMPLACE le programme du processus courant, en gardant l'identifiant, les descripteurs de fichiers ouverts et le père. Il n'y a donc pas de retour à attendre — la seule raison d'atteindre la ligne suivante est un échec. C'est la séparation fork/exec, propre à Unix, qui permet au shell de préparer les redirections ENTRE les deux.
int statut;
wait(&statut);
if (WIFEXITED(statut)) printf("code %d\n", WEXITSTATUS(statut));
else if (WIFSIGNALED(statut)) printf("tué par le signal %d\n", WTERMSIG(statut));Le statut n'est pas le code de sortie : c'est un entier codé, dont il faut extraire les champs avec les macros. Un programme tué par un signal n'a AUCUN code de sortie — d'où la nécessité de tester d'abord comment il s'est terminé. C'est ce que le shell affiche dans sa variable de statut, et ce sur quoi repose le set -e du TP 2.
ZOMBIE : le fils est mort, le père n'a pas fait wait ps → « defunct » ou état Z il ne consomme ni mémoire ni processeur, mais garde une ENTRÉE dans la table des processus, ressource finie ORPHELIN : le père est mort avant le fils ppid du fils passe à 1 (ou au subreaper) init fait wait à sa place → jamais de zombie durable
Un zombie n'est pas un processus qui plante : c'est un cadavre que personne n'a réclamé. Le système garde son code de sortie tant que le père ne l'a pas lu. Un serveur qui crée des fils sans jamais faire wait épuise la table des processus en quelques heures — panne réelle et fréquente, dont le symptôme est un fork qui échoue ailleurs sur la machine.
while (1) {
printf("$ "); fflush(stdout);
if (!fgets(ligne, sizeof ligne, stdin)) break; /* Ctrl-D */
int fond = 0;
char *args[64]; int n = 0;
for (char *m = strtok(ligne, " \t\n"); m; m = strtok(NULL, " \t\n"))
args[n++] = m;
args[n] = NULL;
if (n == 0) continue;
if (strcmp(args[n-1], "&") == 0) { fond = 1; args[--n] = NULL; }
pid_t p = fork();
if (p == 0) {
execvp(args[0], args);
fprintf(stderr, "%s : commande introuvable\n", args[0]);
_exit(127);
}
if (!fond) waitpid(p, NULL, 0);
while (waitpid(-1, NULL, WNOHANG) > 0) ; /* récolte les zombies */
}Cinquante lignes, et c'est bien la structure d'un vrai shell : lire, découper, créer, remplacer, attendre. La dernière ligne est celle qu'on oublie — sans elle, chaque commande lancée en arrière-plan laisse un zombie. Le TP 5 ajoutera les tuyaux et les redirections, qui se placent dans le fils entre fork et exec.
Ce que la suite en fait
Le chapitre 4 prend la suite immédiate. Nous avons maintenant des processus, dont certains sont prêts ; il faut choisir. Les états définis ici sont exactement les entrées de l'ordonnanceur, et le changement de contexte est le coût qu'il faut amortir — c'est lui qui interdit un quantum de temps trop court.
Le bloc III part de la remarque sur les fils : dès que deux exécutions partagent de la mémoire, elles peuvent se corrompre. Le chapitre 5 montrera comment, et comment l'empêcher.
À retenir
Flashcards · 5 cartes
- Qu'est-ce qui distingue un programme d'un processus ?
- Le programme est un fichier passif sur le disque ; le processus est une EXÉCUTION de ce programme, un objet vivant avec son état, sa mémoire, ses fichiers ouverts, son propriétaire. La relation n'est pas de un à un : un même programme lancé trois fois donne trois processus indépendants, et un processus peut exécuter successivement plusieurs programmes — c'est ce que fait exec.
- Quelle est la différence entre l'état PRÊT et l'état BLOQUÉ ?
- Un processus PRÊT n'attend que le processeur : donnez-le-lui, il repart. Un processus BLOQUÉ attend autre chose (fin d'une lecture disque, paquet réseau, saisie) et le processeur ne lui servirait à rien. L'ordonnanceur ne choisit QUE parmi les prêts. Le passage de bloqué à prêt est déclenché par l'interruption du périphérique concerné.
- Que fait fork, et pourquoi dit-on qu'il « rend deux fois » ?
- Il duplique le processus appelant : l'enfant reçoit une copie de l'image mémoire, des variables, des descripteurs et de l'environnement, et les deux reprennent à la même instruction. La valeur de retour est le SEUL moyen de savoir où l'on est : 0 dans l'enfant, le PID de l'enfant dans le parent, négatif en cas d'échec. L'enfant n'a pas besoin qu'on lui donne son PID (getpid suffit) ; c'est le parent qui a besoin de celui de l'enfant.
- Pourquoi fork et exec sont-ils deux appels séparés ?
- Parce que l'intervalle entre les deux est utile. Après fork, l'enfant est encore une copie du shell : il peut refermer le descripteur 1 et le rouvrir sur un fichier, brancher un tube, changer de répertoire, abandonner des privilèges. Puis exec écrase le code sans toucher aux descripteurs, et le programme lancé écrit dans le fichier sans le savoir. C'est ce qui rend les redirections du chapitre 2 possibles.
- Qu'est-ce qu'un zombie, qu'est-ce qu'un orphelin ?
- Un ZOMBIE est un processus terminé dont le parent n'a pas encore appelé wait : son bloc de contrôle subsiste uniquement pour conserver le code de retour. Normal et bref ; c'est une fuite si le parent n'attend jamais. Un ORPHELIN a perdu son parent avant sa propre fin : il est adopté par le processus 1, qui appelle wait en permanence, ce qui garantit qu'il ne restera pas zombie.