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.
Exclusion mutuelle
Rappelons qu'une ressource ou un ensemble de ressources doivent être mis en exclusion mutuelle dès lors que plusieurs processus veulent les utiliser simultanément, et que cette utilisation risque d'en altérer l'intégrité.
Par exemple deux processus peuvent lire en mémoire la même zone de données (pas d'interaction) mais un processus ne peut pas la lire pendant qu'un autre en modifie la valeur.
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" (voir
Définition d'un point observable).
Dans un langage d'assemblage, elle est traduite 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 .
Sections critiques
Nous avons vu au cours de l'étude des tâches en ADA que certaines parties (suites d'instructions contiguës) de programme doivent être effectuées au sein de sections critiques.
C'est le moyen de faire apparaître aux autres processus cette suite d'instructions comme atomique.
Cela permet de réaliser l'exclusion mutuelle.
Des sections critiques peuvent être réalisées, qui limitent l'utilisation de ressource(s) à un nombre maximal (? l) de processus.
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.
Dernière mise à jour : 10/10/2001