0% ont trouvé ce document utile (0 vote)
0 vues20 pages

Correction_Devoir_Programmation

Ce corrigé détaillé pour un devoir de programmation système couvre des concepts clés tels que l'ordonnancement Round Robin, les signaux et la gestion des processus. Chaque exercice inclut des rappels théoriques, des simulations pas à pas et des solutions complètes, permettant aux étudiants de comprendre et de reproduire des exercices similaires. Le document met également l'accent sur l'importance de la gestion du temps lors des examens en fournissant des formats de réponse condensés.

Transféré par

superwhitehacking
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
0 vues20 pages

Correction_Devoir_Programmation

Ce corrigé détaillé pour un devoir de programmation système couvre des concepts clés tels que l'ordonnancement Round Robin, les signaux et la gestion des processus. Chaque exercice inclut des rappels théoriques, des simulations pas à pas et des solutions complètes, permettant aux étudiants de comprendre et de reproduire des exercices similaires. Le document met également l'accent sur l'importance de la gestion du temps lors des examens en fournissant des formats de réponse condensés.

Transféré par

superwhitehacking
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

Corrigé détaillé — Devoir de Programmation Système

Master 1 — Université Joseph Ki-Zerbo — Enseignant : Dr Tegawendé F.


BISSYANDÉ

7 Mars 2026

Table des matières


Avant-propos 3

Exercice 1 : Questions de cours (8 points) 4


Q1 (2 points) — Ordonnancement Round Robin . . . . . . . . . . . . . . . . . . 4
Rappel du principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Données de l’énoncé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Simulation pas à pas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Temps de séjour (turnaround) . . . . . . . . . . . . . . . . . . . . . . . . . 5
Q2 (3 points) — Signaux : envoi de SIGUSR1 . . . . . . . . . . . . . . . . . . . 6
Rappel des notions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Déroulement précis, étape par étape . . . . . . . . . . . . . . . . . . . . . 6
Cas particulier : signal envoyé pendant un appel système bloquant . . . . . 7
Q3 (3 points) — Tubes (pipes) et redirections . . . . . . . . . . . . . . . . . . . 8
Code étudié . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1. Propriétés de fd[0] et fd[1] après pipe(fd) . . . . . . . . . . . . . . . . 8
2. Rôle de dup2(fd[1], STDOUT_FILENO) dans le fils . . . . . . . . . . . . . 8
3. Que se passe-t-il si le père oublie de fermer fd[1] ? . . . . . . . . . . . . 9

Exercice 2 : Producteurs-Consommateurs (6 points) 10


Rappel des notions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Question 1 (2 points) — Conditions de course et solution . . . . . . . . . . . . . 10
Conditions de course possibles (sans synchronisation) . . . . . . . . . . . . 10
Solution avec sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Question 2 (4 points) — Code C complet . . . . . . . . . . . . . . . . . . . . . . 11
Remarques pédagogiques importantes . . . . . . . . . . . . . . . . . . . . 13

Exercice 3 : Lecteurs-Rédacteurs (6 points) 15


Rappel des notions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Question 1 (2 points) — Approche readers-preference . . . . . . . . . . . . . . 15
Sémaphores/mutex nécessaires . . . . . . . . . . . . . . . . . . . . . . . . 15
Algorithme (rappel) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Pourquoi cette approche peut affamer les rédacteurs . . . . . . . . . . . . 16
Question 2 (4 points) — Solution writers-preference en C . . . . . . . . . . . . 16
Principe de la solution (2ᵉ problème des lecteurs-rédacteurs) . . . . . . . . 16

1
Code C complet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Analyse de la solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

Récapitulatif des points clés à retenir 20

2
Avant-propos
Ce corrigé ne se contente pas de donner les réponses : chaque exercice commence par
un rappel des notions nécessaires, puis propose une solution complète et justifiée.
L’objectif est que ce document puisse servir de support autonome pour comprendre et
refaire des exercices similaires.
Important — gestion du temps en examen : les explications détaillées (étape par
étape) servent à comprendre et à vérifier ton raisonnement au brouillon. Elles ne
représentent pas ce qu’il faut recopier intégralement sur la copie. Chaque question se
termine par un encadré « Réponse à donner sur la copie », qui indique le format
condensé réellement attendu par un correcteur, compatible avec le temps imparti (le
barème donne une indication : une question à 2 points ne doit pas prendre 20 minutes à
rédiger).

3
Exercice 1 : Questions de cours (8 points)
Q1 (2 points) — Ordonnancement Round Robin
Rappel du principe

Le Round Robin (RR) est un algorithme d’ordonnancement préemptif : chaque


processus reçoit un temps CPU maximal appelé quantum. Les processus prêts sont
rangés dans une file FIFO circulaire.
Règles de simulation :
1. Le processus en tête de file s’exécute pendant min(quantum, temps restant).
2. S’il n’a pas terminé à la fin du quantum, il repart à la fin de la file.
3. Les nouveaux processus qui arrivent pendant l’exécution d’un quantum sont
ajoutés à la file avant que le processus interrompu n’y retourne (convention
standard, celle utilisée ici).
4. Le temps de séjour (turnaround time) d’un processus = date de fin d'exécution
− date d'arrivée.

Données de l’énoncé

Processus Arrivée Durée (burst)


P1 0 7
P2 2 4
P3 4 8
P4 6 3

Quantum = 3.

Simulation pas à pas

Étape File avant Élu Intervalle Restant Événement


1 [P1] P1 0→3 4 P2 arrive
(t=2)
2 [P2, P1] P2 3→6 1 P3 arrive
(t=4), P4
arrive (t=6)
3 [P1, P3, P4, P1 6→9 1 —
P2]
4 [P3, P4, P2, P3 9 → 12 5 —
P1]
5 [P4, P2, P1, P4 12 → 15 0 (fin) —
P3]
6 [P2, P1, P3] P2 15 → 16 0 (fin) P2 n’avait
plus que 1
unité
7 [P1, P3] P1 16 → 17 0 (fin) —
8 [P3] P3 17 → 20 2 —
9 [P3] P3 20 → 22 0 (fin) —

4
Lecture du tableau, étape par étape :
• Étape 1 : la file ne contient que P1 (seul arrivé à t=0). On l’exécute de 0 à 3
(quantum=3). Il lui restait 7, donc il reste 7−3 = 4. Pendant ce temps, P2 arrive
(t=2) et est ajouté en fin de file. P1 n’ayant pas fini, il repart aussi en fin de file →
nouvelle file : [P2, P1].
• Étape 2 : on exécute P2 (en tête) de 3 à 6. Il lui restait 4, donc il reste 4−3 = 1.
Pendant ce temps, P3 (t=4) puis P4 (t=6) arrivent et sont ajoutés à la file, puis P2
(pas fini) repart en fin de file → file : [P1, P3, P4, P2].
• Étape 3 : on exécute P1 (en tête) de 6 à 9. Il lui restait 4, donc il reste 4−3 = 1.
Aucun nouvel arrivant. P1 repart en fin de file → file : [P3, P4, P2, P1].
• Étape 4 : on exécute P3 de 9 à 12. Il lui restait 8, donc il reste 8−3 = 5. P3 repart
en fin de file → file : [P4, P2, P1, P3].
• Étape 5 : on exécute P4 de 12 à 15. Il lui restait exactement 3, donc il termine
(reste 0). P4 quitte définitivement le système → file : [P2, P1, P3].
• Étape 6 : on exécute P2, qui n’avait plus que 1 unité restante (et non 3) : il tourne
seulement 1 unité, de 15 à 16, puis termine → file : [P1, P3].
• Étape 7 : on exécute P1, qui n’avait plus que 1 unité restante : il tourne de 16 à 17,
puis termine → file : [P3].
• Étape 8 : on exécute P3, qui avait encore 5 : il tourne 3 unités (quantum complet),
de 17 à 20, il reste 5−3 = 2. Il repart en fin de file → file : [P3] (seul restant).
• Étape 9 : on exécute P3 une dernière fois, pour ses 2 dernières unités, de 20 à 22,
puis il termine.
Ordre d’exécution :
P1(0-3) → P2(3-6) → P1(6-9) → P3(9-12) → P4(12-15) → P2(15-16) → P1(16-
17) → P3(17-20) → P3(20-22)
Vérification de cohérence : la somme des tranches (3+3+3+3+3+1+1+3+2 = 22) doit
être égale à la somme des durées (7+4+8+3 = 22). C’est bien le cas, ce qui confirme
qu’il n’y a pas d’erreur de calcul.

Temps de séjour (turnaround)

Processus Fin d’exécution Arrivée Turnaround = Fin − Arrivée


P1 17 0 17
P2 16 2 14
P3 22 4 18
P4 15 6 9

17 + 14 + 18 + 9 58
Turnaround moyen = = = 14,5
4 4

5
Réponse à donner sur la copie (format minimal, suffisant pour 2 points) :
Un diagramme de Gantt est en lui-même la justification attendue — inutile de
réécrire les étapes en phrases.
Gantt : | P1 | P2 | P1 | P3 | P4 | P2 | P1 | P3 | P3 |
0 3 6 9 12 15 16 17 20 22
Ordre d’exécution : P1 → P2 → P1 → P3 → P4 → P2 → P1 → P3 → P3
Turnaround = fin − arrivée : P1 = 17−0 = 17 ; P2 = 16−2 = 14 ; P3 = 22−4 =
18 ; P4 = 15−6 = 9
Turnaround moyen = (17+14+18+9)/4 = 14,5
Remarque examen : le tableau détaillé et les explications ci-dessus servent à
vérifier ton raisonnement au brouillon ; sur la copie, seuls ce diagramme, cet ordre
et ce calcul sont nécessaires (5 à 7 minutes suffisent avec de l’entraînement).

Q2 (3 points) — Signaux : envoi de SIGUSR1


Rappel des notions

• Un signal est une notification asynchrone envoyée à un processus (ici via kill()
ou équivalent) pour l’informer d’un événement.
• Chaque processus possède une table de dispositions (une par signal) : ignorer,
action par défaut, ou handler (fonction utilisateur installée via signal()/sigaction()).
• Le PCB (rappelé dans l’énoncé) contient notamment les signaux en attente
(pending signals) et le masque de signaux (signaux temporairement bloqués).

Déroulement précis, étape par étape

1. Émission du signal (côté émetteur) Le processus émetteur appelle un appel


système (kill(pid, SIGUSR1)). Le noyau vérifie les droits (même utilisateur ou
privilèges suffisants), puis positionne le bit correspondant à SIGUSR1 dans le champ
signaux en attente du PCB du processus cible. L’émission elle-même ne provoque pas
d’exécution immédiate du handler : le signal est seulement marqué « en attente ».
2. Réveil / prise en compte par le noyau Le signal n’est réellement délivré que lorsque
le noyau reprend la main sur le processus cible, c’est-à-dire : - au prochain passage du
processus cible en mode noyau → mode utilisateur (retour d’interruption, fin de quantum,
retour d’appel système), ou - immédiatement s’il était bloqué en attente de ce signal
précis (par exemple en sigwait()), auquel cas il repasse de l’état Bloqué à Prêt.
Le noyau consulte alors le masque de signaux du PCB : si SIGUSR1 n’est pas bloqué, la
délivrance se poursuit.
3. Transition d’états du processus - Si le processus cible était Prêt ou En cours
d’exécution : pas de changement d’état lié au signal lui-même, mais son flux d’exécution
normal est interrompu au prochain point de contrôle. - Si le processus cible était Bloqué
(attente d’E/S, sleep(), etc.) et que le signal n’est pas masqué : il repasse à l’état
Prêt, sera réélu par l’ordonnanceur, et le handler sera exécuté avant la reprise du code
interrompu.
4. Exécution du handler Quand le processus repasse en mode utilisateur, le noyau : 1.

6
Sauvegarde le contexte courant (registres, pointeur d’instruction) — ces informations
transitent par le PCB. 2. Construit une pile de signal (signal frame) qui simule un appel
de fonction vers le handler. 3. Fait « sauter » l’exécution vers le handler installé (ici
la fonction qui affiche "Signal reçu !"). 4. Le handler s’exécute en mode utilisateur,
exactement comme du code normal du processus (il partage donc le même espace
mémoire, les mêmes descripteurs de fichiers, etc., visibles dans le PCB).
5. Terminaison Le handler appelle (implicitement ou explicitement) exit(). Ceci
déclenche un appel système qui : - libère les ressources du processus (mémoire,
descripteurs de fichiers ouverts — fermés un à un), - passe l’état du processus dans
le PCB à Terminé (zombie) : le PCB n’est pas immédiatement détruit, il conserve le
code de sortie jusqu’à ce que le processus parent appelle wait()/waitpid() pour le
récupérer, - une fois le parent ayant récupéré le code retour, le PCB est définitivement
libéré par le noyau.

Cas particulier : signal envoyé pendant un appel système bloquant

Si au moment de la réception du signal le processus cible est au milieu d’un appel


système bloquant (read(), wait(), sleep()…) :
• L’appel système est interrompu.
• Deux comportements possibles selon le drapeau SA_RESTART positionné à
l’installation du handler :
– Sans SA_RESTART : l’appel système retourne immédiatement avec la valeur
d’erreur -1 et errno = EINTR. C’est ensuite au programmeur de vérifier ce cas
et de décider de relancer ou non l’appel.
– Avec SA_RESTART : le noyau relance automatiquement l’appel système après
exécution du handler, de façon transparente pour le programme.
• Dans notre cas précis, cela importe peu pour la suite : puisque le handler appelle
exit(), le processus se termine directement après l’exécution du handler, sans
jamais revenir à l’appel système interrompu (qu’il ait été relancé ou non).

Réponse à donner sur la copie (synthèse en 5 points) :


1. Le noyau marque SIGUSR1 comme signal en attente dans le PCB du processus
cible (aucune exécution immédiate).
2. Le signal n’est délivré qu’au prochain passage en mode utilisateur (ou
immédiatement si le processus était bloqué en attente de ce signal, avec transition
Bloqué → Prêt).
3. Le noyau sauvegarde le contexte courant, construit une pile de signal et
transfère l’exécution vers le handler installé.
4. Le handler affiche ”Signal reçu !” puis appelle exit() : ressources libérées, état
du PCB → Terminé (zombie) jusqu’à récupération par le parent via wait().
5. Si le signal arrive pendant un appel bloquant : l’appel est interrompu (retour
EINTR sans SA_RESTART, ou relance automatique avec SA_RESTART) ; mais comme
le handler appelle exit(), le processus se termine de toute façon sans y revenir.

7
Q3 (3 points) — Tubes (pipes) et redirections
Code étudié

pipe(fd);
if (fork() == 0) {
// processus fils
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", "-l", NULL);
} else {
// processus père
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}

1. Propriétés de fd[0] et fd[1] après pipe(fd)

pipe(fd) crée un tube (buffer FIFO en mémoire noyau) et retourne deux descripteurs
de fichiers liés à ce même tube :

Descripteur Rôle Sens Fermeture


fd[0] Extrémité de lecture seule Aucune fermeture
lecture (read()) automatique : doit
être fermé
explicitement par
close()
fd[1] Extrémité écriture seule Idem, fermeture
d’écriture (write()) manuelle
obligatoire

Points importants : - Le tube est unidirectionnel : ce qui est écrit dans fd[1] peut être
lu dans fd[0], jamais l’inverse. - Par défaut, les deux extrémités sont en mode bloquant
: read() bloque tant qu’il n’y a rien à lire (et que l’écriture n’est pas fermée), write()
bloque si le tampon du tube est plein. - Après fork(), les deux processus (père et fils)
héritent des deux descripteurs fd[0] et fd[1] (copie de la table des descripteurs)
: c’est justement ce qui permet de les utiliser pour communiquer. - Les descripteurs
restent ouverts après un exec*() sauf si le flag FD_CLOEXEC a été positionné (ce n’est pas
le cas ici).

2. Rôle de dup2(fd[1], STDOUT_FILENO) dans le fils

dup2(fd[1], STDOUT_FILENO) fait pointer le descripteur 1 (la sortie standard,


STDOUT_FILENO) vers la même ressource ouverte que fd[1] (l’extrémité d’écriture
du tube). Concrètement, tout ce qui sera désormais écrit sur stdout (descripteur 1) ira
en réalité dans le tube.

8
Pourquoi est-ce indispensable avant execlp ? - La commande ls ne connaît rien
du tube : elle écrit toujours, par convention Unix, sur le descripteur 1 (printf, write(1,
...), etc.). - exec*() remplace entièrement le code du processus (le programme ls
écrase le programme courant en mémoire), mais conserve la table des descripteurs
de fichiers ouverts. - Il faut donc faire la redirection avant l’appel à execlp, car après
l’appel, il n’y a plus aucune instruction du programme fils qui puisse s’exécuter — le code
de ls a pris sa place. Si dup2 était placé après execlp, il ne serait tout simplement jamais
exécuté.
Ainsi, grâce à ce montage, ls -l croit écrire sur le terminal alors qu’il écrit en réalité
dans le tube, qui sera lu par le processus père (ici exécutant wc -l).
(close(fd[0]) dans le fils est également nécessaire par hygiène : le fils n’a pas besoin
de l’extrémité de lecture.)

3. Que se passe-t-il si le père oublie de fermer fd[1] ?

C’est une erreur classique qui provoque un blocage (deadlock applicatif) du pipeline.
Explication : - Un tube ne signale la fin des données (EOF, read() retournant 0) à son
lecteur que lorsque toutes les extrémités d’écriture ont été fermées, dans tous
les processus qui les détiennent. - Si le père ne ferme pas fd[1], alors après son
execlp("wc", ...), ce descripteur fd[1] reste ouvert dans le processus qui exécute wc
(les descripteurs survivent à exec). - Résultat : même quand le fils (ls) a fini d’écrire
et fermé son extrémité d’écriture, il reste une extrémité d’écriture ouverte —
celle du père/wc lui-même. - wc, en lisant sur son entrée standard (fd[0] dupliqué
sur STDIN_FILENO), ne recevra jamais de EOF, car le tube « pense » qu’un écrivain
(potentiel) existe toujours. - Conséquence concrète : wc -l reste bloqué indéfiniment
en attente de données supplémentaires, le pipeline ne se termine jamais (il faut le tuer
manuellement, ex. Ctrl+C).
Règle générale à retenir : dans tout usage de pipe(), chaque processus doit fermer
l’extrémité du tube qu’il n’utilise pas, dès que possible.

Réponse à donner sur la copie :


1. fd[0] = extrémité de lecture seule ; fd[1] = extrémité d’écriture seule. Pas
de fermeture automatique, héritées après fork(), conservées après exec() sauf
FD_CLOEXEC.
2. dup2(fd[1], STDOUT_FILENO) redirige la sortie standard du fils vers le tube.
Nécessaire avant execlp car exec remplace tout le code du processus : après
l’appel, aucune instruction du fils ne peut plus s’exécuter.
3. Si le père ne ferme pas fd[1], une extrémité d’écriture reste ouverte (héritée
par wc après son propre exec). Le tube ne renvoie jamais EOF → wc reste bloqué
indéfiniment en lecture.

9
Exercice 2 : Producteurs-Consommateurs (6 points)
Rappel des notions
• Un sémaphore est un compteur entier protégé, muni de deux opérations atomiques
: wait() (ou P(), décrémente ; bloque si le compteur devient négatif) et signal()
(ou V(), incrémente et réveille éventuellement un processus en attente).
• Un sémaphore binaire utilisé comme mutex (ici initialisé à 1) garantit l’exclusion
mutuelle sur une section critique, exactement comme un pthread_mutex_t, mais
avec l’API sem_t.
• Le problème producteurs-consommateurs avec buffer circulaire de taille N
nécessite de synchroniser sur deux conditions :
– le buffer ne doit jamais être plein quand un producteur écrit,
– le buffer ne doit jamais être vide quand un consommateur lit.

Question 1 (2 points) — Conditions de course et solution


Conditions de course possibles (sans synchronisation)

1. Écritures concurrentes des deux producteurs : P1 et P2 peuvent tous deux lire


la même valeur de l’index in, écrire chacun à cette position (l’un écrase l’écriture de
l’autre), puis incrémenter in deux fois de façon non atomique → perte de données,
ou incrémentation « perdue » (les deux threads lisent in, l’incrémentent localement,
réécrivent la même valeur : in n’avance que d’une unité au lieu de deux).
2. Lectures concurrentes des trois consommateurs : deux consommateurs
peuvent lire la même case du buffer avant que out soit mis à jour (double lecture
de la même valeur, valeurs consommées deux fois), ou incrémenter out de façon
non atomique (même problème que ci-dessus).
3. Débordement du buffer (overflow) : sans compteur protégé du nombre
d’éléments présents, un producteur peut écrire alors que le buffer est déjà
plein, écrasant une donnée non encore consommée.
4. Lecture sur buffer vide (underflow) : un consommateur peut tenter de lire alors
qu’aucune donnée n’est disponible, lisant une valeur non initialisée ou obsolète.
5. Interruption au milieu d’une mise à jour d’index : in = (in+1) % N n’est pas
une opération atomique (lecture, calcul, écriture) ; un changement de contexte au
milieu peut corrompre l’état partagé.

Réponse à donner sur la copie :


Conditions de course : (1) écritures concurrentes de P1/P2 sur le même index
in ; (2) lectures concurrentes des 3 consommateurs sur le même index out ;
(3) débordement du buffer (overflow) si aucun compteur protégé du nombre
d’éléments ; (4) lecture sur buffer vide (underflow) ; (5) mise à jour non atomique
des index in/out.
Solution : empty (init. N) compte les cases libres, full (init. 0) compte les cases
occupées, mutex (init. 1) protège le buffer et les index. Toujours wait(empty/full)
avant wait(mutex), jamais l’inverse (sinon interblocage).

Solution avec sémaphores

On utilise exactement les trois sémaphores demandés :

10
• empty (initialisé à N) : compte le nombre de cases libres dans le buffer. Un producteur
doit décrémenter empty avant d’écrire (bloque si empty == 0, c’est-à-dire buffer
plein).
• full (initialisé à 0) : compte le nombre de cases occupées. Un consommateur doit
décrémenter full avant de lire (bloque si full == 0, c’est-à-dire buffer vide).
• mutex (sémaphore binaire initialisé à 1) : protège l’accès au buffer lui-même et aux
index in/out, pour garantir qu’un seul producteur/consommateur à la fois manipule
ces variables partagées.
Ordre des opérations essentiel : wait(empty) / wait(full) doit être fait avant
wait(mutex), jamais après — sinon on risque un interblocage (un consommateur
bloqué sur wait(full) alors qu’il détient déjà mutex empêcherait tout producteur de
produire).

Question 2 (4 points) — Code C complet

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

#define N 5
#define P1_MAX 100
#define P2_MAX 200

int buffer[N];
int in = 0, out = 0;

sem_t empty, full, mutex;

/* ---- Section critique d'insertion ---- */


void produire(int valeur) {
sem_wait(&empty); /* attend qu'il y ait une case libre */
sem_wait(&mutex); /* entre en section critique */

buffer[in] = valeur;
printf("[Producteur] insere %d en position %d\n", valeur, in);
in = (in + 1) % N;

sem_post(&mutex); /* quitte la section critique */


sem_post(&full); /* signale une case occupee de plus */
}

/* ---- Section critique de retrait ---- */


int consommer(int id) {
int valeur;

sem_wait(&full); /* attend qu'il y ait une donnee dispo */


sem_wait(&mutex); /* entre en section critique */

11
valeur = buffer[out];
printf("[Consommateur %d] retire %d en position %d\n", id, valeur, out);
out = (out + 1) % N;

sem_post(&mutex); /* quitte la section critique */


sem_post(&empty); /* signale une case libre de plus */

return valeur;
}

/* ---- Thread producteur 1 : produit 1..100 ---- */


void* producteur1(void* arg) {
for (int i = 1; i <= P1_MAX; i++) {
produire(i);
}
return NULL;
}

/* ---- Thread producteur 2 : produit 101..200 ---- */


void* producteur2(void* arg) {
for (int i = 101; i <= P2_MAX; i++) {
produire(i);
}
return NULL;
}

/* ---- Coordination de la terminaison des consommateurs ----


Les producteurs produisent au total (P1_MAX) + (P2_MAX - 100)
elements. On utilise un compteur partage "restant", protege par
son propre semaphore binaire, pour repartir la consommation entre
les 3 threads consommateurs sans depasser le total. */
int restant;
sem_t mutex_restant;

void* consommateur(void* arg) {


int id = *(int*)arg;
while (1) {
sem_wait(&mutex_restant);
if (restant <= 0) {
sem_post(&mutex_restant);
break; /* plus rien a consommer : on arrete */
}
restant--;
sem_post(&mutex_restant);

consommer(id);
}
return NULL;

12
}

int main() {
pthread_t p1, p2, c1, c2, c3;
int id1 = 1, id2 = 2, id3 = 3;

restant = P1_MAX + (P2_MAX - 100); /* 100 + 100 = 200 elements */

sem_init(&empty, 0, N);
sem_init(&full, 0, 0);
sem_init(&mutex, 0, 1);
sem_init(&mutex_restant, 0, 1);

pthread_create(&p1, NULL, producteur1, NULL);


pthread_create(&p2, NULL, producteur2, NULL);
pthread_create(&c1, NULL, consommateur, &id1);
pthread_create(&c2, NULL, consommateur, &id2);
pthread_create(&c3, NULL, consommateur, &id3);

pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_join(c1, NULL);
pthread_join(c2, NULL);
pthread_join(c3, NULL);

sem_destroy(&empty);
sem_destroy(&full);
sem_destroy(&mutex);
sem_destroy(&mutex_restant);

return 0;
}

Compilation (penser à lier la bibliothèque pthread) :


gcc -o prodcons prodcons.c -lpthread

Réponse à donner sur la copie : le code complet ci-dessus, avec au


minimum : sem_init(&empty,0,N), sem_init(&full,0,0), sem_init(&mutex,0,1) ;
la fonction produire() respectant l’ordre wait(empty) → wait(mutex) → écriture
→ signal(mutex) → signal(full) ; la fonction consommer() symétrique ; les
threads producteurs/consommateurs créés avec pthread_create et attendus avec
pthread_join dans main().

Remarques pédagogiques importantes

• Le triplet wait(empty) → wait(mutex) → … → signal(mutex) → signal(full) (et


symétriquement côté consommateur) est le patron classique à retenir pour tout
problème producteurs-consommateurs à buffer borné.

13
• Le compteur restant (protégé par mutex_restant) n’est pas exigé par l’énoncé
strict, mais il est nécessaire en pratique pour que les threads consommateurs
sachent quand s’arrêter (sinon ils bloqueraient indéfiniment sur wait(full) une
fois toute la production terminée). C’est une bonne pratique à mentionner même si
une version plus simple avec un nombre d’itérations fixe par consommateur serait
aussi acceptée.
• sem_init(&sem, 0, valeur) : le second paramètre à 0 signifie que le sémaphore
est partagé entre threads d’un même processus (et non entre processus, ce qui
nécessiterait un sémaphore nommé ou en mémoire partagée).

14
Exercice 3 : Lecteurs-Rédacteurs (6 points)
Rappel des notions
Le problème des lecteurs-rédacteurs modélise l’accès à une ressource partagée (ici
database) où : - plusieurs lecteurs peuvent accéder simultanément (la lecture ne
modifie rien), - un rédacteur nécessite un accès exclusif (ni lecteur, ni autre rédacteur
en même temps).
Il existe deux variantes classiques : - Préférence lecteurs (1ᵉʳ problème) : favorise les
lecteurs, au risque d’affamer les rédacteurs. - Préférence rédacteurs (2ᵉ problème) :
favorise les rédacteurs, au risque d’affamer les lecteurs (mais c’est ce qui est demandé
ici).

Question 1 (2 points) — Approche readers-preference


Sémaphores/mutex nécessaires

• rw_mutex (binaire, init. 1) : protège l’accès exclusif à la base de données ; pris par
le premier lecteur qui entre, relâché par le dernier lecteur qui sort ; pris/relâché
directement par un rédacteur.
• mutex (binaire, init. 1) : protège la variable partagée readcount (compteur de
lecteurs actifs).
• readcount (entier, init. 0).

Algorithme (rappel)

Lecteur:
wait(mutex)
readcount++
if readcount == 1: // premier lecteur : verrouille la base
wait(rw_mutex)
signal(mutex)

... lire database ...

wait(mutex)
readcount--
if readcount == 0: // dernier lecteur : libere la base
signal(rw_mutex)
signal(mutex)

Redacteur:
wait(rw_mutex)
... ecrire database ...
signal(rw_mutex)

15
Pourquoi cette approche peut affamer les rédacteurs

rw_mutex n’est verrouillé que par le premier lecteur d’un « groupe » et libéré que
par le dernier. Tant qu’il y a en permanence au moins un lecteur actif — c’est-
à-dire qu’un nouveau lecteur arrive avant que readcount ne retombe à 0 — rw_mutex
reste continuellement détenu par le camp des lecteurs. Un rédacteur en attente sur
wait(rw_mutex) n’a aucune priorité : il attend derrière une file de lecteurs qui peut,
en théorie, ne jamais se tarir. C’est la définition même de la famine (starvation)
: le rédacteur peut attendre indéfiniment alors que des ressources CPU/accès sont
constamment accordées à d’autres.
Réponse à donner sur la copie :
Sémaphores : rw_mutex (init. 1, accès exclusif à la base) et mutex (init. 1, protège
readcount).
Algorithme lecteur : incrémente readcount sous mutex ; le premier lecteur
verrouille rw_mutex ; le dernier lecteur (readcount=0) le libère. Le rédacteur
verrouille/libère rw_mutex directement.
Famine des rédacteurs : rw_mutex reste détenu tant qu’il y a en permanence au
moins un lecteur actif (un nouveau lecteur arrivant avant que le dernier ne parte)
; le rédacteur en attente n’a aucune priorité et peut attendre indéfiniment.

Question 2 (4 points) — Solution writers-preference en C


Principe de la solution (2ᵉ problème des lecteurs-rédacteurs)

On ajoute un mécanisme qui bloque l’arrivée de nouveaux lecteurs dès qu’un


rédacteur est en attente, en utilisant un sémaphore supplémentaire wrt qui protège
writecount et sert de verrou de priorité :
• Dès qu’un rédacteur se présente (writecount passe de 0 à 1), il verrouille rw_mutex
— soit immédiatement s’il est libre, soit en attente s’il est détenu par des lecteurs
déjà actifs.
• Tout nouveau lecteur doit d’abord franchir la porte wrt (wait(wrt); signal(wrt);)
avant de pouvoir s’enregistrer : si un rédacteur est en train de modifier writecount
à cet instant précis, le lecteur est momentanément retardé, ce qui suffit à donner
la priorité structurelle aux rédacteurs dans l’algorithme classique enseigné pour ce
problème.

Code C complet

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

/* Variables globales */
int database = 0;
int readcount = 0;
int writecount = 0;

16
sem_t rw_mutex; /* acces exclusif a la base (groupe de lecteurs OU redacteur) */
sem_t mutex; /* protege readcount */
sem_t wrt; /* protege writecount ET sert de porte de priorite redacteurs */

/* ---------- Lecteur ---------- */


void start_read(int id) {
sem_wait(&wrt); /* porte : bloque si un redacteur modifie writecount */
sem_post(&wrt);

sem_wait(&mutex);
readcount++;
if (readcount == 1)
sem_wait(&rw_mutex); /* premier lecteur : verrouille la base */
sem_post(&mutex);

printf("[Lecteur %d] entre en lecture (readcount=%d)\n", id, readcount);


}

void end_read(int id) {


sem_wait(&mutex);
readcount--;
if (readcount == 0)
sem_post(&rw_mutex); /* dernier lecteur : libere la base */
sem_post(&mutex);

printf("[Lecteur %d] quitte la lecture (readcount=%d)\n", id, readcount);


}

/* ---------- Redacteur ---------- */


void start_write(int id) {
sem_wait(&wrt);
writecount++;
if (writecount == 1)
sem_wait(&rw_mutex); /* premier redacteur en attente : verrouille la base */
sem_post(&wrt);

printf("[Redacteur %d] commence l'ecriture\n", id);


}

void end_write(int id) {


sem_wait(&wrt);
writecount--;
if (writecount == 0)
sem_post(&rw_mutex); /* plus aucun redacteur en attente/actif */
sem_post(&wrt);

printf("[Redacteur %d] termine l'ecriture\n", id);


}

17
/* ---------- Threads ---------- */
void* reader(void* arg) {
int id = *(int*)arg;
start_read(id);
usleep(200000); /* simulation du temps de lecture */
printf("[Lecteur %d] lit database = %d\n", id, database);
end_read(id);
return NULL;
}

void* writer(void* arg) {


int id = *(int*)arg;
start_write(id);
database++; /* modification de la base */
printf("[Redacteur %d] ecrit database = %d\n", id, database);
usleep(300000); /* simulation du temps d'ecriture */
end_write(id);
return NULL;
}

int main() {
pthread_t readers[3], writers[2];
int rid[3] = {1, 2, 3};
int wid[2] = {1, 2};

sem_init(&rw_mutex, 0, 1);
sem_init(&mutex, 0, 1);
sem_init(&wrt, 0, 1);

for (int i = 0; i < 3; i++)


pthread_create(&readers[i], NULL, reader, &rid[i]);
for (int i = 0; i < 2; i++)
pthread_create(&writers[i], NULL, writer, &wid[i]);

for (int i = 0; i < 3; i++)


pthread_join(readers[i], NULL);
for (int i = 0; i < 2; i++)
pthread_join(writers[i], NULL);

sem_destroy(&rw_mutex);
sem_destroy(&mutex);
sem_destroy(&wrt);

return 0;
}

Compilation :

18
gcc -o lecteurs_redacteurs lr.c -lpthread

Analyse de la solution

Sémaphore Rôle Analogie


rw_mutex Verrou d’accès à database Comme dans la version
lecteurs-préférence, mais
aussi utilisé par les
rédacteurs pour se bloquer
entre eux
mutex Protège readcount Section critique classique
pour compteur partagé
wrt Protège writecount et sert C’est l’ajout clé par rapport
de porte de synchronisation à la version
pour retarder les nouveaux lecteurs-préférence
lecteurs

Limite à connaître (pour la culture, souvent demandée en question de cours


complémentaire) : cette solution empêche tout nouveau groupe de lecture de
démarrer dès qu’un rédacteur est en attente, mais elle n’interrompt pas un groupe de
lecteurs déjà en cours de lecture. Ce n’est donc pas une préférence rédacteurs absolue
dans tous les cas extrêmes, mais c’est bien la solution canonique attendue pour ce type
d’exercice de Master 1.
Réponse à donner sur la copie : le code complet ci-dessus,
avec au minimum les trois sémaphores rw_mutex, mutex, wrt (tous
init. à 1), les compteurs readcount/writecount, et les 4 fonctions
start_read()/end_read()/start_write()/end_write() respectant : le lecteur
passe par la porte wrt avant de s’enregistrer sous mutex ; le rédacteur incrémente
writecount sous wrt et verrouille rw_mutex dès le premier rédacteur en attente.
Programme principal créant 3 threads lecteurs et 2 threads rédacteurs sur la
variable globale database.

19
Récapitulatif des points clés à retenir
1. Round Robin : toujours vérifier la cohérence (somme des tranches = somme
des durées) et respecter l’ordre d’arrivée des nouveaux processus par rapport au
processus préempté dans la file.
2. Signaux : bien distinguer émission (signal marqué en attente dans le PCB) de
délivrance (exécution effective du handler, qui n’a lieu qu’au prochain passage en
mode utilisateur).
3. Tubes : toujours fermer l’extrémité non utilisée dans chaque processus, sous peine
de blocage par absence de EOF.
4. Producteurs-consommateurs : patron wait(empty/full) puis wait(mutex)
… signal(mutex) puis signal(full/empty) — jamais l’inverse, pour éviter les
interblocages.
5. Lecteurs-rédacteurs : la différence entre préférence lecteurs et préférence
rédacteurs se joue entièrement sur le moment où l’on bloque l’arrivée de
nouveaux lecteurs (jamais vs. dès qu’un rédacteur attend).

20

Vous aimerez peut-être aussi