6. Laboratoire du 9 octobre : lire, expérimenter et synchroniser
6.1. Documents, VM et liens symboliques
La séance utilise d’abord la fiche affichée LAB3.5_processus_thread, livrée localement comme LAB2.5_processus_thread.pdf, puis LAB3Enonce, dont le titre interne est Laboratoire#3. Les trois exemples C++ sont dans Laboratoire3.5_code.zip de Labo 03. L’énoncé et cinq fichiers C sont dans Labo 04; les corrigés LAB3Solution et Question6_solution sont publiés dans Labo 03. Ces différences de noms n’autorisent pas à appeler ces documents « module Labo 05 ».
L’ancienne VM sert aux exercices de noyau; les exemples ordinaires de processus et de threads peuvent utiliser la VM habituelle selon l’assistant du labo. Le module propose aussi Lab03GLO2001Xubuntu, 822,96 Mo, référencé comme environnement sans téléchargement ni démarrage ici. Pour les expériences de section critique/Peterson, la consigne est un processeur/un cœur. Cette condition pédagogique n’établit pas la correction générale du code C concurrent sur tout matériel ou avec toute optimisation.
L’activité sur ln demande de consulter sa documentation, rappeler sa syntaxe et inspecter /usr/bin/g++ :
Code (sh) · sélectionner pour copier
ln -s cible nom_du_lien
ls -l /usr/bin/g++
-s crée un lien symbolique; ls -l le reconnaît par le premier caractère l et la flèche vers la cible. Un lien symbolique conserve un chemin, sans copier la cible; le cours distingue aussi les liens physiques.
La redirection vers /dev/null est une hypothèse à expliquer : ce fichier spécial écarte les écritures et donne une fin de fichier à la lecture. Si le nom utilisé pour invoquer le compilateur pointe vers lui, le compilateur attendu ne sera plus lancé. Cela n’efface pas par magie l’ancien exécutable : le problème est la redirection ou le remplacement de l’entrée qui y donnait accès. L’assistant demande de ne pas casser ainsi le compilateur de travail. Aucune modification de /usr/bin n’a été faite dans cette tâche.
6.2. ExempleFork.cpp : attendre avant et après la création
Compilation proposée dans la source : g++ ExempleFork.cpp -o ExempleFork. Les en-têtes unistd.h, sys/types.h, sys/wait.h déclarent les opérations de processus/attente, et stdio.h fournit printf.
Le parcours exact est :
- Le processus initial appelle
getpid()et affiche son PID. - Il appelle
wait(&status)avant d’avoir un enfant : l’appel retourne ici−1, au lieu d’attendre un enfant qui pourrait être créé plus tard. fork()crée le fils. Les deux processus continuent après l’appel : retour 0 dans le fils, PID du fils dans le père en cas de succès; −1 en cas d’échec, non traité séparément par ce vieil exemple.- La branche
pid==0du fils récupère son propre PID, affiche 5,4, 3,2, 1 avecsleep(1)à chaque tour, puis annonce sa fin. - Le père garde le PID retourné, annonce son attente, puis
wait(&status)attend cette fois réellement une terminaison d’enfant. Le retour permet son message final.
sleep(1) impose un délai au processus appelant; il ne remplace pas l’attente de l’enfant. getpid identifie, sans créer. wait concerne les enfants du processus; pthread_join concerne un thread, sans exiger une relation père/fils entre threads.
La ligne optionnelle active un accès invalide :
Code (cpp) · sélectionner pour copier
// int *pRandom = 0x000000; (*pRandom) = 0;
Dans la démonstration, la compilation passe mais le fils s’arrête au premier accès : déréférencer le pointeur nul est indéfini et provoque ici une faute de segmentation. Le décompte et le message final du fils ne s’achèvent plus. Le père peut quand même sortir de wait, puisque la fin anormale est aussi une terminaison. Son message final ne certifie pas le succès du fils.
Le execl("/usr/bin/ps", "ps", 0) commenté est une autre option du fichier. S’il réussissait, il remplacerait le programme du fils, sans créer un troisième processus. Son exécution n’est pas présentée comme une démonstration effectuée dans cette séance.
6.3. ExempleThreads1/2 : ordre de création, join et durée de vie
Les compilations proposées sont :
Code (sh) · sélectionner pour copier
g++ ExempleThreads1.cpp -o ExempleThreads1 -lpthread
g++ ExempleThreads2.cpp -o ExempleThreads2 -lpthread
Les fichiers sont anciens, avec pthread_yield() et exit(NULL); ils sont conservés sans annoncer une compilation vérifiée sur Windows ou leur modernisation. Les exécutions décrites ci-dessous sont celles de la séance, pas des essais réalisés pour l’utilisateur.
Threads 1 utilise N_THREADS=5. Dans la boucle 0 à 4, le principal annonce la création, prépare Arg[i]=i, puis appelle :
Code (cpp) · sélectionner pour copier
status = pthread_create(&threads[i], NULL, print_hello, &Arg[i]);
Le premier argument reçoit l’identifiant, le deuxième choisit les attributs — NULL=par défaut —, le troisième la fonction de départ et le quatrième l’argument. Le retour 0 indique le succès; sinon le code affiche l’erreur et termine. Chaque travailleur lit son numéro dans une case distincte de Arg, affiche son message puis appelle pthread_exit(NULL). Ce choix évite de faire lire à tous l’adresse d’un unique indice modifié par la boucle.
La création suit 0,1, 2,3, 4, mais les messages des travailleurs peuvent s’entrelacer autrement. Une sortie identique obtenue plusieurs fois n’est pas un ordre garanti. Attributs et priorités sont évoqués comme paramètres possibles, mais aucune configuration SJF/FCFS n’est implantée dans cet exemple.
La ligne optionnelle pthread_join(threads[i],NULL) se trouve dans la boucle de création. L’activer donne : créer 0→attendre sa fin→créer 1→attendre sa fin, etc. Les cinq activités deviennent séquentielles. Placer les joins dans une boucle après toutes les créations permet au contraire aux travailleurs de progresser concurremment, tout en garantissant leur terminaison avant la suite du principal.
Threads 2 contient N_SPINS=100000000 et deux fonctions. CalculIntense affiche le début, fait la boucle volatile de 100 millions d’itérations et affiche la fin. CalculAvecYield répète cinq tours : affichage d’itération, boucle N_SPINS/5, pthread_yield, puis message de cession. volatile préserve ici certaines opérations de calcul contre leur suppression; ce n’est pas un verrou.
La sélection est if (i>4) alors que i parcourt 0 à 4 : la branche CalculIntense n’est jamais choisie dans la version fournie. Les cinq travailleurs exécutent CalculAvecYield. Après leurs créations, une seconde boucle les rejoint tous. Les affichages varient selon les exécutions; yield n’est pas un rendez-vous où tous atteignent ensemble l’itération suivante.
En commentant les trois lignes des joins, le principal n’attend plus réellement les travailleurs : il fait seulement sleep(1) avant de sortir du processus. Une seconde laisse parfois suffisamment de temps dans la démonstration; un délai raccourci fait mieux apparaître le travail interrompu. Un sommeil ne garantit pas une terminaison. La sortie du processus met fin à ses threads inachevés, tandis que pthread_exit termine le thread appelant.
La question « avant qu’un thread soit créé? » diffère de « avant qu’il termine? ». Dans ce parcours, le principal exécute les créations avant sa sortie; sans join, il peut ensuite sortir avant la fin des travailleurs. Les threads partagent l’espace d’adressage du processus, contrairement aux deux processus séparés produits par fork.
6.4. Question 1 : diagnostiquer l’interblocage puis ajouter Peterson
Interblocage1.c crée deux travailleurs numérotés 0 et 1. Chacun effectue un million d’itérations sur count. ModuloPrint=100 règle la fréquence d’affichage et fflush(stdout) rend la progression visible. Le principal attend Count2 puis Count1. Le candidat d’entrée pose critical[process]=1, calcule other=1-process, puis attend tant que critical[other]==1. La sortie retire son propre drapeau.
Si les deux demandes sont posées avant leur test, chacun attend le retrait de l’autre. Aucun n’atteint la sortie; l’affichage arrête, mais les boucles tournent. Les étapes du diagnostic demandé sont :
Code (sh) · sélectionner pour copier
gcc -g -o Interblocage1 Interblocage1.c -lpthread
ulimit -c unlimited
./Interblocage1
# Dans un autre terminal, après arrêt de la progression :
top
pkill -11 Interblocage1
gdb Interblocage1 core
-g conserve les renseignements de débogage. ulimit s’applique au shell de l’exécution et autorise les core dumps, sous réserve de la configuration du noyau. top montre ici un CPU proche de 100 % : l’arrêt d’affichage ne signifie pas que le programme est endormi ou terminé. Le signal 11 est SIGSEGV dans l’environnement Linux du labo; on le provoque volontairement pour obtenir une image inspectable.
Dans GDB :
Code (text) · sélectionner pour copier
bt
print count
print critical
thread apply all backtrace
Le principal est arrêté dans pthread_join, car ses travailleurs n’ont pas fini. count localise la progression; [1,1] montre deux intentions d’entrée, pas deux accès déjà réussis à la section critique. Les backtraces des travailleurs montrent l’attente réciproque. Le cours propose de changer ModuloPrint si nécessaire pour rendre le scénario observable.
La solution de Peterson ajoute le départage waiting :
Code (c) · sélectionner pour copier
critical[process] = 1;
waiting = process;
while (waiting == process && critical[other] == 1) {
/* attente active */
}
/* section critique */
critical[process] = 0;
Le dernier demandeur inscrit son numéro et cède à l’autre lorsque celui-ci demande aussi. L’autre peut franchir l’attente, travailler puis baisser son drapeau; le demandeur en attente progresse alors. Ce n’est ni un sémaphore ni un sommeil : la boucle reste active. L’assistant ajoute la variable et la condition, puis observe la disparition du blocage dans son environnement.
L’énoncé propose d’augmenter N_ITER et de supprimer les printf pour tester plus intensément. Il exige la VM monocœur, sans transformer une observation réussie en garantie générale sur la sémantique mémoire C, les optimisations ou le multicœur. Un mutex est évoqué comme protection alternative; une solution supplémentaire complète n’est pas démontrée à cet endroit.
6.5. Question 2 : attendre ne fusionne pas la mémoire de fork
La globale value=5 précède fork. Le fils fait value+=15 : sa copie vaut 20. Le père fait wait(NULL) puis affiche sa propre copie, Value=5. L’oral peut dire « changer à 15 »; le code écrit l’addition de 15, ce qui tranche la valeur du fils.
Fork duplique logiquement l’espace d’adressage : chacun possède ensuite sa variable ordinaire. Le wait impose une dépendance de fin, sans recopier la nouvelle valeur du fils. Seul le père exécute le printf dans la branche else. Les en-têtes des appels de processus ne sont pas tous inclus dans le fichier historique; cela ne change pas le raisonnement demandé.
6.6. Question 3 : graphe de threads et compteur sans protection
Le principal initialise glob=0, crée inc_dec avec pas+1 puis−1, fait deux joins et affiche le résultat. Chaque travailleur réalise 100000 mises à jour glob=glob+pas, affiche puis termine par pthread_exit. Le graphe doit préserver deux branches concurrentes et les dépendances de fin :
Code (text) · sélectionner pour copier
Fil principal
├─ create(tid1) → inc_dec(+1) → fin du travailleur1
├─ create(tid2) → inc_dec(-1) → fin du travailleur2
└─ join(tid1), puis join(tid2) → affichage final → return0
La seconde création suit la première dans le principal; les activités des travailleurs peuvent néanmoins s’entrelacer, et le premier peut déjà avoir avancé avant le message « création réussie ». Chacun des joins attend la fin concernée; les calculs ne deviennent pas séquentiels pour autant.
La présence de pthread_spinlock_t *lock ne protège rien : il n’est pas utilisé. Les mises à jour de glob partagée ne sont pas atomiques et contiennent une course. Le bilan arithmétique +100000−100000 ne garantit donc pas zéro dans ce programme. Join garantit l’achèvement, pas la protection des accès.
Le PDF utilise th1/th2 dans les messages de création sans les déclarer; le code de l’archive utilise un/moinsun. Le corrigé représente les branches inc_dec+1 et−1 rejoignant les attentes du principal. Ce schéma explique l’ordre requis sans prétendre que le fragment PDF compile tel quel.
6.7. Question 4 : trois appels fork, huit processus
Le code est fork(); fork(); fork(); return 1;. Sous l’hypothèse que chaque création réussit, les descendants poursuivent les appels restants :
Code (text) · sélectionner pour copier
Avant les appels : 1 processus
Après fork1 : 2 processus (1 nouveau)
Après fork2 : 4 processus (2 nouveaux)
Après fork3 : 8 processus (4 nouveaux)
Le total est 8, initial inclus, soit 7 nouveaux. Le deuxième appel n’est pas réalisé seulement par l’original : les deux processus présents le réalisent. Le troisième est exécuté par les quatre. L’énoncé demande ce total, pas seulement trois enfants directs. Le fichier historique manque la déclaration adéquate de fork; aucune compilation personnelle n’est attestée ici.
6.8. Question 5 : un contre-exemple pour le mauvais placement de turn
Le candidat lève flag[myself], attend flag[other] && turn==other, puis, dans leave, fait turn=other avant de retirer son drapeau. Le corrigé déplace l’affectation dans enter après le drapeau. Sa justification par une mauvaise initialisation est insuffisante : une globale C int turn; est initialisée à 0, et cela ne corrige pas les décisions d’entrée.
Voici un contre-exemple dès turn=0 et deux drapeaux faux, dans le modèle entrelacé pédagogique :
- Thread 1 lève flag 1, lit flag 0 encore faux et franchit le test; il est suspendu avant sa section critique.
- Thread 0 lève flag 0. Il voit flag 1 vrai, mais son other=1 et
turn==1est faux; il franchit aussi. - Thread 0 entre; thread 1 reprend l’entrée qu’il avait déjà autorisée. Les deux peuvent être dans la section critique.
On obtient le contre-exemple symétrique avec turn=1 après une sortie antérieure du thread 0. L’échec concerne le placement du départage, pas seulement l’existence de deux drapeaux ni une valeur initiale à choisir. Placer turn=other à chaque entrée, après l’annonce d’intention, donne la forme usuelle de Peterson. Ce déroulement précise l’argument du corrigé et ne prétend pas reprendre mot à mot celui de la vidéo.
6.9. Question 6 : création 0 à 5, affichage 2,3, 1,4, 5,0
Le gabarit est appelé ExempleThreads3.c dans l’énoncé et question6.c dans l’archive. Il possède six sémaphores, la permutation Ordre={2,3,1,4,5,0}, des paramètres individuels et les joins, mais aucun appel de synchronisation n’est encore implanté dans le départ fourni.
Les lignes grises sont protégées et l’ordre de création doit rester 0,1, 2,3, 4,5. Les emplacements d’ajout dans main et les paramètres peuvent être complétés. L’essentiel de la synchronisation doit résider dans les travailleurs, car le principal attend immédiatement après les créations. Ordre est supposé bien formé; l’énoncé permet de ne pas contrôler les codes de retour sem_xxx pour cet exercice.
La solution du PDF Question6_solution, distinct du squelette encore non rempli dans LAB3Solution, construit un successeur NextThread :
| Thread | Successeur |
|---|---|
| 2 | 3 |
| 3 | 1 |
| 1 | 4 |
| 4 | 5 |
| 5 | 0 |
| 0 | Aucun : −1 |
Pour chaque numéro i, la recherche parcourt Ordre, j=0 à N_THREADS−2; si Ordre[j]==i, son successeur est Ordre[j+1]. Le −1 par défaut reste pour le dernier. Chaque travailleur reçoit son propre numéro et son successeur avant sa création.
Le parcours du principal est :
Code (text) · sélectionner pour copier
Pour chaque sémaphore i : sem_init(Semaphore[i], pshared=0, valeur=0)
sem_post(Semaphore[Ordre[0]])
Pour i=0 à5 :
préparer ThreadNum=i et NextThread selon la permutation
créer thread i avec son propre bloc de paramètres
Pour i=0 à5 : joindre thread i
Pour chaque sémaphore i : sem_destroy
Le travailleur attend avant son effet, puis transmet après :
Code (c) · sélectionner pour copier
sem_wait(&Semaphore[pParam->ThreadNum]);
printf("Je suis le thread %d.\n", pParam->ThreadNum);
if (pParam->NextThread != -1)
sem_post(&Semaphore[pParam->NextThread]);
pthread_exit(NULL);
L’initialisation à 0 bloque tous sauf celui dont le jeton est fourni. Le post initial est placé avant les créations dans le corrigé : le sémaphore conserve ce jeton jusqu’au wait du thread 2. Si thread 2 affiche avant la création de 3, son post est aussi conservé pour le futur wait de 3. Cette mémoire donne la correction malgré l’ordre 0 à 5 des créations.
La chaîne établit printf 2→post 3→printf 3→post 1→printf 1→post 4→printf 4→post 5→printf 5→post 0→printf 0. Un travailleur prêt sans jeton ne passe pas son attente, quelle que soit la vitesse du CPU ou l’ordre décidé par l’ordonnanceur. Le dernier ne poste pas vers un indice−1. Les joins garantissent que les sémaphores ne sont détruits qu’après les travailleurs.
La sortie contient donc 2,3, 1,4, 5,0 à chaque exécution correcte. La contrainte concerne ces affichages, sans exiger que toutes les instructions de chaque thread deviennent une activité entièrement séquentielle. Dans une implémentation réelle, les échecs de création et d’attente nécessiteraient un traitement; le corrigé applique la simplification autorisée par l’exercice. Aucun programme corrigé n’a été compilé ni soumis pour l’utilisateur dans cette tâche.
Comparaison de deux exercices distincts. Question 2 : avant fork, value=5. Après fork réussi, le fils modifie sa copie par value+=15, soit 20; le père attend la fin du fils avec wait puis affiche sa propre value=5. L’attente ne fusionne pas les espaces d’adressage. Question 3 : dans un seul processus, le principal initialise glob=0, crée tid1 inc_dec(+1), puis tid2 inc_dec(−1). Chaque travailleur réalise 100000 mises à jour sur la même glob, affiche et termine. Le principal joint tid1, puis tid2, puis affiche et retourne. Les traits vers les joins expriment la terminaison attendue, pas l’ordre réel des calculs. Le premier travailleur peut avancer avant la seconde création; les deux peuvent ensuite progresser concurremment. Join ne protège pas glob; le pointeur de spinlock déclaré n’est pas utilisé. La somme idéale +100000−100000=0 ne garantit pas zéro en présence de la course C. Aucun ordre d’affichage ni résultat final numérique des threads n’est promis. Les échecs de création sont hors du modèle pédagogique; aucune compilation ou exécution C n’est attestée.
Source : Maître S06 du 14 octobre 2026 — ajout éditorial WEB-10, sources originales conservées · §6.5–6.6, lignes 692–713 ; LAB3Enonce et code_lab3.zip (question2.c, question3.c), LAB3Solution ; ED14–ED15. Deux exercices séparés, sans PID ou durée inventés.