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 :

    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 :

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 :
 
X := X + 5;

    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 Xles trois instructions en Assembleur doivent se trouver en section critique.

    Quelques règles doivent être données pour une utilisation correcte des sections critiques :

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:
 
{1}, {1'}, {2}, {2'}

    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