Académique Documents
Professionnel Documents
Culture Documents
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
local _ in in;
palcer _ job(local _ in);
in local _ in 1;
1
cohérent avec l’état des variables, et le processus d’impression ne pourra
détecter aucune anomalie.
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.
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.
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).
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.
Table
Assiette
Baguette
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 :
typedef enum
{
PENSE,
A_FAIM,
MANGE
} Philosophe;
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 */
}
}
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
}
}