Add synchronisation section
This commit is contained in:
parent
c0166072c1
commit
9c58721451
1 changed files with 564 additions and 0 deletions
564
content/systemes_exploitation/3-synchronisation/index.md
Normal file
564
content/systemes_exploitation/3-synchronisation/index.md
Normal file
|
@ -0,0 +1,564 @@
|
|||
---
|
||||
title: "Systèmes d'exploitation : Les outils de synchronisation"
|
||||
date: 2021-09-24
|
||||
tags: ["système", "sémaphore", "synchronisation", "thread"]
|
||||
categories: ["Systèmes d'exploitation", "Cours"]
|
||||
---
|
||||
|
||||
Dans ce chapitre, nous allons étudier différents problèmes (et solutions)
|
||||
relatives à la synchronisation :
|
||||
|
||||
* l'exclusion mutuelle des sections critiques
|
||||
* le problème des lecteurs - écrivains
|
||||
* le problème des producteurs consommateurs
|
||||
* le problème des points de rendez-vous
|
||||
|
||||
## L'exclusion mutuelle
|
||||
|
||||
Les processeurs savent juste faire des opérations de lecture / écriture de façon
|
||||
atomique. Mais dans le cadre de l'incrémentation d'une variable comme nous
|
||||
l'avions vu dans le chapitre traitant des processus légers
|
||||
[en lpro]({{< ref"../../progsys/3-processus/index.md">}} "Les thread") ce n'est
|
||||
pas suffisant. Il nous faut donc utiliser **l'exclusion mutuelle**.
|
||||
|
||||
L'algorithme de [Peterson][l_peterson] datant de 1981 utilise l'attente active
|
||||
pour arriver à ses fins. Un problème se pose alors: le temps processeur est
|
||||
gaspillé! De plus, cet algorithme est valable pour deux threads seulement.
|
||||
|
||||
```c
|
||||
//Gary L. Peterson, 1981. Works with two processes : #0 and #1
|
||||
bool flag [2] = { FALSE, FALSE };
|
||||
unsigned turn = 0;
|
||||
void enter_sc ()
|
||||
{
|
||||
flag[me] = TRUE;
|
||||
turn = me;
|
||||
wait (flag[1 – me] == FALSE or turn != me);
|
||||
}
|
||||
void exit_sc ()
|
||||
{
|
||||
flag[me] = FALSE;
|
||||
}
|
||||
```
|
||||
|
||||
Nous avons donc besoin d'une solution non seulement valable pour un nombre
|
||||
indéfinis de fils d'exécutions, mais aussi plus efficace.
|
||||
|
||||
## Une solution matérielle
|
||||
|
||||
Vu du processeur, une opération atomique ne peut être interompue par une autre.
|
||||
Les processeurs modernes en comportent un certain nombres. La solution viens
|
||||
donc des fabricant de processeurs.
|
||||
|
||||
Pour forcer l'exclusion mutuelle, nous en avons besoin d'une seule:
|
||||
`Test_and_Set`. C'est donc une instruction matérielle, voici une pseudo
|
||||
implémentation en C:
|
||||
|
||||
```c
|
||||
int test_and_set (int *verrou){
|
||||
// -- Début de la partie atomique --
|
||||
// Ceci est du pseudocode, montré ici juste pour illustrer le principe.
|
||||
// S'il était utilisé normalement dans un compilateur,
|
||||
// les garanties nécessaires d'atomicité, non-usage de cache,
|
||||
// non-réorganisation par l'optimiseur etc.
|
||||
// ne seraient pas fournies.
|
||||
int old = *verrou;
|
||||
|
||||
// positionne le verrou à 1 pour dire qu'on veut occuper la SC (nb: ne
|
||||
// change pas sa valeur si *verrou vaut déjà 1 car un autre processus
|
||||
// est en SC)
|
||||
*verrou = 1;
|
||||
// -- Fin de la partie atomique --
|
||||
|
||||
// renvoie l'ancienne valeur (c'est-à-dire 1 (vrai) si et seulement si il
|
||||
// y avait déjà un processus en SC à l'entrée dans test_and_set)
|
||||
return old;
|
||||
}
|
||||
```
|
||||
Ce code source est issu de la [page Wikipedia][l_testandset] sur Test and Set,
|
||||
voyons maintenant cpmment l'utiliser:
|
||||
|
||||
```c
|
||||
int lock = 0;
|
||||
void enter_sc ()
|
||||
{
|
||||
while (test_and_set(&lock) == 1)
|
||||
while (lock)
|
||||
/* wait */ ;
|
||||
}
|
||||
void exit_sc ()
|
||||
{
|
||||
lock = 0;
|
||||
}
|
||||
```
|
||||
|
||||
Le problème d'accès à la section critique est réglé. Nous avons tout de même
|
||||
toujours un problème d'attente active... Car le noyau ne peut savoir qui a poser
|
||||
le verrou. Les threads attendant la libération de la section critique **seront
|
||||
toujours "promus"** sur le processeurs et consomerons du temps. Il faut donc
|
||||
trouver une solutions pour **endormir les threads attendant d'entrée en section
|
||||
critique**.
|
||||
|
||||
La solution est donc un appel système permettant d'endormir (et réveiller) les
|
||||
threads en attente. Car seul le noyau peut endormir les processus.
|
||||
|
||||
## Les sémaphores
|
||||
|
||||
On en a déjà parlé [en lpro]({{< ref"../../progsys/8_IPC">}} "Les IPC"). Ce sont
|
||||
des outils de haut-niveau, implémentés dans le noyau. Il se compose d'une
|
||||
structures contenant un entier positif et une liste de processus en attente et
|
||||
de deux méthodes `P()` et `V()`. Il ont été inventés en 1962 par E. Dijsktra.
|
||||
|
||||
Les méthodes permettent :
|
||||
* `P()`: attendre un jeton, le prendre lorqu'il devient disponible et continuer
|
||||
son exécution.
|
||||
* `V()`: remettre un jeton
|
||||
|
||||
Lorqu'un thread appelle `V()`, le sémaphore va reveiller les threads référencés
|
||||
dans la liste d'attente.
|
||||
|
||||
Comme seul le noyau peut arrêter et reveiller des processus, les sémaphores sont
|
||||
donc implémentés sur le noyau.
|
||||
|
||||
### Implementer un rendez-vous entre plusieurs processus.
|
||||
|
||||
Il faut faire en sorte que plusieurs processus s'attendent avant de continuer
|
||||
leurs exécutions.
|
||||
|
||||
Voici le code implementant notre fonction barrière. Celle ci permet à plusieurs
|
||||
processus de se donner un rendez-vous. On utilise des **sémaphore** pour
|
||||
implementer une sorte de mutex.
|
||||
|
||||
```c {linenos=table}
|
||||
// point 1
|
||||
Semaphore wait[2](0), mutex[2](1);
|
||||
int count[2] = { 0, 0 };
|
||||
void barrier (int i)
|
||||
{
|
||||
// point 2
|
||||
P(mutex[i]);
|
||||
count[i]++;
|
||||
|
||||
if (count[i] < N) {
|
||||
V(mutex[i]);
|
||||
P(wait[i]);
|
||||
} else {
|
||||
count[i] = 0;
|
||||
V(mutex[i]);
|
||||
|
||||
// point
|
||||
for (int k=0; k < N-1; k++)
|
||||
V(wait[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Explication de code :
|
||||
* **point 1**: Utiliser des tableaux de 2 éléments permet de réutiliser notre
|
||||
fonction `barrier()` pour fixer plusieurs rendez-vous. Il suffit alors
|
||||
d'apeller à tous de rôle `barrier(0);` et `barrier(1);`. Sans ça un
|
||||
processus un peu trop rapide pourrait prendre le sémaphore de la seconde
|
||||
barrière et bloquer alors pa première.
|
||||
* **point 2**: l'incrementation de notre compteur doit se faire dans un
|
||||
*"mutex"* afin d'éviter que plusieurs processus y accèdent en même temps.
|
||||
On relache notre sémaphore *"mutex"* ligne 11.
|
||||
|
||||
Dans la vraie vie, on n'utilise pas de barrière, on utilisera plutôt `signal()`
|
||||
et `wait()`. Mais nous verrons ça plus tard.
|
||||
|
||||
### Le problème des producteurs / consommateurs avec des sémaphore
|
||||
|
||||
Le principe ici est d'implémenter une une structure de type FIFO partagée entre
|
||||
plusiers processus. Cette structure a une capacité de `MAX` élements. Nous
|
||||
allons utiliser deux sémaphores: un pour le producteur et un pour le
|
||||
consommateurs.
|
||||
|
||||
Un producteur appelle la fonction `put(element)` et un consommateurs
|
||||
`element get()`.
|
||||
|
||||
```c
|
||||
#define MAX 8
|
||||
semaphore cons(0), prod(MAX)
|
||||
|
||||
// Pour le consommateur
|
||||
P(cons);
|
||||
elemet = get();
|
||||
V(prod);
|
||||
|
||||
// Pour le producteur
|
||||
P(prod);
|
||||
put(element);
|
||||
V(cons);
|
||||
```
|
||||
|
||||
Le fonctionnement est ici assez simple :
|
||||
|
||||
* **Pour le consomateur**:
|
||||
1. il prend un jeton sur le sémaphore `cons` s'il est disponible (ou attends
|
||||
qu'il le soit)
|
||||
2. consomme l'élement
|
||||
3. on relache celui sur `prod`. Cette dernière action permet de relancer
|
||||
la production si la file était pleine (`prod` en attente).
|
||||
* **pour le producteur**:
|
||||
1. on décrémente la sur la production (`prod`). Cette
|
||||
action permet d'arrêter la production s'il n'y a plus de jeton
|
||||
disponible.
|
||||
2. On produit ensuite l'élément
|
||||
3. puis on relache le sémaphore `cons`. On réveille ansi notre consommateur
|
||||
s'il dormait en attendant la disponibilité d'un élement
|
||||
|
||||
#### Multiple producteurs et consomateur
|
||||
|
||||
Dans la vraie vie, il y souvent plusieurs consomateurs / producteurs. Le
|
||||
problème devient alors plus complexe...
|
||||
|
||||
Le code ressemble à celui ci-dessus, saut qu'il faut faire attention à ce qu'il
|
||||
y ai un seul consmateir à la fois sur `get()` et un seul producteur sur
|
||||
`put(element)`. nous allons donc rajouter deux *"mutex"* :
|
||||
|
||||
```c {linenos=table,hl_lines=[6,8,13,15]}
|
||||
#define MAX 8
|
||||
semaphore cons(0), prod(MAX), mutex_c(1), mutex_p(1);
|
||||
|
||||
// Pour le consomateir
|
||||
P(cons)
|
||||
P(mutex_v);
|
||||
element = get();
|
||||
V(mutex_v);
|
||||
V(prod)
|
||||
|
||||
// Pour le producteur
|
||||
P(prod);
|
||||
P(mutex_c);
|
||||
put(element);
|
||||
V(mutex_p);
|
||||
V(cons)
|
||||
```
|
||||
|
||||
### Problème des lecteurs rédacteurs
|
||||
|
||||
Ce problème ressemble à celui des producteurs / consommateurs.
|
||||
|
||||
* *lecteur*: processus qui lit des données sans les modifier. Il n'y a pas de
|
||||
problème d'exclusion mutuelle : plusieurs lecteur peuvent lire la même
|
||||
donnée.
|
||||
* *rédacteur*: processus qui modifie des données. Exclusion avec non seulement
|
||||
les autres rédacteurs mais aussi les lecteurs.
|
||||
|
||||
Voici le code pour les lecteurs:
|
||||
|
||||
```c {linenos=table,hl_lines=[8,12]}}
|
||||
Semaphore write_token(1);
|
||||
int n_reader = 0;
|
||||
Semaphore mutex_r(1);
|
||||
//Waiting Room help us to make our implememtatoion Fair
|
||||
Semaphore wait_room(1);
|
||||
|
||||
// Reader
|
||||
P(wait_room);
|
||||
P(mutex_r);
|
||||
// Pathfinder
|
||||
// Equivalent to nbr++; if nbr == 1
|
||||
if (++nbr == 1)
|
||||
{
|
||||
P(write_token);
|
||||
}
|
||||
V(mutex_r);
|
||||
V(wait_room);
|
||||
|
||||
read();
|
||||
|
||||
P(mutex_r)
|
||||
if(--nbr == 0) // last to leave
|
||||
V(write_token);
|
||||
V(mutex_r);
|
||||
```
|
||||
Pour les lecteur, il faut envoyer **un éclaireur** afin de savoir si un lecteur
|
||||
est actif. Le problème est similaire à celui des producteur consommateurs sauf
|
||||
qu'il faut savoir si notre lecteur est le premier *(ligne 12)*
|
||||
|
||||
De plus. afin que notre algorithme ne privilegie pas les lecteurs au
|
||||
détriment des rédacteur; ils peuvent même ne jamais rendre la main aux
|
||||
rédacteurs. Pour pallier au problème, nous devons mettre en place une **salle
|
||||
d'attente** *(ligne 8)*.
|
||||
|
||||
```c {linenos=table,hl_lines=[8,12]}
|
||||
P(wait_room);
|
||||
P(write_token);
|
||||
V(wait_room);
|
||||
write();
|
||||
V(write_token);
|
||||
```
|
||||
|
||||
Ce code est plutôt clair et ne comporte rien de particulier.
|
||||
|
||||
## Les moniteurs d'Hoare
|
||||
|
||||
Les moniteurs sont des primitives de synchronisation initialement proposées dans
|
||||
les langages objet. Il est utilisé actuellemt dans des langages tel que ADA ou
|
||||
Java et implemente au sein de systèmes d'exploitation.
|
||||
|
||||
Le moniteur se positionne sur une classe et les mutexes sur ses methodes et sont
|
||||
basés sur des variables condition. elle sont forcement privée et inaccessible à
|
||||
l'exterieur de la classe.
|
||||
|
||||
```java
|
||||
Monitor class m {
|
||||
private int i = 1;
|
||||
private condition c;
|
||||
public method f() { // cette accolade signifie P(mutex)
|
||||
code_before();
|
||||
wait(c);
|
||||
code_after();
|
||||
} // et celle-ci V(mutex)
|
||||
|
||||
public method g() {
|
||||
signal(c);
|
||||
}
|
||||
```
|
||||
|
||||
Il sont différent des *sémaphores*: il n'y a pas de jeton à prendre. Leur
|
||||
implémentation dans les systèmes d'exploitation est différente, elle se fait par
|
||||
les les types `mutex_t` et `cond_t`:
|
||||
|
||||
```c
|
||||
mutex_t m;
|
||||
mutex_lock(mutex_t *m);
|
||||
mutex_unlock(mutex_t *m);
|
||||
|
||||
cond_t c;
|
||||
cond_wait(cond_t *c, mutex_t *m);
|
||||
cond_signal(cond_t *c);
|
||||
cond_bcast(cond_t *c);
|
||||
```
|
||||
|
||||
Et voici un exemple d'utilisation:
|
||||
|
||||
```c
|
||||
mutex_t m;
|
||||
cond_t c;
|
||||
|
||||
void c(){
|
||||
mutex_lock(&m)
|
||||
if (...){
|
||||
cond_wait(&c, &m);
|
||||
}
|
||||
mutex_unlock(&m);
|
||||
}
|
||||
|
||||
void g(){
|
||||
mutex_lock(&m);
|
||||
...
|
||||
cond_signal(&c);
|
||||
...
|
||||
mutex_unlock(&m);
|
||||
}
|
||||
```
|
||||
|
||||
### bonne pratiques
|
||||
|
||||
* Pas de variable condition à l'extérieur d'un moniteur, c'est aussi conseillé
|
||||
pour les `cont_signal` et `cond_bcast`.
|
||||
* Les mutex ont un propriétaire.
|
||||
|
||||
### Retour sur le problème du rendez-vous
|
||||
|
||||
L'idée est de bloquer les N-1 processus, le dernier (N) reveillera tous les
|
||||
autres.
|
||||
|
||||
```c
|
||||
mutex_t m;
|
||||
cond_t wait;
|
||||
int nb = 0;
|
||||
|
||||
void barrier ()
|
||||
{
|
||||
// notre variable condition et bien "protégée" par un mutex
|
||||
mutex_lock(&m);
|
||||
nb++;
|
||||
|
||||
// on stoppe les n-1 processus
|
||||
If (nb < N)
|
||||
cond_wait (&wait, &m);
|
||||
else {
|
||||
// le dernier réveille tous les autrespar un Bcast
|
||||
cond_bcast (&wait);
|
||||
// et on positionne nb à 0 permettant ainsi de réutiliser cette fonction
|
||||
// barrière.
|
||||
nb = 0;
|
||||
}
|
||||
mutex_unlock(&m);
|
||||
}
|
||||
```
|
||||
|
||||
En plus d'être plus simple, cette fonction est réutilisable de multiples fois
|
||||
sans le petit *"hack* des tableaux utilisé pour les sémaphore.
|
||||
|
||||
### Producteur / consomateurs avec un moniteur
|
||||
|
||||
tout comme les barrières, le code est ici bien plus simple. Et bien entendu il
|
||||
n'y a pas d'attente active...
|
||||
|
||||
Dans les codes ci-dessous, il est préféfable d'utiliser `while (nbe ...)` plutôt
|
||||
qu'un `if (nbe ...)`. (Mais je ne sais plus pourquoi...)
|
||||
|
||||
Voici les variables nécessaires aux producteurs et au consomate
|
||||
|
||||
```c
|
||||
#define MAX 8
|
||||
mutex_t m;
|
||||
cond_t cons, prod;
|
||||
int nbe = 0;
|
||||
```
|
||||
|
||||
#### pour le producteur
|
||||
|
||||
```c
|
||||
mutex_lock (&m);
|
||||
|
||||
/* si notre file est pleine, alors le thread passe en sommeil
|
||||
et attend le reveil par un cond_signal */
|
||||
while (nbe == MAX) {
|
||||
cond_wait(&prod);
|
||||
}
|
||||
put(element);
|
||||
nbe++;
|
||||
cond_signal(&cons)
|
||||
mutex_unlock (&m);
|
||||
```
|
||||
|
||||
#### pour les consomateurs
|
||||
|
||||
```c
|
||||
mutex_lock (&m);
|
||||
|
||||
/* Et inversement si notre file est vide, rien à consommer, on
|
||||
attend un reveil par un cond_signal dès qu'il y a production */
|
||||
while (nbe == 0) {
|
||||
cond_wait(&cons)
|
||||
}
|
||||
element = get();
|
||||
nbe--;
|
||||
cond_signal(&prod);
|
||||
mutex_unlock (&m);
|
||||
```
|
||||
|
||||
### Les lecteurs / rédateurs
|
||||
|
||||
Nous commençons ici par créer une structure et l'affecter à une variable
|
||||
`my_lock`:
|
||||
|
||||
```c
|
||||
typedef struct{
|
||||
mutex_t m;
|
||||
cond_t c_read, c_write;
|
||||
unsigned nb_reader = 0;
|
||||
unsigned nb_writer = 0;
|
||||
} rwlock_t;
|
||||
|
||||
rwlock_t mylock;
|
||||
```
|
||||
|
||||
#### Du côté des lecteur
|
||||
|
||||
```c
|
||||
void rwl_readlock(rwlock_t *l)
|
||||
{
|
||||
mutex_lock(&l->m);
|
||||
|
||||
/* tout comme le problème des produteurs / consomateurs, un
|
||||
while est préférable ici */
|
||||
while(l->nbw > 0) {
|
||||
cond_wait(&l->cr, &l->m);
|
||||
}
|
||||
l->nbr++;
|
||||
mutex_unlock(&l->m);
|
||||
}
|
||||
|
||||
void rwl_readunlock(rwlock_t *l)
|
||||
{
|
||||
mutex_lock(&l->m);
|
||||
l->nbr--;
|
||||
if(l->nbr == 0) {
|
||||
cond_signal(&l->cw);
|
||||
}
|
||||
mutex_unlock(&l->m);
|
||||
}
|
||||
|
||||
Et voici son l'utilisation :
|
||||
|
||||
```c
|
||||
rwl_readlock(mylock);
|
||||
read();
|
||||
rwl_readunlock(mylock);
|
||||
```
|
||||
|
||||
#### du côté des rédacteurs
|
||||
|
||||
```c
|
||||
void rwl_writelock(rwlock_t *l)
|
||||
{
|
||||
mutex_lock (l->m);
|
||||
/* Comme ces deux nombres sont des entiers strictement positifs
|
||||
on peut utiliser l'adition afin d'attendre en cas de présence
|
||||
de lecteurs ou de rédacteur.
|
||||
On évite ainsi une double condition :
|
||||
while ( l->nb_reader > 0 || l->nb_writer > 0) */
|
||||
|
||||
while( l->nb_reader + l->nb_writer > 0 ) {
|
||||
cond_wait (&l->cw, &l->m);
|
||||
}
|
||||
l->nbw++;
|
||||
mutex_unlock (&l->m);
|
||||
}
|
||||
|
||||
void rwl_writeunlock(rwlock_t *l)
|
||||
{
|
||||
mutex_lock (l->m);
|
||||
l->nbw--;
|
||||
cond_signal (&l->cw);
|
||||
rwl_writeunlock (&cool_lock);
|
||||
|
||||
/* On utilise le bcast pour les lecteur car on peut *tous*
|
||||
les réveiller (+ieurs lecteurs possibles en même temps */
|
||||
cond_bcast(&l->c_read);
|
||||
/* mais on ne reveille qu'un seul écrivain */
|
||||
cond_signal(&l->c_write);
|
||||
mutex_unlock (&l->m);
|
||||
}
|
||||
```
|
||||
|
||||
Et voici son utilisation:
|
||||
|
||||
```c
|
||||
rwl_writelock(mylock);
|
||||
write();
|
||||
rwl_writeunlock(mylock);
|
||||
```
|
||||
|
||||
## conclusion
|
||||
|
||||
Les sémaphores et moniyeurs sont implémentés au niveau du système d'exploitation
|
||||
et utilient le matériel sous-jacent (`test_and_set`). Ces appels sont
|
||||
**coûteux**, nécessitent des **changements de contexte**. Dans les systèmes
|
||||
modernes, on leur préfèrera un **mix de primitive de synchronisation et de
|
||||
polling (attente de variable)**. Ainsi un thread attendra une valeur / variable
|
||||
pendant quelques cycles et se bloquera si elle n'est pas disponible.
|
||||
|
||||
Les moniteurs de Hoare sont en général préférés par les programmeurs cas comme
|
||||
on l'a vu, ils sont plus simple à implémenter.
|
||||
|
||||
Il eest intéressant aussi de parler des **FUTEX**, apparus en 2003 sur Linux et
|
||||
plus tard sous Windows 8 (sous l'appelation `wait_on_address` et brevete en
|
||||
2013).
|
||||
|
||||
Ils utilisent des opétations atomiques sur des variables entières de 32bits *en
|
||||
espace utilisateur* et deux operation bloquante si nécessaires:
|
||||
|
||||
```c
|
||||
sys_futex(&var, FUTEX_WAIT);
|
||||
sys_futex (&var, FUTEX_WAKE);
|
||||
```
|
||||
|
||||
[l_peterson]:https://fr.wikipedia.org/wiki/Algorithme_de_Peterson
|
||||
[l_testandset]:https://fr.wikipedia.org/wiki/Test-and-set
|
Loading…
Add table
Add a link
Reference in a new issue