Exclusion mutuelle - Sections critiques
© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 13/12/1999 - Dernière mise à jour : 28/02/2000

Sommaire
Processus
Ressources
Echanges de données entre processus
Exclusion mutuelle
Sections critiques
Sémaphores - Premièreapproche
Définition succincte
Remarques générales
Réalisation de l'exclusion mutuelle
Problèmes posés par l'utilisation
des sémaphores
Interblocage
Attente indéfinie en section critique
Privation (ou famine)
Processus
Nous nous contenterons de donner ici une définition
très simple et intuitive de la notion de processus.
"Un processus est un programme en cours d'exécution".
Cette notion sera reprise en détail dans le
chapitre du cours S.E. : "Vie et mort d'un processus".
Ressources
Les ressources d'un processus sont les objets qui lui
sont nécessaires au cours de son déroulement. Il peut s'agir
aussi bien de variables, de fichiers, que de processeur, mémoire,
périphériques. Lorsque plusieurs processus s'exécutent
simultanément sur un même site (même machine ou même
réseau), ils sont en général en compétition
pour accéder aux ressources réelles. En particulier, sur
les machines monoprocesseurs, un seul processus peut se voir attribuer
le processeur à un moment donné. De façon générale,
les ressources dont le nombre de processus utilisateurs simultanés
est limité (souvent à 1) sont appelées ressources
critiques (processeur, imprimante, etc...).
Sommaire
Echanges de données
entre processus
Lorsque plusieurs processus s'exécutent en parallèle,
leurs interactions peuvent être nulles, faibles ou très fortes,
et se situer à des niveaux différents. Prenons par exemple
un compilateur ADA et un éditeur de texte sur une station de travail.
Par nature, ils sont totalement indépendants et pourraient tourner
sur des machines complètement séparées. Cependant,
sur la même machine, il peut arriver qu'une compilation soit lancée
sur un fichier source en cours de modification. Il y a donc interaction
et on peut considérer le fichier source comme une ressource critique.
Les conflits qui peuvent en résulter sont réglés par
le système d'exploitation, de façon (heureusement) à
peu près transparente pour l'utilisateur.
Un autre type d'interaction, cette fois voulue et
forte, intervient lorsque deux processus coopèrent : chacun a une
fonction définie dans le système et des échanges d'informations
sont en général fréquemment nécessaires. Considérons
par exemple un programme qui lit des données, les stocke dans une
structure (liste, arbre, objet, fichier, etc...) et les traite.
Une analyse classique du programme conduit à
un corps du programme principal qui appelle successivement ou alternativement
trois sortes de procédures correspondant aux trois sortes d'activités.
On peut donc envisager le programme comme une boucle qui commence par une
saisie, puis, en fonction de cette saisie, stocke la donnée, ou
la traite avant de la stocker, ou la stocke sans la traiter, ou la retire
de la structure de données, ou ..., puis retourne à la saisie.
Il s'agit ici seulement d'appels à des procédures qui s'exécutent
l'une après l'autre, donc séquentiellement. L'interaction
entre les procédures est définie par l'intersection des antécédents
et conséquents de ces procédures, soit, pour l'essentiel,
la donnée en cours de traitement.
Une autre approche, souvent beaucoup plus élégante,
évolutive, et ayant de nombreux autres avantages, consiste à
considérer l'application comme constitué de trois activités
relativement indépendantes :
-
les entrées/sorties sur un écran-clavier (menus déroulants,
masque de saisie, etc...)
-
le traitement proprement dit des données,
-
la mémorisation et la restitution des données dans une structure
de données (liste, arbre, etc...)
Chacune de ces activités est confiée à
un processus différent. La réalisation en est beaucoup
plus simple puisque le travail à accomplir par chaque processus
est plus limité et plus homogène. De plus, si les processus
peuvent s'exécuter vraiment en parallèle (processeurs multiples
ou machines en réseau), des gains de temps importants peuvent être
obtenus. En reprenant l'exemple ci-dessus, un processus peut finir de ranger
une information dans la liste chaînée alors que le processus
de saisie a déjà "rendu la main" à l'utilisateur pour
la saisie de l'information suivante.
Le problème qui se pose alors est celui de
l'échange d'informations entre processus. Plusieurs solutions peuvent
être envisagées selon les cas :
-
un processus écrit dans un fichier, l'autre lit dedans (avec des
conflits possibles si les opérations sont simultanées (ce
type de situation sera repris en détail ultérieurement, lors
de l'étude du problème des "Producteurs-consommateurs").
C'est de toutes façons une solution très lente, à
n'utiliser que s'il n'y a pas mieux.
-
un processus envoie les informations sur le réseau, l'autre les
récupère à l'autre bout. Tous les problèmes
sont gérés par la "couche de communication" et sont totalement
transparents pour l'utilisateur. Cette solution ne correspond qu'au cas
de processus tournant sur des machines distinctes et reliées par
un réseau.
-
lorsque les processus tournent sur la même machine, ils peuvent partager
et utiliser la même mémoire centrale et le même processeur
(et ses registres). Cette dernière solution est trop particulière
pour être envisagée ici. Reste la mémoire : un processus
peut recopier ses informations dans une zone mémoire d'emplacement
convenu à l'avance, où l'autre processus ira les chercher.
On parle alors de mémoire partagée. C'est la méthode
la plus efficace et la plus rapide, qui peut encore être améliorée
dans certains cas en évitant la recopie de l'espace propre d'un
processus vers la mémoire partagée ou inversement. Il suffit
pour cela que les différents processus travaillent directement
avec les variables en mémoire partagée.
Sommaire
Exclusion mutuelle
Lorsque plusieurs processus veulent accéder simultanément
à une même ressource critique, il y a un conflit qui ne peut
être résolu sans mécanisme approprié. Ce mécanisme
a pour objectif d'assurer l'intégrité et la cohérence
du traitement, en n'autorisant l'accès à la ressource qu'à
un seul processus, le temps que l'opération soit complète.
Soit X une variable en mémoire partagée.
L'accès à cette variable doit être effectué
en exclusion mutuelle. En effet considérons l'instruction ADA suivante
:
Cette opération n'est pas atomique
: elle n'est pas effectuée "en une fois", (nous reviendrons plus
tard dans le cours S.E. sur la notion d'atomicité). Elle est traduite
dans un assembleur par plusieurs instructions élémentaires
qui vont être exécutées successivement :
{1} LOAD reg, x
{2} ADD reg, 5
{3} STO reg, x |
Supposons que deux processus veuillent incrémenter
simultanément X. La séquence d'instructions peut
être la suivante : {1}, {2}, {1'}, {2'},
{3'}
et {3}, le processus 1 ayant été interrompu après
avoir commencé l'opération. Le résultat sera faux.
Il faut donc que la variable X soit protégée pendant
toute la durée de l'opération par une exclusion mutuelle.
Le processeur étant une ressource critique,
son utilisation doit être faite en exclusion mutuelle. C'est bien
ce qui se passe dans la réalité, un seul processus étant
exécuté à la fois. Le mécanisme qui réalise
l'exclusion mutuelle est ici totalement matériel.
L'accès à une ressource critique de plusieurs processus a
pour effet de les sérialiser.
Sommaire
Sections critiques
L'exclusion mutuelle ne se limite pas à empêcher
le partage de ressources, mais aussi à éviter que deux processus
effectuent simultanément des actions qui provoqueraient une situation
conflictuelle. L'ensemble des instructions consécutives qui doivent
être exécutées dans un processus en exclusion mutuelle
constituent une section critique. Il peut y avoir plusieurs sections
critiques dans le même processus. La section critique d'un programme
n'a de sens que s'il existe (ou peut exister) au moins une autre section
critique correspondant aux mêmes ressources dans au moins un autre
processus. Il peut aussi y avoir plusieurs sections critiques dans un programme,
correspondant chacun à l'accès à des ressources critiques
différentes. Dans l'exemple ci-dessus (incrémentation de
la variable X) les trois instructions en Assembleur doivent
se trouver en section critique.
Quelques règles doivent être données
pour une utilisation correcte des sections critiques :
-
une section critique doit être la plus courte possible. Elle perturbe
en effet le quasi-parallélisme et pourrait priver les autres processus
de la ressource processeur.
-
un processus ne peut rester indéfiniment dans une section critique.
Cela implique que, si un programme boucle dans une section critique, le
système puisse non seulement l'interrompre au bout d'un temps raisonnable,
mais encore débloquer les autre processus en attente de la même
ressource.
-
il ne faut faire aucune hypothèse sur les vitesses d'exécution
relative des différentes instructions,
-
un processus hors de sa section critique ne doit pas pouvoir empêcher
un autre d'accéder à la ressource critique associée
à cette section.
Sommaire
Sémaphores - Première
approche
Il existe de nombreux moyens permettant de réaliser
l'exclusion mutuelle, qui seront étudiés ultérieurement
dans le cours S.E. On peut envisager des moyens purement matériels
(instruction machine TAS), logiciels (algorithmes d'exclusion
mutuelle), ou des services de haut niveau offerts par le S.E. : sémaphores,
moniteurs, etc...
Les sémaphores sont parmi les outils les plus
élémentaires permettant de réaliser l'exclusion mutuelle.
Ils sont à la fois faciles à implanter et suffisamment puissants
pour permettre d'exprimer élégamment les solutions aux problèmes
d'exclusion mutuelle et de synchronisation. Ils ont été imaginés
par DIJSKTRA en 1965.
Définition succincte
Un sémaphore est une "variable" protégée
s (implémentation cachée), dont une "donnée-membre"
est un compteur "NATURAL" initialisé à sa création
à une valeur s0 de type "NATURAL".
Seules les deux primitives P(s) et V(s) permettent
d'accéder à cette variable (P et V sont
les initiales en hollandais des mots attendre (Passeren) et signaler
(Vrijmaken)). Dans certains ouvrages, les primitives de base sont
wait(s) et signal(s). Rappelons qu'une primitive est
une opération indivisible, c'est-à-dire non interruptible,
atomique.
L'appel par un programme de la primitive P(s),
où s est la variable "sémaphore", provoque l'exécution
de la séquence suivante :
si (valeur du compteur de s > 0)
alors
décrémenter le compteur de 1
sinon
<suspendre l'exécution du processus
en cours>
finsi |
L'appel par un programme de la primitive V(s),
où s est la variable "sémaphore", provoque l'exécution
de la séquence suivante :
si (au moins un processus a été précédemment
suspend
sur s par
une opération P(s))
alors
<réveiller un de ces processus>
sinon
incrémenter le compteur de 1
finsi |
Sommaire
Remarques générales
Il va de soi que les sémaphores sont eux-mêmes
des ressources partagées : deux processus dont les conflits
d'accès à des ressources critiques sont réglés
au moyen de sémaphores doivent pouvoir accéder aux mêmes
sémaphores. Pour que ces "objets" soient "globaux" aux processus,
ils doivent appartenir soit au S.E. qui contrôle les processus, soit
à un processus qui les englobe.
Les primitives P et V s'excluent
mutuellement (par définition), donc si deux d'entre elles se produisent
simultanément, elles sont exécutées séquentiellement
dans un ordre arbitraire.
La primitive V n'est jamais bloquante. La
primitive P peut l'être.
Un sémaphore qui ne prend que les valeurs
0 et 1 est appelé sémaphore binaire. Sinon, il est
appelé sémaphore général ou sémaphore
n-aire.
Il est impossible d'accéder au contenu d'un
sémaphore autrement que par les deux primitives. En particulier,
il est impossible de tester ou de modifier la valeur du compteur. De plus,
les processus en attente sur un sémaphore sont stockés dans
une structure cachée (une donnée-membre du sémaphore),
elle aussi inaccessible à l'utilisateur qui ne peut ni savoir comment
elle est gérée, ni en modifier gestion. C'est souvent une
structure FIFO (file d'attente First In First Out)
qui est adoptée.
Quelle que soit l'implémentation (mécanisme
offert par le S.E., par un langage de programmation, ou simulation de sémaphore
par un autre mécanisme), nous supposons ici qu'un sémaphore
est un objet de type T_Semaphore. Suivant l'implémentation,
les primitives P(s) et V(s) existent ou doivent être
implémentées. De plus l'utilisateur dispose de, ou doit mettre
en place, deux primitives permettant de créer et de détruire
un sémaphore: Creer_Sem(s, v) et Detruire_Sem(s),
où s est de type T_Semaphore et v est
la valeur entière à laquelle le compteur associé au
sémaphore est initialisé à la création. On
peut montrer que v correspond au nombre d'accès simultanés
maximal à la ressource critique.
Sommaire
Réalisation de
l'exclusion mutuelle
La séquence suivante illustre la réalisation de l'exclusion
mutuelle par des sémaphores. On suppose que deux processus se déroulent
en parallèle, que le sémaphore s a été
créé et initialisé à la valeur 1 (sémaphore
binaire) et qu'il est partagé par les deux processus. L'algorithme
ci-dessous est celui du processus 1. Le processus 2 n'est pas représenté,
il a la même forme générale.
procedure Process_1 is -- représente le coeur du processus
1
begin
loop
Section_Non_Critique;
-- partie a executer sans exclusion mutuelle
P (s); -- blocage du
semaphore
Section_Critique; --
partie a executer en exclusion mutuelle
V (s)
-- liberation du semaphore
Section_Non_Critique;
-- partie a executer sans exclusion mutuelle
end loop;
end Process_1; |
Le fonctionnement est le suivant: l'un des deux processus
se présente devant le sémaphore (exécute la primitive
ininterruptible P(s)). Le compteur est décrémenté
et vaut donc 0 et le processus entre dans sa section critique. Si le deuxième
processus exécute à son tour P(s), le compteur ne
peut être décrémenté, donc le processus est
mis en file d'attente. Il ne sera réveillé que lorsque l'autre
aura fini d'exécuter V(s).
Ce mécanisme peut être étendu
à un nombre quelconque de processus: le premier arrivé entrera
dans sa section critique, tous les autres iront en file d'attente, l'un
d'eux sera réveillé par la sortie du premier, entrera lui-même
dans sa section critique, et ainsi de suite.
Sommaire
Problèmes
posés par l'utilisation des sémaphores
Malgré l'apparente simplicité de la solution,
de nombreuses difficultés peuvent se présenter. Le premier
reproche qui peut être fait au mécanisme des sémaphores
est qu'il laisse beaucoup trop de responsabilité aux utilisateurs,
ne garantissant à aucun processus que les autres vont fonctionner
correctement, comme le montrent les exemples suivants.
Interblocage
L'interblocage est la situation la plus facile à
obtenir, par exemple s'il y a deux ressources critiques convoitées
par deux processus différents, et protégées par deux
sémaphores s1 et s2, ce qui provoque l'imbrication
de deux sections critiques. L'exemple ci-dessous en est une illustration
:
Processus_1
{1} P (s1);
{2} P (s2);
{3} Section_Critique_1;
{4} V (s2);
{5} V (s1); |
Processus_2
{1'} P (s2);
{2'} P (s1);
{3'} Section_Critique_2;
{4'} V (s1);
{5'} V (s2); |
A l'évidence, si les deux processus
se présentent à peu près simultanément devant
leurs section critique, la séquence suivante est possible et conduit
à l'interblocage:
Aucun des deux processus ne peut aller plus loin!
On démontre que seules les sections imbriquées dans le
même ordre protègent de l'interblocage.
Il est d'ailleurs encore plus facile d'obtenir un
simple blocage d'un processus par lui-même :
P (s);
P (s); !!! c'est fini. |
Il est donc risqué de confier à
l'utilisateur la responsabilité de s'assurer de la validité
de l'implémentation.
Sommaire
Attente indéfinie
en section critique
Le fonctionnement satisfaisant de l'exclusion mutuelle
par les sémaphores suppose qu'un processus qui est rentré
dans sa section critique finit toujours par en sortir en un temps fini.
Mais cela est laissé à la responsabilité de l'utilisateur,
ce qui est risqué. De plus, indépendamment de sa volonté,
un processus peut avoir des problèmes (par exemple être détruit,
ou en attente d'une E/S impossible : impression sur une imprimante éteinte
!!). Pour remédier à cet inconvénient, il faut prendre
des mesures de précaution qui seront étudiées ultérieurement
dans le cours.
Privation (ou famine)
La sémantique des sémaphores ne précisant
pas la façon de gérer les files d'attente, certains processus
bloqués peuvent se trouver indéfiniment privés des
ressources critiques. Les files organisées en FIFO garantissent
les processus contre la famine.

© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique