Partie 4 - Processus¶
Note
Séance 5 (cours jusqu’à « Communication inter-processus ») : p4e1 à p4e3, puis le mini-shell p4e8, qui n’utilise que
fork,execetwaitpid.Séance 6 (signaux, pipes, FIFO) : p4e4 à p4e7.
p4e1 - Premier fork¶
Écrivez un programme C qui :
Affiche
"Avant le fork, PID = X"Crée un processus enfant avec
fork()Le parent affiche :
"Je suis le parent, PID = X, mon enfant a le PID = Y"L’enfant affiche :
"Je suis l'enfant, PID = X, mon parent a le PID = Y"puis se termine avec_exit(0)Le parent attend l’enfant avec
waitet récupère le statut de fin (status) de l’enfantLe parent affiche ensuite le code de retour de l’enfant (testez avec l’enfant qui fait un
_exit(0)ou_exit(1))
Pensez à fflush(stdout) avant fork et avant _exit (voir buffers et fork).
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e1.c && ./a.out
Avant le fork, PID = 12345
Je suis le parent, PID = 12345, mon enfant a le PID = 12346
Je suis l'enfant, PID = 12346, mon parent a le PID = 12345
Parent : enfant terminé normalement, code retour = 0
Les lignes 2 et 3 peuvent être inversées : parent et enfant s’exécutent en parallèle.
Avec _exit(1) dans l’enfant, la dernière ligne devient :
Parent : enfant terminé normalement, code retour = 1
Ajoutez enfin printf("Fin, PID = %d\n", getpid()); juste après le if/else : combien de fois cette ligne s’affiche-t-elle, et par quel(s) processus ? Que se passe-t-il si vous retirez le _exit de l’enfant ? (voir Deux exécutions du même code dans le cours)
p4e2 - Copy-On-Write et isolation mémoire¶
Écrivez un programme qui montre l’isolation mémoire entre processus :
Créez un entier
x = 4fork()un processus enfantL’enfant modifie
x = 2, puis affiche tout de suite son PID, son PPID, la valeur et l’adresse dex(avec%p)Le parent dort 1 seconde (
sleep(1)) pour laisser à l’enfant le temps de modifierx, puis affiche son PID, la valeur et l’adresse dexLe parent attend ensuite la fin de l’enfant avec
wait
La fonction sleep endort le processus pendant un nombre entier de secondes (sleep(0.1) est donc converti en sleep(0) et n’attend pas du tout).
Ici, le sleep rend l’ordre des affichages très probable, mais ce n’est pas une synchronisation : seul wait (ou un moyen de communication comme un pipe) garantit un ordre. Il sert seulement à ce que le parent affiche x après la modification faite par l’enfant.
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e2.c && ./a.out
[enfant] PID=12346, PPID=12345 x=2 &x=0x7ffd055fdc60
[parent] PID=12345 x=4 &x=0x7ffd055fdc60
Les adresses sont identiques, et le parent affiche x après la modification faite par l’enfant, mais les valeurs diffèrent. Pourquoi ? (voir la pagination et le Copy-On-Write)
p4e3 - fork + exec simple¶
Écrivez un programme qui :
fork()un enfantL’enfant exécute la commande
cat -n p4/p4e3.c(voir exec, avecexeclppour quecatsoit cherché dans lePATH). Siexecéchoue →perror("exec")puis_exit(1)Le parent attend avec
waitpid()et afficheParent : enfant terminé (OK)si le code retour est 0, sinon le code retour (ou le numéro du signal si l’enfant a été tué par un signal).
Testez ensuite :
avec un nom de fichier inexistant : qui affiche le message d’erreur, votre
perroroucat? Quel code retour obtient le parent ?avec une commande inexistante (par exemple
"catt"à la place de"cat") : qui affiche le message d’erreur cette fois ?
Le chemin p4/p4e3.c suppose que le programme est lancé depuis le dossier qui contient p4/, comme dans la commande ci-dessous.
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e3.c && ./a.out
... contenu de p4e3.c ...
Parent : enfant terminé (OK)
p4e4 - Intercepter SIGTERM et SIGINT¶
Le programme boucle en affichant son PID puis en dormant une seconde (sleep(1)), jusqu’à réception de SIGTERM.
Un handler, installé avec sigaction et SA_RESTART (voir la recette du cours) :
enregistre le numéro du dernier signal reçu dans
volatile sig_atomic_t last_signal;utilise un drapeau
volatile sig_atomic_t stop = 0pour arrêter la boucle.
sigaction et strsignal sont des fonctions POSIX : mettez #define _GNU_SOURCE (ou #define _POSIX_C_SOURCE 200809L) avant les #include, sinon elles ne sont pas déclarées avec -std=c2x (voir la note du cours).
Procédez par étapes :
Installez le handler pour
SIGINTseulement : un Ctrl+C arrête la boucle (comme l’exemple du cours, avecsleep(1)à la place depause()).Installez aussi le handler pour
SIGTERM: c’est maintenantSIGTERMqui arrête la boucle. UnSIGINTappelle toujours le handler, mais ne modifie plusstop: le programme continue.Après la boucle, affichez le numéro du dernier signal reçu et son nom, avec
strsignal(last_signal).
sleep est interrompu par un signal, même avec SA_RESTART : après un Ctrl+C, le PID suivant s’affiche tout de suite.
Pour tester, utilisez deux terminaux :
terminal 1 : lancez
./a.out, qui affiche son PID (les^Cci-dessous représentent un appui sur Ctrl+C) ;terminal 2 : envoyez
SIGTERMaveckill -TERM <PID affiché>(oukill <PID>, qui envoieSIGTERMpar défaut).
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e4.c && ./a.out
PID=420928
PID=420928
^CPID=420928
PID=420928
^CPID=420928
PID=420928
PID=420928
PID=420928
Signal reçu : 15 (Terminated)
p4e5 - Pipe unidirectionnel parent → enfant¶
Écrivez un programme qui transmet ses arguments de ligne de commande à un enfant par un pipe :
Le parent crée un
pipe(fd), puisfork()un enfantChaque processus ferme l’extrémité qu’il n’utilise pas (le parent
fd[0], l’enfantfd[1]) juste après leforkLe parent écrit dans
fd[1]chacun de ses arguments (argv[1]àargv[argc - 1]), un par ligne (suivi d’un\n), puis fermefd[1]L’enfant lit depuis
fd[0]jusqu’à EOF (readrenvoie 0), affiche ce qu’il reçoit et compte le nombre de lignes reçues, puis fermefd[0]et se termine avec_exit(nombre_de_lignes)Le parent attend la fin de l’enfant et affiche le nombre de lignes reçues par l’enfant, en lisant son code retour (
WEXITSTATUS, voir wait)
Un read peut recevoir plusieurs lignes d’un coup, ou une ligne en plusieurs morceaux (voir les pipes) : comptez les \n reçus plutôt que les appels à read.
Un write peut écrire moins d’octets que demandé : vous pouvez réutiliser write_full (voir write_full dans le cours Fichiers).
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e5.c && ./a.out bonjour le monde
bonjour
le
monde
Parent : l'enfant a reçu 3 lignes
Une fois le programme fonctionnel, retirez le close(fd[1]) de l’enfant : que se passe-t-il ? Pourquoi ?
p4e6 - Ping-pong bidirectionnel avec deux pipes¶
Écrivez un programme parent-enfant utilisant deux pipes :
Pipe P1 (parent → enfant)
Pipe P2 (enfant → parent)
Le parent envoie
ping#1, l’enfant lit et répondpong#1Répétition pour
Néchanges (par ex. 5)Affichage de chaque message envoyé/reçu
Après les
Néchanges, le parent ferme P1 : l’enfant, qui lit jusqu’à EOF, se termine, et le parent l’attend
Après le fork, chaque processus ferme les deux extrémités qu’il n’utilise pas :
|
|
|
|
|
|---|---|---|---|---|
parent |
ferme |
écrit |
lit |
ferme |
enfant |
lit |
ferme |
ferme |
écrit |
Procédez par étapes :
Un seul échange : le parent envoie
ping#1avecwrite, l’enfant le lit avecreadet répondpong#1(comme dans p4e5, mais dans les deux sens).Néchanges, en échangeant des lignes avecfdopen,fprintfetfgets(voir simplifier les échanges) :FILE *to_child = fdopen(P1[1], "w"); setvbuf(to_child, NULL, _IOLBF, 0); // buffer vidé à chaque '\n' fprintf(to_child, "ping#%d\n", i);
fdopenétant une fonction POSIX, mettez#define _POSIX_C_SOURCE 200809Lavant les#include, sinon elle n’est pas déclarée avec-std=c2x.Terminaison : après les
Néchanges, le parent ferme P1 ; l’enfant sort de sa boucle de lecture quandfgetsrenvoieNULL(EOF), puis se termine, et le parent l’attend.
Pour obtenir exactement l’ordre ci-dessous, affichez chaque message avant de l’envoyer, puis faites fflush(stdout) : sinon l’autre processus peut afficher Reçu avant votre Envoi.
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e6.c && ./a.out
[Parent] Envoi : ping#1
[Enfant] Reçu : ping#1
[Enfant] Envoi : pong#1
[Parent] Reçu : pong#1
[Parent] Envoi : ping#2
...
Mon programme est figé
Un programme qui utilise des pipes peut se bloquer sans aucun message. Vérifiez dans l’ordre :
le buffer n’est pas vidé : le message est encore dans le buffer du
FILE *de l’écrivain, et le lecteur attend. Utilisezsetvbuf(..., _IOLBF, 0)et terminez chaque message par\n, ou faitesfflushaprès chaquefprintf;une extrémité d’écriture est restée ouverte : le lecteur ne reçoit jamais EOF tant qu’un
P1[1]est ouvert, y compris celui de l’enfant lui-même (voir le tableau ci-dessus) ;les deux processus lisent en même temps : chacun attend un message de l’autre. Vérifiez l’ordre des envois et des lectures.
Pour voir quels descripteurs sont encore ouverts : ls -l /proc/<pid>/fd dans un autre terminal.
p4e7 - FIFO : écrivain et lecteur¶
Commencez par tester une FIFO dans le terminal (voir le cours) : mkfifo canal, puis cat < canal dans un terminal et echo coucou > canal dans un autre.
Observez que la première commande lancée attend la seconde.
Écrivez ensuite deux programmes séparés qui communiquent via une FIFO :
Programme p4e7_writer.c :
Crée la FIFO
canalavecmkfifo("canal", 0666)si besoin (gérerEEXIST)Ouvre
"canal"en écriture (O_WRONLY), bloquant jusqu’à ce qu’un lecteur ouvre la FIFOEnvoie
"Hello FIFO\n"puis fermeSupprime la FIFO avec
unlink("canal")
Programme p4e7_reader.c :
Crée la FIFO
canalavecmkfifo("canal", 0666)si besoin (gérerEEXIST) : le lecteur est lancé en premierOuvre
"canal"en lecture (O_RDONLY), bloquant jusqu’à ce qu’un écrivain ouvre la FIFOLit tout jusqu’à EOF (
readretourne 0 quand l’écrivain a fermé la FIFO) et l’affiche sur la sortie standardFerme le descripteur
Test :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e7_reader.c -o reader
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e7_writer.c -o writer
$ ./reader &
[1] 12345
$ ./writer
Hello FIFO
$
[1]+ Fini ./reader
« Hello FIFO » est affiché par ./reader (en arrière-plan) : l’ordre avec le retour de l’invite peut varier.
Lancez les deux programmes depuis le même dossier : le chemin "canal" est relatif au dossier courant.
Si un fichier ordinaire canal existe déjà (reste d’un essai précédent), mkfifo échoue avec EEXIST mais ce fichier n’est pas une FIFO : supprimez-le.
p4e8 - Mini-shell avec commandes simples¶
Écrivez un mini-shell qui :
Affiche un prompt
mini-shell>(sans\n: pensez àfflush(stdout))Lit une ligne de commande (avec
fgets) et retire le\nfinal (avecstrcspn, comme dans le cours)Ignore les lignes vides
Découpe la ligne en arguments (séparateur : un espace) dans un tableau
char *args[]terminé parNULLLance chaque commande avec
fork+execvp, puis attend l’enfant avecwaitpidCommande interne
exit: afficheAu revoir !et quitte le shellQuitte aussi le shell sur Ctrl+D (fin de l’entrée standard :
fgetsrenvoieNULL)
Procédez par étapes :
La boucle : prompt,
fgets, lignes vides ignorées,exitet Ctrl+D, sans encore lancer de commande.Lancer une commande sans argument (
date,ls) avecfork+execlp(line, line, NULL)+waitpid, comme dans p4e3.Découper la ligne en arguments, puis lancer la commande avec
execvp(args[0], args).
Pour le découpage, utilisez strtok (<string.h>, man 3 strtok) : le premier appel strtok(ligne, " ") retourne le premier mot, puis chaque appel strtok(NULL, " ") retourne le mot suivant, jusqu’à NULL. strtok modifie la chaîne (elle remplace les espaces par des '\0').
Schéma : line et args après le découpage
Les mots ne sont pas copiés : chaque case de args pointe vers le début d’un mot dans line.
args doit se terminer par NULL, comme argv (voir exec).
Vous pouvez vous aider de ce tutoriel : https://brennan.io/2015/01/16/write-a-shell-in-c/
Résultat attendu :
$ gcc -std=c2x -Wall -Wextra -pedantic -g p4/p4e8.c && ./a.out
mini-shell> echo hello
hello
mini-shell> date
mer. 15 oct. 2025 14:30:12 CEST
mini-shell> exit
Au revoir !
Vous pouvez ensuite ajouter petit à petit des éléments à votre shell :
lancer une commande en arrière-plan quand la ligne se termine par
&(séparé de la commande par un espace, ex.sleep 5 &) : le shell n’attend pas l’enfant. Pour éviter les zombies, récupérez régulièrement les enfants terminés avecwaitpid(-1, NULL, WNOHANG)(voir wait) :mini-shell> sleep 5 & [bg] PID=12350 lancé en arrière-plan
des commandes internes comme
cdouhistory(voir la commandehelpdans un terminal bash si vous voulez de l’inspiration).cddoit forcément être interne (utilisezchdir, voirman 2 chdir) : lancée dans un enfant, elle ne changerait que le répertoire courant de l’enfant, pas celui du shell, car chaque processus a son propre répertoire courant.