Vous êtes sur la page 1sur 9

Notes de cours :

Synchronisation des
processus
1. Introduction
Les processus d’un système ne s’exécutent pas tous de manière isolée.
Certains processus ont besoin de coopérer, et nécessitent donc des moyens
de communication et de synchronisation, et d’autres se trouvent en
compétition pour l’accès aux ressources du système, soit à cause de la
nature physique de la ressource (non partageabilité), soit parce que les
opérations sur cette ressource peuvent provoquer des incohérence. Pour
illustrer ces problèmes, prenons comme exemple la sérialisation de travaux
d’impression. Les jobs à imprimer sont stockées dans une file, tel que
chaque job reçoive à son arrivée un numéro d’ordre d’exécution
strictement croissant. Pour administrer cette file, deux variables globales
partagées in et out contiennent respectivement le prochain numéro de
case attribuée, et le prochain numéro de job à imprimer (on a donc
toujours in  out ). Supposons que, à un instant donné, on a in  7 et
out  4 .

4 Job 4
out 4
5 Job 5 Processus A
6 Job 6
in 7
7
Processus B

Si, au même moment, deux processus A et B souhaitent lancer


l’impression d’un job, ils doivent, chacun, exécuter la séquence suivante :

local _ in  in;
palcer _ job(local _ in);
in  local _ in  1;

Si le processus à effectuer cette séquence est interrompu par


l’ordonnanceur entre la première et la troisième instruction, les deux
processus utiliseront le même numéro pour placer leurs jobs, et l’un des
deux jobs sera perdu. Cependant, à la fin, le contenu de la file sera

1
cohérent avec l’état des variables, et le processus d’impression ne pourra
détecter aucune anomalie.

Il est donc nécessaire que le système fournisse un support sûr et efficace


de la communication inter-processus, permettant tant la synchronisation
que l’échange de données.

En fait, tous les problèmes de synchronisation se ramènent au problème de


l’accès concurrent à une variable partagée. La mise au point de
programmes contenant de tels accès est difficile, car ces programmes
peuvent fonctionner pendant de longues périodes de temps et produire des
résultats incohérents de façon imprévisible.

2. Sections critiques
La solution conceptuellement naturelle à la résolution des problèmes
d’accès concurrents consiste à interdire la modification de données
partagées à plus d’un processus à la fois, c'est-à-dire définir un mécanisme
d’exclusion mutuelle sur des portions spécifiques du code, appelées
sections critiques.

On peut formaliser le comportement des sections critiques au moyen des


quatre conditions suivantes :

1. deux processus ne peuvent être simultanément dans la même


section critique ;

2. aucune hypothèse n’est faite sur la vitesse relatives des processus,


ni sur le nombre des processeurs ;

3. aucun processus suspendu en dehors d’une section critique ne peut


bloquer les autres ;

4. aucun processus ne doit attendre trop longtemps avant d’entrer en


section critique.

La première condition est suffisante à elle seule pour éviter les conflits
d’accès. Cependant, elle ne suffit pas pour garantir le bon fonctionnement
du système d’exploitation, en particulier en ce qui concerne la compétition
au niveau accès aux sections critiques.

3. Masquage des interruptions


Le moyen le plus simple pour éviter les accès concurrents est de masquer
les interruptions avant d’entrer dans une section critique, et de les restaurer
à la sortie de celle-ci. Ainsi, le processus en section critique ne pourra pas
être suspendu au profit d’un autre processus, puisque l’inhibition (blocage)
des interruptions empêche l’ordonnanceur de s’exécuter.

Cette approche, qui autorise un processus utilisateur de masquer les


interruptions du système, est inéquitable, car un processus restant

2
longtemps en section critique peut monopoliser le processeur. Cependant,
elle est couramment utilisée par le système d’exploitation lui-même, pour
manipuler de façon sure ses structures internes. Malgré cela il faut
remarquer qu’elle n’est pas efficace pour les systèmes multiprocesseurs,
puisque les processus s’exécutant sur les autres processeurs peuvent
toujours entrer en section critique.

4. Variables de verrouillage
L’inconvénient du masquage des interruptions est son inefficacité :
lorsqu’un processus entre en section critique, tous les autres processus sont
bloqués, même si la section critique ne concerne qu’un seul processus. Il
faut donc pouvoir définir autant de sections critiques indépendantes que
nécessaire.
Pour cela, on déclare une variable par section critique, qui joue le rôle de
verrou. La variable est mise à 1 par le processus entrant dans la section
critique considérée, et est remise à 0 par le processus lorsqu’il quitte la
section critique.
Avant d’entrer en section critique, un processus doit donc tester l’état de la
variable, et boucler en dehors de la section critique si elle a déjà été
positionnée à 1 par un autre processus, selon l’algorithme suivant :
while (verrou==1) ; /*Attente*/
verrou = 1 ; /*Verrouillage*/
section_critique () ;
verrou =0 ; /*Déverrouillage*/
Malheureusement, cette solution présente le même défaut que l’algorithme
de l’exemple de l’impression : un processus peut être interrompu entre sa
sortie de la boucle while et le positionnement du verrou, permettant ainsi
à un autre processus de rentrer lui aussi en section critique.
On peut imaginer d’interdire les interruptions entre la fin du while et le
positionnement du verrou. Cependant, pour être sûr d’être le seul à entrer
en section critique, il faut inhiber (bloquer) les interruptions avant le
while, ce qui conduit à un blocage puisque l’ordonnanceur ne peut plus
donner la main au processus actuellement en section critique pour qu’il
sorte de celle-ci.
Une solution possible consiste à effectuer le while avec les interruptions
activées.
loop : /*Etiquette de bouce externe*/
while (verrou==1) ; /*Attente hors inhibition*/
s=splx(0) ; /*Inhibition des interruptions*/
if (verrou==1)
{
splx(s) ; /*Restauration des interruptions*/
goto loop ; /*boucle hors inhibition*/
}
verrou=1 ; /*Verrouillage*/
splx(s) ; /*Restauration des interruptions*/
section_critique();
verrou=0; /*Déverrouillage*/

Cette méthode est valable et efficace, mais uniquement dans le cas des
machines mono-processeurs, puisque sur les machines multi-processeurs

3
les processus qui s’exécutent sur les autres processeurs peuvent encore
continuer à entrer en section critique.

5. Les sémaphores
Le concept de sémaphore permet une solution élégante à la plupart des
problèmes d’exclusion.
Le principe est directement hérité des chemins de fer : lorsque le
sémaphore est levé un processus p peut continuer son chemin, lorsqu’il est
baissé il doit attendre jusqu’à ce qu’un autre processus q le lève. p et q
sont l’équivalent de deux trains circulant sur deux vois distinctes qui
doivent se synchroniser pour pouvoir franchir un croisement sans
accidents.
Un sémaphore est donc une variable qui contrôle l’accès à une ressource
partagée et indique le nombre d’éléments de ressource qui sont disponibles
et maintient une liste de processus bloqués en attente de cette ressource.
Quand un sémaphore ne peut pas prendre des valeurs plus grandes que 1
on parlera de sémaphore binaire.
Deux opérations système permettent de modifier l’état (valeur) d’un
sémaphore s :
 down(s) (wait (s)) : décrémente la valeur du sémaphore lorsque
celle-ci est supérieur à 0, ou endort le processus appelant si elle
était déjà à 0.
 up(s) (signal(s)) : incrémente la valeur du sémaphore si aucun
processus n’est endormi du fait d’un appel down, sinon réveille
l’un de ces processus (choisi au hasard).

6. Problèmes classiques de synchronisation


6.1. Le problème du producteur/consommateur

Le producteur et le consommateur sont deux processus cycliques qui


manipulent un tampon. Celui-ci consiste en une zone mémoire organisée
comme un ensemble de cases pouvant accueillir plusieurs objets à raison
de un par case.
Producteur Consommateur
… …
produire(objet) retirer(objet)
déposer(case, objet) consommer(objet)
… …
La solution à une case étant triviale, nous détaillons la solution à N cases.
La gestion du tampon implique que :
 si le tampon est vide, le consommateur ne peut rien retirer
 si le tampon est plein, le producteur ne peut rien déposer
 il faut empêcher que le dépôt et le retrait se chevauchent.
Sachant qu’il faut bloquer les processus tantôt sur un tampon vide, tantôt
sur un tampon plein, ce problème nécessite deux sémaphores n-aires. On
utilise ainsi, pleins et vides initialisés à 0 et n. pleins indique le nombre de
cases pleines et vides celui des cases vides.

4
Avant d’accéder au tampon pour y déposer ou retirer un objet, il faut
également disposer d’un accès exclusif. Un troisième sémaphore exmut est
utilisé pour l’accès au tampon. Les indices tête et queue sont manipulés
respectivement lors du dépôt et du retrait d’objets. Ils sont protégés grâce
au sémaphore d’exclusion mutuelle exmut et ne sont pas traités séparément
pour des raisons de simplification.
Le programme suivant écrit en pseudo-code, présente une solution à ce
problème. Les appels à P et V sont remplacés par les appels système Down
et Up.

#define N 20
Struct semaphore exmut;
Struct semaphore vides;
Struct semaphore pleins;
void producteur(void)
{
int objet;
while(true)
{
produire_objet(&objet) ;
down(&vides) ; //décrémenter le nombre des
//cases vides
down(&exmut) ; //pour ne pas accéder au
//tampon en même temps
deposer_objet(objet) ;
up(&exmut) ; //Libérer l’accès au tampon
up(&pleins) ; //incrémenter le nombre de
//cases pleines
}
}
void consommateur(void)
{
int objet;
while(true)
{
down(&pleins) ; //décrémenter le nombre des
//cases pleines
down(&exmut) ; //pour ne pas accéder au
//tampon en même temps
retirer_objet(&objet) ;
up(&exmut) ; //libérer le sémaphore
up(&vides) ; //incrémenter le nombre de
//cases vides

consommer_objet(objet) ;
}
}

L’ordre des P(s) dans chacun des programmes doit être réalisé avec
prudence. Prenons le cas du producteur, il peut se bloquer avec une
mémoire tampon pleine. L’instruction down(&vides) sera bloquante
puisque vides vaut 0. Si le processus prend le sémaphore exmut avant de se
bloquer, le consommateur ne pourra pas accéder au tampon et le nombre
de vides ne pourra jamais être incrémenté.

5
Les sémaphores vides et pleins assurent que le producteur s’arrête quand la
zone tampon est pleine et que le consommateur s’arrête quand elle est
vide : ce n’est pas de l’exécution mutuelle. Celle-ci est réalisée avec le
sémaphore exmut.

Une dernière remarque pour expliquer pourquoi l’objet est produit et


consommé en dehors de la section critique. Durant toute la période où un
sémaphore est pris, on pénalise les autres processus, il vaut mieux écourter
la section critique en retirant les instructions qui ne posent pas de problème
d’accès concurrent.

6.2. Le problème des philosophes

Le problème dit "des philosophes" [dijkstra, 1965] est un problème


classique d’accès concurrent qui a servi de référence pour évaluer
l’efficacité des différents modèles de primitives de synchronisation
proposées au fil du temps.
Cinq philosophes sont au restaurant chinois assis à une table ronde, et ont
chacun devant eux une assiette pleine de riz. Or, pour manger le riz il faut
deux baguettes. Une baguette est disposée entre deux assiettes
consécutives.
Un philosophe passe son temps à penser et à manger. Lorsqu’un
philosophe a faim, il tente de prendre des deux baguettes qui sont
immédiatement à sa gauche et à sa droite. S’il obtient les deux baguettes, il
mange pendant un certain temps, puis repose les baguettes et se met à
penser. Le problème consiste à fournir un algorithme permettant à chaque
philosophe de se livrer à ses activités sans jamais être bloqué de façon
définitive.

Table

Assiette

Baguette

#define N 5 /*Nombre des philosophes*/


philosophe(i) /*Numéro de 0 à (N-1)*/
{
while(1)
{
penser();
prendre_baguette(i);/*Bloqué tant que pas obtenue*/
prendre_baguette((i+1)%N); /*Bloqué pareillement*/
manger();
poser_baguette(i);
poser_baguette((i+1)%N);

6
}
}
Ce programme peut conduire à des interblocages si tous les philosophes
décident de manger en même temps. On peut modifier le programme
précédent en libérant la baguette gauche si la droite est prise, mais alors il
peut arriver que les philosophes ne mangent jamais s’ils sont synchronisés
(on parle de famine ou "starvation"). On peut rendre le programme
statiquement correct en introduisant des délais aléatoires, mais une
solution systématique et déterministe est largement préférable si elle
existe.
Une solution déterministe correcte et efficace est la suivante :

/*Algorithme pour le problème des cinq philosophes*/


#define N 5 /*Nombre des philosophes */
#define DROITE ((i+N-1)%N) /*Numéro du voisin gauche du philosophe i */
#define GAUCHE (i+1)%N /*Numéro du voisin droit du philosophe i */

typedef enum
{
PENSE,
A_FAIM,
MANGE
} Philosophe;

struct semaphore mutex; /*Exclusion mutuelle */

struct semaphore s[N]; /*Un sémaphore par philosophe initialisé à 0 */

Philosophe p[N]; /*Etat des philosophes */

philosophe(i) /*Numéro de 0 à (N-1) */


{
while(1) /*Boucle infinie*/
{
penser();
prendre_baguettes(i); /*Prendre les deux baguettes ou bloqué */
manger();
poser_baguettes(i);
}
}

prendre_baguette(i)
{
down(&mutex); /*Début de la section critique */
p[i]=A_FAIM; /*Demande les baguettes */
test(i); /*Essaie de les prendre les 2 baguettes */
up(&mutex); /*Fin de la section critique */
down(&s[i]); /*S'endort si pas prise */
}

poser_baguette(i)
{
down(&mutex); /*Section critique */
p[i]=PENSE; /*Ne veut plus manger */
test(GAUCHE); /*propose au philosophe de gauche */

7
test(DROITE); /*propose au philosophe de droite */
up(&mutex); /*Fin section critique */
}

test(i)
{
if( (p[i]==A_FAIM) && /*Si i demande les baguettes */
(p[GAUCHE]!=MANGE) && /*Et qu'elles sont toutes libres */
(p[DROITE]!=MANGE))
{
p[i]=MANGE; /*Récupérer implicitement les baguettes */
up(&s[i]); /*Réveille le philosophe i s'il dormait */
}
}

6.3. Le problème des lecteurs rédacteurs

Il s’agit de partager un fichier ou une base de données par des lecteurs et


des rédacteurs. Plusieurs lecteurs peuvent accéder en même temps. En
revanche l’accès est exclusif pour les rédacteurs. Si un rédacteur a eu
l’accès à la base, aucun autre accès ne doit être autorisé aussi bien pour un
lecteur qu’un rédacteur.
Le tableau suivant montre des exemples d’accès concurrents réalisés selon
quatre ordres différents. Seuls les lecteurs peuvent tous lire en même
temps.
Type d’accès concurrent lecteur rédacteur
lecteur possible interdit
rédacteur interdit interdit

Le problème des lecteurs/rédacteurs est également un cas classique d’école


mais en plus des accès concurrents il permet d’exprimer des priorités. Par
exemple, on peut spécifier que :
1. un rédacteur n’attend pas pour écrire,
2. un rédacteur attend que tous les lecteurs qui ont précédé sa
demande aient fini
La condition 1 indique qu’un lecteur vérifie si un rédacteur n’a pas
demandé l’accès avant de le faire à son tour.
La condition 2 indique que tant qu’il existe des lecteurs, le rédacteur
attend, ce qui privilégie les lecteurs.
La solution suivante donne la priorité aux lecteurs. Pour ce faire le premier
lecteur demande le sémaphore d’accès à la base et le dernier lecteur le
libère ainsi les rédacteurs n’accèdent que lorsqu’il n’y a plus aucun lecteur.
N lecteurs, N rédacteurs avec lecture prioritaire.

struct semaphore acces_nbl ; //protège la manipulation de nbl


struct semaphore acces_bd ; //protège l’accès à la base
int nbl=0 ; //nombre de lecteurs
void lecteur(void)
{
while(true)
{

8
down(&acces_nbl) ; //pour accéder avec exclusion à la
//variable nbl
if(nbl==0)
down(&acces_bd) //si c’est le premier lecteur, il faut
//vérifier s’il y a un rédacteur en
//cours auquel cas il va attendre
nbl++ ;
up(&acces_nbl) ;
…..lecture….
down(&acces_nbl) ;
nbl-- ;
if(nbl==0)
up(acces_bd) ; //il n’y a plus de lecteurs, les
//rédacteurs peuvent accéder
up(&acces_nbl) ;
}
}
void redacteur (void)
{
while (true)
{
down(&acces_bd) ; //pour ne pas accéder à la base en
//même temps
….ecriture….
up(&acces_bd) //libérer le sémaphore
}
}

Vous aimerez peut-être aussi