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
Accès concurrent à une variable en mémoire partagée exo_01a
Accès protégé à une variable en mémoire partagée exo_01b
Vecteur en mémoire partagée exo_02
Vecteur générique en mémoire partagée exo_03
Fichier partagé
Vecteur sur fichier à accès global protégé exo_04
Vecteur sur fichier à accès élémentaire protégé exo_05

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 :     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 :

    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 :     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
    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 :

    Dans la fonction ppal() :     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
    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
    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 ;

    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 :

    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 :

    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 :

    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 :

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)
    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
    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
    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 :     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