Coopération entre processus
Partie II : Lecteurs/Rédacteurs
© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 06/10/2000 -
Dernière mise à jour : 21/11/2001

Sommaire
Outils nécessaires au modèle Lecteurs/Rédacteurs
Wrappers de l'I.P.C. "Mémoire partagée"
Fonction shmget()
Fonction shmctl()
Fonctions spécifiques aux mémoires partagées
Mémoire partagée
Fichier partagé
Outils nécessaires au modèle Lecteurs/Rédacteurs
Wrappers de l'I.P.C. "Mémoire partagée"
Les wrappers des mémoires partagées sont assez analogues à ceux des sémaphores.
Fonction shmget()
C'est à la famille
xxxget()
qu'appartient la fonction système
shmget().
Deux wrappers sont nécessaires :
-
l'un pour créer la ressource (même profil que la fonction système),
-
l'autre pour utiliser la ressource lorsqu'elle existe déjà : le seul paramètre nécessaire est la clé.
Les profils des fonctions système, ainsi que le type shmid_ds sont décrits dans <sys/shm.h>.
A ajouter au fichier nsSysteme.h :
// Fonctions concernant les mémoires partagées
// ===========================================
int Shmget (::key_t key, size_t size, int shmflg)
throw (CExcFctSystIPC);
// key != IPC_PRIVATE
int Shmget (::key_t key)
throw (nsUtil::CExcFct);
|
A ajouter au fichier nsSysteme.hxx :
//============================================
// Fonctions concernant les mémoires partagées
//============================================
inline int nsSysteme::Shmget (::key_t key, size_t size, int shmflg)
throw (CExcFctSystIPC)
{
int ShmId;
if (-1 == (ShmId = ::shmget (key, size, shmflg)))
throw CExcFctSystIPC ("shmget() : pour création de shm", key, -1);
return ShmId;
} // Shmget()
|
A ajouter au fichier nsSysteme.cxx :
//============================================
// Fonctions concernant les mémoires partagées
//============================================
// key != IPC_PRIVATE
int nsSysteme::Shmget (::key_t key) throw (CExcFct)
{
if (IPC_PRIVATE == key)
throw CExcFct ("shmget() : clé IPC_PRIVATE sans droits",
CstShmgetErrArg);
int ShmId;
if (-1 == (ShmId = ::shmget (key, 0, 0)))
throw CExcFctSystIPC ("shmget() : pour shm existante", key, -1);
return ShmId;
} // Shmget()
|
La constante CstShmgetErrArg = 135 doit être ajoutée au fichier CstCodErr.h.
Fonction shmctl()
C'est à la famille
xxxctl()
qu'appartient la fonction système
shmctl().
Deux wrappers de cette fonction doivent être écrits :
-
lorsqu'elle est utilisée pour détruire un ensemble de sémaphores, son paramètre cmd doit avoir pour valeur IPC_RMID.
Les autres paramètres, semnum et arg, ne sont pas utilisés mais doivent être présents.
En fait, seul le paramètre semid est utile.
-
dans le cas général.
Il est inutile de vérifier préalablement la cohérence entre le paramètre cmd et le reste du profil : la fonction système s'en charge et échoue éventuellement en cas d'erreur, renvoyant -1 et positionnant la variable globale errno à EINVAL.
A ajouter au fichier nsSysteme.h :
|
// uniquement pour détruire la mémoire partagée
void Shmctl (int shmid)
throw (CExcFctSystIPC);
void Shmctl (int shmid, int cmd, ::shmid_ds * buf)
throw (CExcFctSystIPC);
|
A ajouter au fichier nsSysteme.hxx :
inline void nsSysteme::Shmctl (int shmid) throw (CExcFctSystIPC)
{
if (::shmctl (shmid, IPC_RMID, 0))
throw CExcFctSystIPC ("shmctl() : destruction", -1, shmid);
} // Shmctl()
inline void nsSysteme::Shmctl (int shmid, int cmd, shmid_ds * buf)
throw (CExcFctSystIPC)
{
// Inutile de tester la valeur de cmd car la fonction systeme
// fait elle-même la vérification ==> EINVAL
if (::shmctl (shmid, cmd, buf))
throw CExcFctSystIPC ("shmctl() : IPC_STAT ou IPC_SET", -1, shmid);
} // Shmctl()
|
Fonctions spécifiques aux mémoires partagées
Les mémoires partagées nécessitent deux fonctions système spécifiques :
shmat() et
shmdt().
A ajouter au fichier nsSysteme.h :
void * Shmat (int shmid, const void * shmaddr = 0, int shmflg = 0)
throw (CExcFctSystIPC);
void Shmdt (const void * addr)
throw (CExcFctSyst);
|
A ajouter au fichier nsSysteme.hxx :
inline void * nsSysteme::Shmat (int shmid, const void * shmaddr = 0,
int shmflg = 0) throw (CExcFctSystIPC)
{
void * Addr;
if (reinterpret_cast <void *> (-1) ==
(Addr = ::shmat (shmid, shmaddr, shmflg)))
throw CExcFctSystIPC ("shmat()", -1, shmid);
return Addr;
} // Shmat()
inline void nsSysteme::Shmdt (const void * addr) throw (CExcFctSyst)
{
if (::shmdt (addr)) throw CExcFctSyst ("shmdt()");
} // Shmdt()
|
Lecteurs/Rédacteurs
Le modèle
Lecteurs/Rédacteurs
nécessite d'une part un support de l'information (la ressource critique), d'autre part les mécanismes permettant d'en protéger l'accès.
Unix nous permet d'envisager deux cas possibles :
-
l'information est stockée en mémoire partagée
(shared memory).
Les principaux avantages sont :
-
accès pratiquement immédiat à l'information
Les principaux inconvénients sont :
-
non persistence des données : lorsque le système reboote,
elles sont perdues
-
la mémoire partagée n'intègre pas les mécanismes
de protection d'accès,
-
l'unité d'allocation de la mémoire partagée (on dit un segment de mémoire partagée) est la taille d'une page physique, en général 4096 octets (granule).
Pour stocker la plus petite information il faut donc réserver une page entière.
Pour éviter un important gaspillage, il est donc nécessaire de stocker dans la même page des informations n'ayant aucun rapport entre elles.
La gestion en devient difficile, et chacune nécessite d'avoir ses propres mécanismes de protection d'accès, qui sont les sémaphores du système.
Ceux-ci sont en nombre réduit et peuvent devenir un facteur limitant à l'application.
Inversement, une information volumineuse peut ne pas être totalement contenue dans une page physique, et le système doit alors trouver plusieurs pages contiguës, ou utiliser un mécanisme d'adressage
complexe.
-
l'information est stockée sur disque
Les principaux avantages sont :
-
la persistence des données
-
les fichiers possèdent leur propre mécanisme de protection d'accès, qui ne représente plus une limite pour l'application
-
la taille de l'information est quelconque, seulement limitée par la capacité du support
Les principaux inconvénients sont :
-
accès très lent à l'information
Les deux premiers exercices permettent de se familiariser avec la création de segments de mémoire partagée.
Le troisième permet de mettre en oeuvre une implémentation simple du modèle des lecteurs/rédacteurs en utilisant la mémoire partagée.
Le dernier exercice du paragraphe reprend le même problème avec une implémentation au moyen d'un fichier, et fait apparaître les différences (avantages et inconvénients) des deux solutions.
Mémoire partagée
Accès concurrent à une variable en mémoire partagée
exo_01a
-
Liste des fichiers créés ou modifiés : exo_01a.cxx
Cet exercice est destiné à montrer la concurrence de plusieurs processus partageant l'accès à une variable placée en mémoire partagée.
Il s'agit d'un exemple qui a été étudié en première année, dans le cadre de la concurrence de tâches en Ada 95 : m processus incrémentent n fois une variable Var préalablement initialisée, pendant que m autres processus la décrémentent le même nombre n de fois.
Après la terminaison de ces 2m processus, la valeur finale de la variable partagée est comparée avec sa valeur initiale.
Dans le fichier exo_01a.cxx, écrire dans l'espace de noms anonyme :
-
une fonction FilsPlus() à laquelle on passe :
-
la variable entière Var passée en paramètre donnée-résultat,
-
le nombre n de fois qu'elle doit l'incrémenter.
-
une fonction FilsMoins() à laquelle on passe :
-
la variable entière Var passée en paramètre donnée-résultat,
-
le nombre n de fois qu'elle doit la décrémenter.
Dans la fonction ppal() :
-
créer un segment de mémoire partagée (Shmget()), de taille suffisante pour stocker une variable entière, de clé passée en premier argument de la commande,
-
associer le segment à une variable entière (Shmat()),
-
initialiser la variable à une valeur passée en deuxième argument de la commande,
-
créer m fils (m passé en troisième argument de la commande) qui exécutent la fonction FilsPlus(),
-
créer m fils qui exécutent la fonction FilsMoins(),
-
attendre proprement la fin de tous les fils,
-
afficher les valeurs intiale et finale de la variable,
-
libérer la ressource (Shmctl()).
Chaque fils qui se termine doit préalablement se détacher de la mémoire partagée (Shmdt()).
Compiler et tester en essayant plusieurs valeurs des différents arguments.
On constate en général que les valeurs intiale et finale de la variable sont différentes, de façon non reproductible.
Cela est dû au fait que les incrémentations/décrémentations, bien que très rapides, ne sont pas atomiques et peuvent être interrompues par le scheduler.
Corrigé :
exo_01a.cxx
Sommaire
Accès
protégé à une variable en mémoire partagée
exo_01b
-
Liste des fichiers créés ou modifiés : exo_01b.cxx
Recopier le fichier exo_01a.cxx dans exo_01b.cxx.
Ajouter un mutex, modifier les fonctions FilsPlus() et FilsMoins().
Compiler et tester en essayant plusieurs valeurs des différents arguments.
Sauf erreur, les valeurs intiale et finale de la variable sont toujours égales.
Corrigé :
exo_01b.cxx
Sommaire
Vecteur en mémoire partagée
exo_02
-
Liste des fichiers créés ou modifiés :
exo_02d.cxx,
CVPartage.h,
CVPartage.hxx,
CVPartage.cxx,
ShmLecteur.cxx,ShmRedacteur.cxx,
Makefile,
ScriptShLR
Les objets protégés n'existent ni en C++, ni en système.
Il est cependant possible, et relativement facile, de les réaliser au moyen des mécanismes de base que sont les I.P.C.s.
L'objectif de cet exercice est de donner un modèle de réalisation, qui peut éventuellement être utilisé tel quel ou servir de base à un développement ultérieur.
Il s'agit de permettre à l'utilisateur de placer un vecteur de 10 entiers en mémoire partagée, accessible par un ensemble de lecteurs et de rédacteurs.
Plusieurs lecteurs peuvent lire simultanément le vecteur en l'absence de rédacteurs, mais un seul rédacteur peut écrire dans le vecteur en l'absence de toute autre activité, lecteur ou rédacteur.
La solution optimale consisterait à protéger individuellement chaque élément du vecteur : l'élément i pourrait être consulté par un nombre quelconque de lecteurs pendant que l'élément j serait modifié par un seul rédacteur.
Malheureusement, le nombre de sémaphores proposés par Unix est beaucoup trop limité pour envisager cette solution.
Pendant ces différentes activités, c'est donc la totalité du vecteur qui sera mise en exclusion mutuelle.
Afin de simplifier la tâche du développeur des applications Lecteur et Redacteur, dont la structure générale est présentée dans le cours concernant les sémaphores, une classe qui gère le vecteur doit être mise à sa disposition (un seul objet de cette classe sera instancié).
Cette classe offre les primitives DebutLire(), FinLire(), DebutEcrire() et FinEcrire(), éventuellement bloquantes, qui permettent ou non l'accès au vecteur en lecture ou en écriture.
Ces primitives sont destinées à masquer le mécanisme interne de bloquage/débloquage.
De plus elle offre les primitives GetElem() et SetElem() qui permettent l'accès à un élément donné du vecteur.
Dans les fichiers CVPartage.h et CVPartage.hxx (les fonctions sont courtes, sauf le constructeur, mais qui est appelé rarement : une seule fois par processus, en principe), écrire la classe CVPartage ayant les caractéristiques suivantes ;
-
une donnée membre m_ShmId représentant l'identificateur de la mémoire paragée permettant de stocker le vecteur de 10 entiers,
-
une donnée membre m_SemId représentant l'identificateur
d'un ensemble de deux sémaphores (un pour le nombre de lecteurs
et un pour le nombre de rédacteurs) destinés à gérer
l'accès au vecteur,
-
une donnée membre stockant le pid du processus qui a créé
les ressources I.P.C.,
-
un pointeur vers le vecteur d'entiers qui sera en mémoire partagée
-
un constructeur ayant pour paramètre la clé servant à
acquérir le segment de mémoire partagée et l'ensemble
de sémaphore,
-
un destructeur,
-
l'accesseur GetElem() ayant pour paramètre le rang de l'élément accédé,
et le modifieur SetElem()
-
le modifieur SetElem() ayant pour paramètres le rang de l'élément accédé et la valeur à affecter,
-
les primitives DebutLire(), FinLire(), DebutEcrire()
et FinEcrire().
C'est au constructeur qui réussit à créer le segment de mémoire partagée d'initialiser le vecteur à 0, de créer et d'initialiser correctement l'ensemble de sémaphores.
Dans le fichier ShmLecteur.cxx, écrire la fonction ppal() qui :
-
déclare un objet de la classe CVPartage, (clé passée en argument 1 de la commande),
-
récupère le nom du processus (passé en argument 2 de la commande),
-
attend un délai en secondes (argument 3 de la commande),
-
demande l'accès en lecture,
-
lit les cinq premiers éléments du vecteur avec une temporisation en secondes, passée en argument 4 de la commande (lire plus de cinq éléments prendrait trop de temps, et une temporisation d'une ou 2 secondes est suffisante),
-
rend l'accès en lecture.
L'entrée et la sortie de chacune de ces étapes
sont signalées à l'écran.
Dans le fichier ShmRedacteur.cxx, écrire la fonction ppal() qui :
-
déclare un objet de la classe CVPartage, (clé passée en argument 1 de la commande),
-
récupère le nom du processus (passé en argument 2 de la commande),
-
attend un délai (argument 3 de la commande),
-
demande l'accès en écriture,
-
écrit les cinq premiers éléments du vecteur avec une temporisation en secondes, passée en argument 4 de la commande (même remarque que pour les lecteurs),
-
rend l'accès en écriture.
L'entrée et la sortie de chacune de ces étapes
sont
signalées à l'écran.
Dans le fichier exo_02d.cxx, écrire un programme qui :
-
se transforme en démon,
-
déclare un objet de la classe CVPartage, (clé passée en argument 1 de la commande),
-
crée un script permettant de le réveiller,
-
s'endort dans une pause(),
-
etc...
Modifier le fichier Makefile pour qu'il puisse compiler exo_02d.cxx, ShmLecteur.cxx, ShmRedacteur.cxx et CVPartage.cxx, en ajoutant les dépendances relatives à CVPartage.h et CVPartage.hxx.
Compiler.
Tester séparément un lecteur, puis un rédacteur (bien entendu avec le démon), puis écrire le script qui lance le programme Chrono, plusieurs lecteurs et rédacteurs, en ayant soigneusement choisi les différents délais pour montrer que l'ensemble fonctionne correctement.
Rediriger le chronogramme sur un fichier et le reconstituer sur papier.
Vérifier qu'il est correct.
Remarques :
-
Si on fait confiance aux procédures GetElem() et SetElem(),
il est inutile d'afficher le contenu des valeurs lues.
-
Les problèmes autres que ceux engendrés par la manipulation
des IPCs sont ignorés (indice invalide, boucle infinie dans une
section critique, etc...)
Corrigé :
exo_02d.cxx
-
CVPartage.h
-
CVPartage.hxx
-
CVPartage.cxx
-
ShmLecteur.cxx
-
ShmRedacteur.cxx
-
Makefile
-
ScriptShLR
Sommaire
Vecteur générique en mémoire partagée
exo_03 (facultatif)
-
Liste des fichiers créés ou modifiés :
exo_03d.cxx, CShVector.h, CShVector.hxx, ShmL_03.cxx, ShmR_03.cxx, Makefile
Il est dommage d'avoir créé une classe pour un seul objet.
Il est très facile de généraliser cette classe, grâce à la généricité.
Recopier les fichiers CVPartage.h, CVPartage.hxx et CVPartage.cxx dans CShVector.h et CShVector.hxx.
Remplacer le type int par le type générique T, ajouter une donnée membre représentant la taille du vecteur, passée en paramètre du constructeur (avec valeur par défaut).
Remplacer (éventuellement) l'accesseur GetElem() et le modifieur SetElem() par les surcharges de l'opérateur [], renvoyant respectivement l'adresse de l'objet constant (const T &) et l'adresse de l'objet (T &).
Modifier l'initialisation du vecteur en appelant le constructeur par défaut du type T.
Les seules modifications des lecteurs (fichier ShmL_03.cxx), des rédacteurs (fichier ShmR_03.cxx) et du démon (fichier exo_03d.cxx) consistent à instancier l'objet générique et à remplacer les accesseur/modifieur par l'opérateur[].
Modifier le fichier Makefile.
Compiler et tester avec un script semblable au précédent.
Corrigé :
exo_03d.cxx
  -
CShVector.h
  -
CShVector.hxx
  -
ShmL_03.cxx
  -
ShmR_03.cxx
  -
Makefile
Sommaire
Fichier partagé
Vecteur sur fichier
à accès global protégé
exo_04
-
Utilisation de la fonction flock()
-
Partage d'information sur fichier
-
Liste des fichiers créés ou modifiés :
exo_04d.cxx,
CVLock.h,
CVLock.hxx,
CVLock.cxx,
LockLecteur.cxx,
LockRedacteur.cxx,
Makefile,
ScriptLockLR
Cet exercice reprend le principe des exercices précédents, mais place les données partagées dans un fichier.
Grâce à la fonction flock(), les mécanismes de synchronisation sont directement intégrés au fichier et ne nécessitent pas de ressources supplémentaires (ni mémoire partagée, ni sémaphores).
Très peu de modifications sont nécessaires puisque la fonction flock() verrouille la totalité du fichier.
Ajouter à nsSysteme le wrapper de la fonction flock().
Recopier les fichiers CVPartage.h et CVPartage.hxx dans les fichiers CVLock.h, CVLock.hxx et CVLock.cxx en répartissant dans ces deux derniers les fonctions selon leur longueur.
Remplacer les données membres m_ShmId et m_SemId par le nom et le file descriptor du fichier contenant le vecteur de 10 entiers : m_FileName et m_fd.
Le propriétaire du fichier est le processus qui le crée (O_EXCL | O_CREAT).
C'est lui qui devra le détruire (Unlink()).
Toutes les fonctions membres sont susceptibles de lever l'exception CExcFctSystFile.
Les fonctions GetElem() et SetElem() se positionnent dans le fichier grâce à la fonction Lseek() et lisent ou écrivent un entier grâce à Read() ou Write().
Les fonctions d'accès en lecture/écriture utilisent la fonction Flock().
Recopier les fichiers exo_02d.cxx, ShmLecteur.cxx et ShmRedacteur.cxx dans exo_04d.cxx, LockLecteur.cxx et LockRedacteur.cxx.
Effectuer les quelques modifications nécessaires.
Recopier le fichier ScriptShLR dans ScriptLockLR, et effectuer les quelques modifications nécessaires.
Effectuer les quelques modifications nécessaires dans le fichier Makefile.
Compiler et tester.
Le fonctionnement doit être identique à celui de exo_02 et exo_03.
Corrigé :
exo_04d.cxx
-
CVLock.h
-
CVLock.hxx
-
CVLock.cxx
-
LockLecteur.cxx
-
LockRedacteur.cxx
-
Makefile
-
ScriptLockLR
Sommaire
Vecteur sur fichier à accès élémentaire protégé
exo_05
-
Utilisation de la fonction fcntl() avec verrouillage de fichier
-
Liste des fichiers créés ou modifiés :
exo_05d.cxx,
CVFcntl.h,
CVFcntl.hxx,
CVFcntl.cxx,
FcntlLecteur.cxx,
FcntlRedacteur.cxx,
Makefile,
ScriptFcntlLR.
La fonction
fcntl()
est un peu plus difficile à utiliser mais beaucoup plus puissante que la fonction flock() puisque, contrairement à cette dernière qui bloque tout le fichier, elle ne bloque (en lecture ou en écriture) qu'une partie du fichier définie par :
-
une position de départ,
-
b
un nombre d'octets.
Tout se passe donc comme si chaque octet ou groupe d'octets pouvaient avoir un sémaphore.
Il est alors inutile d'écrire des fonctions d'accès protégé pour commencer la lecture ou l'écriture.
Recopier les fichiers exo_04d.cxx, CVLock.h, CVLock.hxx, CVLock.cxx, LockLecteur.cxx, LockRedacteur.cxx et ScriptLockLR respectivement dans exo_05d.cxx, CVFcntl.h, CVFcntl.hxx, CVFcntl.cxx, FcntlLecteur.cxx, FcntlRedacteur.cxx et ScriptFcntlLR.
Le constructeur et le destructeur de CVFcntl n'ont pas à être modifiés.
En revanche, il faut ajouter un paramètre aux quatre fonctions DebutLire(), FinLire(), DebutEcrire() et FinEcrire(), indiquant le rang de l'élément auquel on accède.
En effet, c'est seulement lui qui doit être verrouillé ou libéré, en lecture ou en écriture.
De plus, puisque chaque acccès doit être précédée d'un verrouillage d'une partie du fichier, il est conseillé d'incorporer le positionnement (Lseek()) à ce verrouillage.
Modifier le lecteur et le rédacteur pour (dé)verrouiller chaque élément à l'intérieur de la boucle.
Effectuer les quelques modifications nécessaires dans le fichier Makefile.
Compiler et tester.
Corrigé :
exo_05d.cxx
-
CVFcntl.h
-
CVFcntl.hxx
-
CVFcntl.cxx
-
FcntlLecteur.cxx
-
FcntlRedacteur.cxx
-
Makefile
-
ScriptFcntlLR
Sommaire
Téléchargement des corrigés :
tplectredact.zip
Chemins d'accès aux sources des corrigés :
~mathieu/PARTAGE/src/tp/tpsys/tplectredact/dirlectredact
~mathieu/PARTAGE/src/tp/tpsys/tplectredact/util
~mathieu/PARTAGE/src/tp/tpsys/tplectredact/include
© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique