Synchronisation§
Systèmes d’exploitation
auteur: | Pierre-Antoine Champin |
---|---|
adresse: | Département Informatique - IUT - Lyon1 |
licence: | ![]() |
Systèmes d’exploitation
auteur: | Pierre-Antoine Champin |
---|---|
adresse: | Département Informatique - IUT - Lyon1 |
licence: | ![]() |
Lorsque deux tâches (processus ou threads) accèdent à une même zone mémoire, elles peuvent interagir de manière indésirable,
Une section de programme est dite atomique lorsqu’elle ne peut pas être interrompue par un autre processus manipulant les mêmes ressources critiques.
→ c’est donc une atomicité relative à la ressource
Un mécanisme d”exclusion mutuelle sert à assurer l’atomicité des sections critiques relatives à une ressource critique.
→ en anglais : mutual exclusion, ou mutex
int compte; /* commun */
void entrer_SC_compte();
void sortir_SC_compte();
printf("ajout de 1000€\n"); /* tâche 1 */
entrer_SC_compte();
compte += 1000;
sortir_SC_compte();
printf("ajout effectué\n");
printf("retrait de 500€\n"); /* tâche 2 */
entrer_SC_compte();
compte -= 500;
sortir_SC_compte();
printf("retrait effectué\n");
On utilise un booléen partagé comme « verrou »
int verrou = 0;
void entrer_SC() { while (verrou) {}
verrou = 1; }
void sortir_SC() { verrou = 0; }
Le processus qui attent consomme du temps processeur (d’où le nom d’attente active).
→ NB: on peut améliorer cela en mettant un sleep
ou un yield
dans la boucle.
Cette approche n’assure par l’exclusion!
→ verrou est lui même une ressource critique.
int compte; /* commun */
sem_t mutex_compte;
status = sem_init(&mutex_compte, 1, 1);
printf("ajout de 1000€\n"); /* tâche 1 */
status = sem_wait(&mutex_compte);
compte += 1000;
status = sem_post(&mutex_compte);
printf("ajout effectué\n");
printf("retrait de 500€\n"); /* tâche 2 */
status = sem_wait(&mutex_compte);
compte -= 500;
status = sem_post(&mutex_compte);
printf("retrait effectué\n");
Certaines opérations sur la ressource critiques ne nécessitent pas forcément d’exclusion mutuelle.
→ exemple : lecture de la valeur du compte (si on accepte que la valeur lue devienne rapidement invalide…)
Le mécanisme d’exclusion mutuelle n’est pas une protection, mais une convention entre les processus souhaitant utiliser la ressource sans la corrompre.
→ il est toujours possible de manipuler la ressource sans « prendre le jeton »
Les sections critiques sont un mal nécessaire:
Conséquence :
/* n'écrivez PAS --------------------------------- */
status = sem_wait(&mutex_compte);
printf("ajout de 1000€\n");
compte += 1000;
printf("ajout effectué\n");
status = sem_post(&mutex_compte);
/* mais écrivez ---------------------------------- */
printf("retrait de 500€\n");
status = sem_wait(&mutex_compte);
compte -= 500;
status = sem_post(&mutex_compte);
printf("retrait effectué\n");
Un inter-blocage (deadlock) est une situation où plusieurs tâches sont bloquées car chacune attend un événement que doit produite une autre.
Un exemple classique se produit lorsque deux tâche attendent chacune un « jeton » détenu par l’autre :
/* tâche 1 */ /* tâche 2 */
sem_wait(&mutex_A); sem_wait(&mutex_B);
utilise(A); utilise(B);
sem_wait(&mutex_B); sem_wait(&mutex_A);
utilise(A, B); utilise(A, B);
/* ... */ /* ... */
Une manière d’éviter les inter-blocages consiste à toujours réclamer les ressources dans le même ordre (quitte à libérer une ressource avant de la réclamer à nouveau).
/* tâche 1 */ /* tâche 2 */
sem_wait(&mutex_A); sem_wait(&mutex_B);
utilise(A); utilise(B);
sem_wait(&mutex_B); sem_post(&mutex_B);
utilise(A, B); sem_wait(&mutex_A);
/* ... */ sem_wait(&mutex_B);
utilise(A, B);
/* ... */
Pas une liste exhaustive, mais une source d’inspiration.
Un ensemble de processus, divisé en deux catégories, partage une zone mémoire.
Les premiers (producteurs) remplissent la mémoire partagée, avec des éléments.
→ la mémoire ne peut contenir qu’un nombre d’éléments limité et connu à l’avance
Les seconds (consommateurs) utilisent ces éléments et les retirent de la mémoire.
Exemple : file d’impression
Si les opérations sur la zone mémoire partagée ne sont pas atomiques, il faut les protéger par une section critique
→ troisième sémaphore
elt_t tab[N]
sem_t libres, occupes, mutex;
sem_init(&libres, 1, N);
sem_init(&occupes, 1, 0);
sem_init(&mutex, 1, 1);
void producteur() {
elt_t e = produire();
sem_wait(&libres);
sem_wait(&mutex);
ajouter(e, tab);
sem_post(&mutex);
sem_post(&occupes);
}
|
void consommateur() {
sem_wait(&occupes);
sem_wait(&mutex);
elt_t e = retirer(tab);
sem_post(&mutex);
sem_post(&libres);
consommer(e);
}
|
libres
et occupes
libres
/occupes
.Un ensemble de processus, divisé en deux catégories, partage une zone mémoire.
Certains processus (les lecteurs) font des accès en lecture seule à cette zone.
D’autres processus (les rédacteurs) modifient le contenu de cette zone.
NB : on parle parfois d’écrivains plutôt que de rédacteurs
Lorsqu’un rédacteur accède à la mémoire partagée, aucune autre processus (qu’il soit lecteur ou rédacteur) ne doit y avoir accès.
→ problème d’exclusion mutuelle classique
En revanche, les lecteurs peuvent être plusieurs à utiliser la zone en même temps, cela ne pose pas de problème.
int c = 0; //
sem_t mutex_m, mutex_c;
sem_init(&mutex_m, 1, 1);
sem_init(&mutex_c, 1, 1);
void redacteur() {
sem_wait(&mutex_m);
ecrire(e, tab);
sem_post(&mutex_m);
}
|
void lecteur() { //
sem_wait(&mutex_c);
if (c == 0)
sem_wait(&mutex_m);
c += 1;
sem_post(&mutex_c);
lire();
sem_wait(&mutex_c);
if (c == 1)
sem_post(&mutex_m);
c -= 1;
sem_post(&mutex_c);
}
|
Le lecteur qui prend le jeton de mutex_m n’est pas forcément celui qui le rend
On note que les lecteurs peuvent parfois se bloquer dans la section critique du compteur c.
→ Cela peut-il créer un inter-blocage ?
L’algorithme proposé fonctionne, mais n’assure pas l’équité : un rédacteur peut attendre indéfiniment avant d’avoir accès à la mémoire (famine)
→ Comment le modifier pour assurer l’équité ?
Illustration : © Benjamin Esham
Problème proposé par Dijkstra.
Cinq philosophes passent leur vie à penser et manger autour d’une table.
Pour manger, ils ont besoin de deux fourchettes, mais il n’y a que cinq fourchettes.
Proposer un algorithme qui évite :
Mauvaise solution :
chaque philosophe, lorsqu’il a faim, prend la fourchette de gauche, puis celle de droite, mange, puis les repose
Chaque philosophe a trois états :
« je pense », « j’ai faim », « je mange »
par lesquels il passe toujours dans cet ordre
Lorsqu’il a faim, un philosophe ne peut manger que si ses deux voisins ne mangent pas, sinon attend
Lorsqu’il termine de manger, le philosophe réveille ses voisins et se remet à penser
sem_t mutex; // init à 1
sem_t reveils[N];// init à 0
int etats[n]; // init à PENSE
void CommenceManger(int id) {
sem_wait(mutex);
etats[id] = FAIM;
bool ok = etat[id-1] != MANGE
&& etat[id+1] != MANGE;
if (ok) {
etat[id] = MANGE;
sem_post(mutex);
} else {
sem_post(mutex);
sem_wait(reveils[id]);
}
}
|
void FinitManger(int id) {
sem_wait(mutex);
etats[id] = PENSE;
/* voisin gauche */
if (etat[id-1] == FAIM
&& etat[id-2] != MANGE) {
etat[id-1] = MANGE;
sem_post(reveils[id-1]);
}
/* voisin droit */
if (etat[id+1] == FAIM
&& etat[id+2] != MANGE) {
etat[id+1] = MANGE;
sem_post(reveils[id+1]);
}
sem_post(mutex);
}
|
Le système d’exploitation lui même peut avoir besoin d’exclusion mutuelle, pour toutes ses structures de données…
… et notamment pour les sémaphores eux-mêmes !
→ on ne peut pas utiliser de sémaphore pour implémenter un sémaphore…
Les procédures sem_wait et sem_post sont des sections critiques pour le sémaphore lui même…
proc sem_wait(sem): . si sem.c = 0 alors bloquer le processus courant finsi sem.c ← sem.c - 1 proc sem_post(sem) sem.c ← sem.c + 1 si des processus sont bloqués en attente du sémaphore alors débloquer un des processus finsi
proc sem_wait(sem): entrer_SC() si sem.c = 0 alors sortir_SC() bloquer le processus courant sinon sem.c ← sem.c - 1 sortir_SC() finsi |
proc sem_post(sem): entrer_SC() sem.c ← sem.c + 1 si des processus sont bloqués en attente du sémaphore alors débloquer un des processus sem.c ← sem.c - 1 finsi sortir_SC() |
On utilise l’instruction test_and_set du langage machine: elle consulte et modifie une variable de manière atomique:
Cela permet d’écrire une attente active qui assure l’exclusion:
void entrer_SC() {
while (test_and_set(&verrou)) {}
}