Partie 4 - Processus

Note

  • Séance 5 (cours jusqu’à « Communication inter-processus ») : p4e1 à p4e3, puis le mini-shell p4e8, qui n’utilise que fork, exec et waitpid.

  • Séance 6 (signaux, pipes, FIFO) : p4e4 à p4e7.

p4e1 - Premier fork

Écrivez un programme C qui :

  1. Affiche "Avant le fork, PID = X"

  2. Crée un processus enfant avec fork()

  3. Le parent affiche : "Je suis le parent, PID = X, mon enfant a le PID = Y"

  4. L’enfant affiche : "Je suis l'enfant, PID = X, mon parent a le PID = Y" puis se termine avec _exit(0)

  5. Le parent attend l’enfant avec wait et récupère le statut de fin (status) de l’enfant

  6. Le 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 = 4

  • fork() un processus enfant

  • L’enfant modifie x = 2, puis affiche tout de suite son PID, son PPID, la valeur et l’adresse de x (avec %p)

  • Le parent dort 1 seconde (sleep(1)) pour laisser à l’enfant le temps de modifier x, puis affiche son PID, la valeur et l’adresse de x

  • Le 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 enfant

  • L’enfant exécute la commande cat -n p4/p4e3.c (voir exec, avec execlp pour que cat soit cherché dans le PATH). Si exec échoue → perror("exec") puis _exit(1)

  • Le parent attend avec waitpid() et affiche Parent : 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 perror ou cat ? 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 = 0 pour 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 :

  1. Installez le handler pour SIGINT seulement : un Ctrl+C arrête la boucle (comme l’exemple du cours, avec sleep(1) à la place de pause()).

  2. Installez aussi le handler pour SIGTERM : c’est maintenant SIGTERM qui arrête la boucle. Un SIGINT appelle toujours le handler, mais ne modifie plus stop : le programme continue.

  3. 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 ^C ci-dessous représentent un appui sur Ctrl+C) ;

  • terminal 2 : envoyez SIGTERM avec kill -TERM <PID affiché> (ou kill <PID>, qui envoie SIGTERM par 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), puis fork() un enfant

  • Chaque processus ferme l’extrémité qu’il n’utilise pas (le parent fd[0], l’enfant fd[1]) juste après le fork

  • Le parent écrit dans fd[1] chacun de ses arguments (argv[1] à argv[argc - 1]), un par ligne (suivi d’un \n), puis ferme fd[1]

  • L’enfant lit depuis fd[0] jusqu’à EOF (read renvoie 0), affiche ce qu’il reçoit et compte le nombre de lignes reçues, puis ferme fd[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épond pong#1

  • Ré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 :

P1[0]

P1[1]

P2[0]

P2[1]

parent

ferme

écrit

lit

ferme

enfant

lit

ferme

ferme

écrit

Procédez par étapes :

  1. Un seul échange : le parent envoie ping#1 avec write, l’enfant le lit avec read et répond pong#1 (comme dans p4e5, mais dans les deux sens).

  2. N échanges, en échangeant des lignes avec fdopen, fprintf et fgets (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 200809L avant les #include, sinon elle n’est pas déclarée avec -std=c2x.

  3. Terminaison : après les N échanges, le parent ferme P1 ; l’enfant sort de sa boucle de lecture quand fgets renvoie NULL (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
...

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 canal avec mkfifo("canal", 0666) si besoin (gérer EEXIST)

  • Ouvre "canal" en écriture (O_WRONLY), bloquant jusqu’à ce qu’un lecteur ouvre la FIFO

  • Envoie "Hello FIFO\n" puis ferme

  • Supprime la FIFO avec unlink("canal")

Programme p4e7_reader.c :

  • Crée la FIFO canal avec mkfifo("canal", 0666) si besoin (gérer EEXIST) : le lecteur est lancé en premier

  • Ouvre "canal" en lecture (O_RDONLY), bloquant jusqu’à ce qu’un écrivain ouvre la FIFO

  • Lit tout jusqu’à EOF (read retourne 0 quand l’écrivain a fermé la FIFO) et l’affiche sur la sortie standard

  • Ferme 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 \n final (avec strcspn, comme dans le cours)

  • Ignore les lignes vides

  • Découpe la ligne en arguments (séparateur : un espace) dans un tableau char *args[] terminé par NULL

  • Lance chaque commande avec fork + execvp, puis attend l’enfant avec waitpid

  • Commande interne exit : affiche Au revoir ! et quitte le shell

  • Quitte aussi le shell sur Ctrl+D (fin de l’entrée standard : fgets renvoie NULL)

Procédez par étapes :

  1. La boucle : prompt, fgets, lignes vides ignorées, exit et Ctrl+D, sans encore lancer de commande.

  2. Lancer une commande sans argument (date, ls) avec fork + execlp(line, line, NULL) + waitpid, comme dans p4e3.

  3. 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').

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 avec waitpid(-1, NULL, WNOHANG) (voir wait) :

    mini-shell> sleep 5 &
    [bg] PID=12350 lancé en arrière-plan
    
  • des commandes internes comme cd ou history (voir la commande help dans un terminal bash si vous voulez de l’inspiration). cd doit forcément être interne (utilisez chdir, voir man 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.