13
Synchronisation et communication entre processus
Nous avons déjà dit que, si les processus
étaient des entités autonomes et indépendantes, ils
pouvaient se trouver en conflit pour l'accès à certaines
ressources communes. Ceci nécessite la mise en œuvre de
mécanismes dits de synchronisation pour gérer ces conflits. Par
ailleurs, si les processus sont en général indépendants,
cela ne doit pas interdire la communication. La conséquence de ces
mécanismes est le blocage des processus en attente d'une ressource
momentanément indisponible. Deux problèmes en découlent:
l'interblocage et la famine.
13.1. Les mécanismes de synchronisation
Considérons un compte bancaire, dont le montant est
mémorisé dans un emplacement donné
A sur disque. Le programme qui consiste
à ajouter 100 à ce compte
pourrait être le suivant, où N
est une variable locale du programme:
lire (N, A);
N := N + 100;
écrire (N, A);
Si
maintenant deux processus différents consistent en l'exécution de
ce même programme, le processeur sera alloué à chacun d'eux
dans un ordre quelconque. En particulier, on peut imaginer que l'ordre soit le
suivant:
processus P1 processus P2
lire (N, A);
lire (N, A);
N := N + 100;
écrire (N, A);
N := N + 100;
écrire (N, A);
N
étant une variable locale du programme, implique qu'il en existe un
exemplaire pour chacun des deux processus. Il s'ensuit que, si la valeur
initiale du compte est 1000, on constate
qu'après ces exécutions, il est de
1100 au lieu de
1200. Les deux processus ne sont pas en
fait totalement indépendants. Ils partagent la ressource commune qu'est
la valeur du compte bancaire à l'adresse
A sur disque. Cette ressource doit
être à un seul point d'accès, c'est donc une ressource
critique, et les processus sont en exclusion mutuelle sur cette ressource
critique.
Si la variable N est
commune aux deux processus, le problème n'est pas résolu pour
autant. En effet l'instruction N := N + 100
n'est pas une instruction “indivisible”. En fait elle est
décomposée en trois instructions de la machine qui sont par
exemple:
LOAD N
ADD 100
STORE N
L'allocateur
du processeur prend en compte les instructions de la machine et non les
instructions du langage évolué pour déterminer les moments
où il peut remplacer le processus actif par un autre. En particulier,
dans cet exemple, il est possible que le changement ait lieu juste après
la première instruction, donnant la séquence suivante:
processus P1 processus P2
LOAD N
LOAD N
ADD 100
STORE N
ADD 100
STORE N
13.1.1. Les verrous
Un mécanisme proposé pour permettre de
résoudre l'exclusion mutuelle d'accès à une ressource est
le mécanisme de verrou (en anglais lock). Un verrou est un
objet système sur lequel deux opérations sont définies. Un
tel objet peut s'assimiler à une ressource logiciel, les
opérations permettant d'acquérir ou de libérer cette
ressource.
- verrouiller (v) permet au
processus d'acquérir le verrou v s'il est disponible. S'il n'est
pas disponible, le processus est bloqué en attente de la
ressource.
- déverrouiller (v) permet
au processus de libérer le verrou v qu'il possédait. Si un
ou plusieurs processus étaient en attente de ce verrou, un seul de ces
processus est réactivé et reçoit le
verrou.
En tant qu'opérations systèmes, ces
opérations sont indivisibles, c'est-à-dire que le système
garantit l'absence d'interférence entre leurs exécutions par
plusieurs processus. On dit encore que ce sont des primitives.
13.1.2. Les sémaphores
Un
sémaphore est un mécanisme
proposé par E.W.Dijkstra en 1965, et qui est un peu plus
général que le verrou. Il se présente comme un distributeur
de jetons, mais le nombre de jetons est fixe et non renouvelable: les processus
doivent restituer leur jeton après utilisation. S'il y a un seul jeton en
circulation, on retrouve le verrou. Les deux opérations sur un
sémaphore sont traditionnellement dénotées
P(s) et
V(s), mais cette notation est de peu de signification pour ceux qui
parlent le Hollandais, et aucune pour les
autres
[1]!
- P(s) ou down(s) permet à un
processus d'obtenir un jeton, s'il y en a de disponibles. Si aucun n'est
disponible, le processus est
bloqué.
- V(s) ou up(s) permet
à un processus de restituer un jeton. Si des processus étaient en
attente de jeton, l'un d'entre eux est réactivé et le
reçoit.
Notons qu'un sémaphore peut
être vu comme un couple constitué d'un entier, encore appelé
le niveau du sémaphore et d'une file d'attente de processus. Le
niveau est le nombre de jetons encore disponibles. Il est évident que si
le niveau est positif, la file d'attente est vide. Parfois on utilise les
valeurs négatives du niveau pour représenter le
“déficit” en jetons, c'est à dire le nombre de
processus en attente de jeton.
À la création d'un sémaphore, il faut
décider du nombre de jetons dont il dispose. On voit que si une ressource
est à un seul point d'accès (critique), le sémaphore doit
avoir initialement 1 jeton, qui sera attribué successivement à
chacun des processus demandeurs. Si une ressource est à
n points d'accès,
c'est-à-dire, peut être utilisée par au plus
n processus à la fois, il suffit
d'un sémaphore initialisé avec
n jetons. Dans les deux cas, un processus
qui cherche à utiliser la ressource, demande d'abord un jeton, puis
utilise la ressource lorsqu'il a obtenu le jeton et enfin rend le jeton
lorsqu'il n'a plus besoin de la ressource.
13.1.3. Les mécanismes plus élaborés
Les mécanismes proposés ci-dessus sont assez
rudimentaires. Ils posent deux problèmes:
- Que faire lorsqu'on désire mettre en
œuvre une politique d'allocation plus
élaborée?
- Que faire pour obliger
les processus à respecter une certaine règle du
jeu?
Le premier problème peut être
résolu en construisant un algorithme plus ou moins complexe, qui utilise
une zone de mémoire commune aux processus et des sémaphores pour
assurer la synchronisation. La figure 13.1 donne un exemple où l'on
distingue deux sortes de processus qui accèdent à un même
fichier, les “lecteurs” qui exécutent une suite de lectures
sur le fichier, et les “rédacteurs” qui exécutent une
suite de lectures et d'écritures. Les morceaux de programme
correspondants montrent une façon de garantir qu'un rédacteur
accède seul au fichier, alors que les lecteurs peuvent y accéder
ensembles.
initialisation
mutex.niveau := 1;
lect_redac.niveau := 1;
n_l := 0;
processus lecteurs processus rédacteurs
down(mutex);
n_l := n_l + 1;
si n_l = 1 alors
down(lect_redac); down(lect_redac);
finsi;
up(mutex);
...
lectures lectures et écritures
...
down(mutex);
n_l := n_l - 1;
si n_l = 0 alors
up(lect_redac); up(lect_redac);
finsi;
up(mutex);
Fig.
13.1. Exemple de synchronisation entre des lecteurs et des
rédacteurs.
Deux sémaphores dotés chacun d'un seul jeton
sont utilisés, ainsi qu'un compteur commun qui note le nombre de lecteurs
en cours. Le jeton lect_redac est
possédé soit par un rédacteur, soit par l'ensemble des
lecteurs. C'est pourquoi, seul le premier lecteur demande ce jeton, qui est
restitué par le dernier lecteur qui sort. Le deuxième
sémaphore contrôle l'accès à la ressource critique
n_l par les lecteurs.
Le deuxième problème évoqué
ci-dessus est bien visible dans cet exemple: comment obliger les processus
lecteurs ou rédacteurs à respecter la règle du jeu qui
vient d'être décrite? La solution adoptée par certains
systèmes est de définir plusieurs politiques de gestion de
certaines ressources par le biais d'opérations spécifiques, et de
contrôler le respect de la règle du jeu par les processus dans ces
opérations.
Ainsi certains systèmes de gestion de fichiers ont un
paramètre particulier de l'opération d'ouverture qui
précise si le processus désire l'accès exclusif ou
partagé au fichier.
- Lorsque le processus demande l'accès
exclusif, il sera mis en attente si un autre processus accède
déjà au fichier; de plus, lorsque le processus obtient
l'accès, le système empêche tout autre processus d'y
accéder.
- Lorsque le processus demande
l'accès partagé, il ne sera mis en attente que si un processus a
obtenu l'accès en exclusif sur le fichier.
Notons
que ceci implique que le système exécute pour le compte du
processus un algorithme voisin de celui d'un lecteur ou d'un rédacteur
proposé plus haut.
De même, les SGBD prennent en compte les conflits
d'accès aux données de la base, et obligent explicitement ou
implicitement les processus à respecter une règle du jeu
fixée par le SGBD ou par l'administrateur de la base de données.
Du point de vue des processus, une règle du jeu implicite transforme la
base de données en une ressource virtuelle au sens où nous l'avons
défini dans le chapitre précédent, les processus pouvant y
accéder comme s'ils étaient seuls.
13.2. La communication entre processus
Lorsque des processus ont besoin d'échanger des
informations, ils peuvent le faire par l'intermédiaire d'une zone de
mémoire commune. Nous en avons déjà vu un exemple lors de
la présentation des lecteurs et des rédacteurs ci-dessus: les
lecteurs accèdent au même compteur
n_l. Cette communication présente
l'inconvénient d'être peu structurée, et de
nécessiter l'utilisation de l'exclusion mutuelle d'accès à
cette zone commune. Par ailleurs elle pose des problèmes de
désignation des données de cette zone qui doit être dans
l'espace de chaque processus qui désire communiquer avec les
autres.
13.2.1. Le schéma producteur-consommateur
On peut définir un mécanisme
général de communication entre processus, où l'un est
l'émetteur de l'information (on lui donne le nom de producteur),
et l'autre est le récepteur de l'information (on lui donne le nom de
consommateur). Il n'est souvent pas nécessaire de faire attendre
le producteur jusqu'à ce que le consommateur ait reçu
l'information. Pour cela il suffit de disposer d'un tampon entre les deux
processus pour assurer la mémorisation des informations produites non
encore consommées. Nous appellerons message le contenu d'une telle
information. Le tampon peut recevoir un nombre limité de ces messages,
noté N. Les deux processus doivent
se synchroniser entre eux de façon à respecter certaines
contraintes de bon fonctionnement. La figure 13.2 donne un schéma de
cette synchronisation.
initialisation
n_plein.niveau := 0;
n_vide.niveau := N;
producteur consommateur
down(n_vide); down(n_plein);
dépôt dans le tampon; retrait du tampon;
up(n_plein); up(n_vide);
Fig.
13.2. Le schéma producteur-consommateur.
- Le producteur ne peut déposer un message
dans le tampon s'il n'y a plus de place libre. Le nombre de places libres dans
le tampon peut être symbolisé par un nombre correspondant de jetons
disponibles; ces jetons peuvent être conservés par un
sémaphore
n_vide.
- Le
consommateur ne peut retirer un message depuis le tampon s'il n'y en a pas. Le
nombre de messages déposés peut être également
symbolisé par un nombre correspondant de jetons disponibles; ces jetons
peuvent être conservés par un sémaphore
n_plein.
- Le
consommateur ne doit pas retirer un message que le producteur est en train de
déposer. Cette dernière contrainte est implicitement obtenue, dans
le schéma de la figure, si les messages sont retirés dans l'ordre
où ils ont été mis.
Informellement,
on peut dire que le producteur prend d'abord un jeton de place libre,
dépose son message et rend un jeton de message disponible. De son
côté, le consommateur prend un jeton de message disponible, retire
le message et rend un jeton de place libre.
Pour obliger les processus à respecter la règle
du jeu, les systèmes proposent souvent des opérations de
dépôt et de retrait qui assurent elles-mêmes la
synchronisation entre les processus.
13.2.2. Les tubes Unix
Les tubes Unix (en anglais pipes) sont une
implantation de la communication suivant le schéma
producteur-consommateur, avec un tampon de taille fixe, interne au
système. Pour un processus, un tube se présente comme deux flots,
l'un ouvert en écriture (pour le producteur), l'autre ouvert en lecture
(pour le consommateur). Lorsqu'un processus crée un tube, le
système lui retourne les deux flots. Si ce processus crée ensuite
un processus fils (opération fork
vue précédemment), celui-ci obtient une copie des flots ouverts,
lui permettant ainsi de lire ou d'écrire dans le tube, à moins que
l'un de ces flots n'ait été fermé. Ne peuvent donc
communiquer par un tube que les processus situés dans la
sous-arborescence dont la racine est le processus qui a créé ce
tube.
Lors d'une écriture de
n octets par le producteur, celui-ci sera
mis en attente s'il n'y a pas assez de place dans le tube pour y mettre les
n octets. Il sera réactivé
lorsque les n octets auront pu être
tous écrits dans le tube. Par ailleurs, le système interrompra
l'exécution normale du processus s'il n'y a plus de consommateur pour ce
tube.
Lors d'une lecture de p
octets par le consommateur, celui-ci sera mis en attente s'il n'y a pas assez
d'octets dans le tube pour satisfaire la demande. Il sera réactivé
lorsque le nombre d'octets demandé aura pu lui être
délivré. Par ailleurs, s'il n'y a plus de producteur pour ce tube,
le système lui délivrera les octets restants, à charge pour
le consommateur, lorsqu'il obtient un nombre d'octets nul, d'en déduire
que la production est terminée.
La figure 13.3 donne un exemple de cette communication. Le
processus père crée le tube et le processus fils, puis envoie dans
le tube la suite des entiers de 0 à 1000, les entiers étant
supposés sur 2 octets. Le processus fils lit les octets 2 par 2 depuis le
tube dans un entier et imprime celui-ci. Il s'arrête lorsque le tube est
vide et que le père a fermé le tube en écriture, puisque
alors il n'y a plus de producteur.
var tube: tableau [0..1] de flot;
i : entier;
début pipe(tube); { création du tube }
si fork() ≠ 0 alors { création du processus fils }
fermer (tube[0]); { le père ne lit pas dans le tube }
pour i := 0 jusqu'à 1000 faire
écrire (tube[1], i, 2); { écriture de deux octets }
fait;
fermer (tube[1]); { le père a fini }
wait(0); { attente de la fin du fils }
sinon
fermer (tube[1]); { le fils n'écrit pas dans le tube }
tant que lire (tube[0], i, 2) = 2 faire
imprimer (i); { on suppose qu'un entier occupe 2 octets }
fait;
fermer (tube[0]); { le fils a fini }
finsi;
fin;
Fig.
13.3. Communication par tubes Unix.
Les tubes Unix ne conservent pas la structuration des messages
qui sont envoyés par le producteur. Dans l'exemple ci-dessus, producteur
et consommateur participent tous les deux à cette structuration: le
consommateur lit depuis le tube les octets 2 par 2 pour retrouver la structure
des messages émis par le producteur. Notons que ceci est conforme
à la philosophie déjà énoncée de ce
système qui présente tout objet externe comme une suite
linéaire d'octets. Cette uniformité présente l'avantage de
cacher au programme la nature de l'objet externe manipulé, même
lorsqu'il s'agit d'un tube.
13.2.3. La communication par boîte aux lettres
Nous appellerons boîte aux lettres un objet
système qui implante le schéma producteur-consommateur
défini plus haut, en conservant le découpage en messages tels que
les a déposés le producteur. Les messages sont parfois
typés, comme dans iRMX86, où les messages comportent un
en-tête qui précise, en particulier, sa longueur et son type sous
la forme d'un code défini par le programmeur.
Le problème de la désignation d'une boîte
aux lettres par les processus est résolu de différentes
façons. Il s'agit, en effet, de permettre à des processus dont les
espaces mémoire sont disjoints de pouvoir désigner une même
boîte aux lettres. Une solution comme celle proposée par Unix pour
les tubes, peut être retenue, qui consiste à mettre cette
désignation dans le contexte des processus au moment de leur
création. On préfère, en général, une
solution semblable à une édition de liens, qui consiste à
donner un nom symbolique aux boîtes aux lettres, sous forme d'une
chaîne de caractères. Le système maintient une table de ces
noms symboliques associés aux boîtes aux lettres qui ont
déjà été créées et non encore
détruites. Lorsqu'un processus demande l'accès à une telle
boîte aux lettres, le système consulte cette table, et retourne au
demandeur le descripteur de la boîte aux lettres (c'est, par exemple, son
indice dans le table). Les opérations suivantes se feront en utilisant ce
descripteur. Dans certains cas, cette demande d'accès entraîne sa
création implicite si elle n'existe pas, alors que d'autres
systèmes séparent l'opération de création explicite
de la demande d'accès.
Les opérations sur une boîte aux lettres sont
l'envoi et la réception de message. L'envoi peut être avec ou sans
blocage du demandeur lorsque la boîte aux lettres est pleine. La
réception peut être également avec ou sans blocage lorsque
la boîte aux lettres est vide. De plus, certains systèmes
permettent de filtrer les messages reçus, c'est-à-dire, qu'il est
possible de préciser le type des messages que l'on accepte de
recevoir.
Les boîtes aux lettres ont souvent une capacité
limitée de messages, comme nous l'avons indiqué dans le
schéma général. Parfois cette capacité est nulle,
l'envoi de message ne pouvant avoir lieu que lorsque le receveur est prêt
à recevoir le message. Constatons que le schéma
précédent doit être modifié; nous en laissons le soin
au lecteur. On dit alors qu'il y a communication synchrone entre le producteur
et le consommateur. L'intérêt essentiel est que cette fois,
après le retour de la primitive d'envoi, le producteur a la garantie que
le consommateur a effectivement reçu le message, alors que dans le cas
général, le producteur ne peut préjuger du moment où
cette réception aura lieu. En contrepartie, les processus supportent une
contrainte de synchronisation plus forte.
13.3. Les conséquences sur les temps de réponse
Dans les premiers chapitres, nous avons défini le temps
de réponse comme le délai qui sépare l'envoi d'une commande
par un utilisateur, de la fin d'exécution de la commande. Nous pouvons
ici décomposer ce délai en trois mesures différentes,
suivant les états du processus correspondant.
Dans l'état actif, on mesure le temps de processeur (le
temps virtuel) nécessaire à l'exécution proprement dite de
la commande. Ce temps est évidemment incompressible, pour un programme
donné. L'algorithme de la commande elle-même doit être
efficace pour minimiser ce temps.
Dans l'état prêt, on mesure le temps pendant
lequel le processus attend le processeur, ressource possédée par
un autre processus. L'idéal, pour l'utilisateur, est de réduire le
plus possible ce temps. C'est en quelque sorte le prix qu'il doit payer pour ne
pas avoir à payer plus cher la machine. Sur un système normalement
chargé, ce temps ne devrait pas être important. Si ce temps est
important pour tous les processus, il est probable que ce soit l'indication d'un
système surchargé, qui nécessite une augmentation de
puissance.
Dans l'état bloqué, on mesure le temps pendant
lequel le processus attend une ressource autre que le processeur. Cette attente
peut avoir deux causes:
- La première est due à la lenteur
de l'accès demandé lors des entrées-sorties. Comme pour le
temps de processeur, le programmeur peut améliorer l'algorithme pour
réduire le nombre de ces entrées-sorties. Un choix plus judicieux
du support peut également avoir une certaine
influence.
- L'autre cause est due au fait que la
ressource est possédée par un autre processus. Ceci implique un
conflit entre les utilisateurs. Évidemment, si l'utilisateur était
seul, ce conflit n'existerait pas. C'est souvent un paramètre très
important, mais parfois négligé et difficile à
contrôler. La réduction de ces conflits nécessite de
multiplier les ressources (beaucoup de petites au lieu d'une grosse), pour
diminuer la probabilité de conflit, et de minimiser la portion de
programme pendant laquelle les processus réservent l'accès
à ces ressources (réservation le plus tard possible et
libération le plus tôt
possible).
13.4. La notion d'interblocage
La synchronisation et la communication des processus peut
conduire à un autre problème. Supposons qu'un premier processus
P1 demande et obtienne la ressource imprimante, seule
ressource de ce type. Un second processus P2 demande ensuite
et obtient la ressource dérouleur de bande, également seule de ce
type. Si P1 demande ultérieurement la ressource
dérouleur de bande sans avoir libéré la ressource
imprimante, il est bloqué en attendant la libération par
P2 . Si maintenant, P2 demande la ressource
imprimante avant de libérer la ressource dérouleur de bande, il
est bloqué en attendant la libération par P1 de
cette ressource. On voit que chacun des processus est bloqué, en
attendant que l'autre libère la ressource qu'il possède. On dit
qu'il y a un interblocage entre les processus (en anglais
deadlock).
On peut donner une représentation graphique à
l'interblocage. Représentons les ressources par des rectangles, et les
processus par des cercles. On met une flèche d'une ressource vers un
processus lorsque la ressource est allouée au processus, et une
flèche d'un processus vers une ressource lorsque le processus attend
cette ressource. La figure 13.4 représente l'évolution du graphe
de notre exemple précédent conduisant à l'interblocage.
Fig. 13.4. Allocation conduisant à
l'interblocage.
On peut constater que dans le graphe final, il y a un cycle
représenté par:
En fait, tout interblocage est signalé par la
présence d'un tel cycle dans ce graphe, et inversement, ce qui explique
l'intérêt de cette représentation.
Le problème de l'interblocage est résolu de
diverses façons par les systèmes.
- La première méthode est la
politique de l'autruche: elle consiste à ignorer le
problème! Considérant que le coût des autres méthodes
est assez élevé, il peut s'avérer rentable de ne pas
résoudre le problème lorsque la probabilité d'avoir un
interblocage est faible. S'il survient, il faut relancer le
système.
- La deuxième méthode
est une méthode de détection-guérison. Elle consiste
à vérifier de temps en temps la présence de tels cycles, et
à tuer un ou plusieurs processus jusqu'à la disparition de ces
cycles. Chaque destruction d'un tel processus entraîne évidemment
la restitution des ressources qu'il
possédait.
- La troisième
méthode est une méthode de prévention. Elle consiste
à imposer des contraintes sur les demandes des processus de telle sorte
qu'un tel cycle ne puisse jamais se produire. Ainsi, si les deux processus
ci-dessus demandaient leurs ressources dans le même ordre (par exemple
imprimante puis dérouleur), le deuxième processus serait
bloqué sur la demande de la première ressource, sans avoir encore
obtenu aucune ressource, et il ne pourrait y avoir de
cycle.
- La quatrième méthode est une
méthode d'évitement. Elle consiste à prévoir
à l'avance les différents cas où un cycle pourrait se
former, et à éviter l'interblocage en retardant une allocation qui
serait possible, jusqu'à être certain qu'il n'y a pas de risque
d'interblocage. La figure 13.5 montre sur notre exemple qu'en retardant
l'allocation du dérouleur à P2 jusqu'à ce
que P1 ait libéré l'imprimante, on permet
à celui-ci d'acquérir le dérouleur avant
P2 et on évite ainsi la création du cycle. Il
faut évidemment savoir à l'avance quels seront les besoins des
processus dans ce
cas.
Fig. 13.5. Évitement de
l'interblocage.
La méthode utilisée dépend des
applications qui sont les plus courantes sur le système. Par exemple, les
systèmes d'IBM, mais aussi ceux de bien d'autres constructeurs, ont
utilisé successivement plusieurs de ces méthodes. Après
avoir appliqué la politique de l'autruche justifiée par le fait
que les “plantages” du système étaient bien plus
fréquents que les interblocages, la politique de prévention a
été utilisée pour le traitement par lot. La politique
d'évitement est également souvent utilisée pour le
traitement par lot. Par contre, la plupart des systèmes transactionnels
préfèrent la politique de détection-guérison, en
détruisant une transaction qui demande une ressource si son blocage
entraîne un cycle. Le noyau d'Unix utilise la politique de l'autruche,
considérant que l'utilisateur qui constate un interblocage entre ses
processus, peut toujours les tuer lui-même depuis sa console.
13.5. La notion de famine
Un processus qui est en attente d'une ressource peut rester
bloqué indéfiniment sans être pour autant en situation
d'interblocage. On dit que l'on a une situation de famine (en anglais
starvation), lorsque un ou plusieurs processus n'obtiennent pas les
ressources dont ils ont besoin par suite du comportement de l'ensemble des
processus, sans être en situation d'interblocage. La différence
essentielle réside dans le fait qu'il est possible de sortir de la
situation de famine par suite du changement du comportement des processus, alors
qu'il n'est pas possible de sortir d'un interblocage sans retirer
autoritairement une ressource à un des processus en
interblocage.
Reprenons l'exemple des lecteurs-rédacteurs que nous
avons présenté plus haut. Supposons qu'aucun processus
n'accède initialement au fichier. Lorsque le premier lecteur obtient le
jeton lect_redac, il interdit à tous
les écrivains de l'obtenir tant que le dernier lecteur ne l'a pas
été libéré. Par contre les lecteurs peuvent toujours
obtenir l'accès en lecture qu'ils désirent. Il peut s'en suivre
une coalition des lecteurs de telle sorte qu'il y ait toujours un lecteur en
permanence sur le fichier. Les rédacteurs n'obtiendront alors jamais
satisfaction. Cependant, il ne s'agit pas d'un interblocage, puisque si les
lecteurs s'arrêtent de travailler, le dernier libèrera le jeton
lect_redac permettant ainsi aux
rédacteurs d'obtenir l'un après l'autre satisfaction.
initialisation
équité.niveau := 1;
mutex.niveau := 1;
lect_redac.niveau := 1;
n_l := 0;
processus lecteurs processus rédacteurs
down(équité); down(équité);
down(mutex);
n_l := n_l + 1;
si n_l = 1 alors
down(lect_redac); down(lect_redac);
finsi;
up(mutex);
up(équité); up(équité);
...
lectures lectures et écritures
...
down(mutex);
n_l := n_l - 1;
si n_l = 0 alors
up(lect_redac); up(lect_redac);
finsi;
up(mutex);
Fig.
13.6. Synchronisation équitable des lecteurs et des
rédacteurs.
La famine est souvent une conséquence de la politique
d'allocation qui est suivie. Si elle est équitable, elle sera
évitée. Sur notre exemple (figure 13.6), l'introduction d'un
sémaphore supplémentaire
équité évite la
famine, pourvu que le déblocage des processus en attente sur un
sémaphore soit effectué dans l'ordre où ils sont
arrivés. En effet, tous les processus doivent maintenant acquérir
un jeton équité pour savoir
s'ils peuvent entrer, et le libèrent lorsqu'ils ont obtenu
l'autorisation. Comme l'attribution du jeton
équité est faite dans l'ordre
où les processus sont demandeurs, un processus lecteur n'obtiendra le
sien que lorsque tous les rédacteurs arrivés avant lui auront
obtenu satisfaction. Par contre, si plusieurs lecteurs se suivent, le premier
obtiendra le jeton équité
puis le jeton lect_redac, et
libèrera le jeton
équité permettant aux
lecteurs qui le suivent immédiatement d'entrer dans leur phase de
lecture.
13.6. Conclusion
+ Le mécanisme
de verrouillage permet de faire en sorte qu'au plus un seul processus
possède le verrou à un instant donné. Les opérations
sur les verrous sont des primitives systèmes.
+ Un sémaphore
est une généralisation du verrou. Il s'apparente à un
distributeur de jetons non renouvelables qui sont rendus après
utilisation. Lorsqu'un processus désire un jeton et qu'aucun n'est
disponible, il est mis en attente jusqu'à ce qu'un autre processus rende
son jeton.
+ Des
mécanismes plus élaborés peuvent être construits
à l'aide des sémaphores et de variables communes aux processus. Le
respect d'une règle du jeu est souvent imposé par le
système en fournissant directement ces mécanismes, comme par
exemple l'accès exclusif ou partagé à un fichier.
+ Le schéma
producteur-consommateur est un mécanisme général de
communication entre processus, dont les tubes Unix sont une implantation fournie
par le système. Les boîtes aux lettres sont aussi une implantation
de ce schéma qui tient compte de la structure des messages
échangés.
+ Le blocage des
processus en attente de ressources a des conséquences évidentes
sur les temps de réponse, et ceci ne doit pas être
négligé.
+ La
conséquence du blocage des processus est le risque d'interblocage. Ceci
se présente lorsqu'on a un ensemble de processus qui attendent des
ressources qui sont toutes possédées par des processus de
l'ensemble. Lorsqu'il survient, il est nécessaire de tuer des processus
de l'ensemble.
+ Un processus peut
également être en famine, lorsqu'il attend une ressource qu'il
n'obtient pas parce que les autres processus se coalisent pour l'en
empêcher. On l'évite par une allocation équitable.
+ La différence
entre la famine et l'interblocage est qu'un changement de comportement des
processus permettrait de satisfaire le processus en famine, alors que l'on ne
peut sortir de l'interblocage qu'en retirant autoritairement une ressource
à l'un des processus.
[1] En Français, on peut
leur donner éventuellement la signification mnémonique
Puis-je pour
P, et
Vas-y pour
V.