Département INFormatique 
  CSC4508/M2 : Concepts des systèmes d'exploitation et mise en œuvre sous Unix


    Évaluation



TÉLÉCOM & Management SudParis
TÉLÉCOM SudParis 2ème année

TP Noté CSC4508/M2 du 21/05/10

Modalités

Durée : 3 heures

Tous documents autorisés.

Les questions 1, 2 et 3 sont indépendantes les unes des autres. Aussi, n'hésitez pas à lire tout le sujet avant de commencer pour déterminer l'ordre dans lequel vous souhaitez traiter les questions.

Le barème est donné à titre indicatif des poids entre les différentes questions.

La « livraison » de votre travail en fin de TP noté se fera par remise de votre copie à l'enseignant et par remontée sous Moodle (rubrique « TP noté de 3 heures ») du fichier d'extension tgz constitué de la manière suivante :
cd votreRepertoireDeTravailPourCSC4508M2
tar cvfz $USER.tgz TPNote2010

Préparation

cd votreRepertoireDeTravailPourCSC4508M2
cp ~simatic/Cours/CSC4508/tPNote2010.tgz .
tar xvfz tPNote2010.tgz
cd TPNote2010

Question 1 : Questions de cours (7 points)

Pour chacune des questions ci-dessous, répondez sur votre copie ou bien dans le fichier Q1/reponse1.txt (dans ce dernier cas, indiquez sur votre copie « Réponse dans Q1/reponse1.txt »).

Question 1.1 : Avant l'heure, ce n'est pas l'heure. Après l'heure, ce n'est pas l'heure non plus. (1,5 point)

On considère une application constituée de 4 processus. 3 processus (notés P1, P2 et P3) s'exécutent en permanence. 1 processus (noté P0) est chargé d'exécuter un bref traitement chaque seconde. Le traitement de P0 doit s'exécuter chaque seconde précisément. S'il s'écoule 950 millisecondes ou bien 1.050 millisecondes entre deux exécutions de P0, l'application ne rend pas correctement son service.
Le processus P0 est actuellement codé selon l'algorithme suivant :
dureeAttente = 1.000.000 micro-secondes
while (1) {
usleep(dureeAttente)
debut = mesureDeLHeure()
traitement()
fin = mesureDeLHeure()
dureeAttente = 1.000.000 - (fin - debut)
}

À l'exécution, il apparaît que l'exécution de P0 ne se fait pas chaque seconde précisément. En effet, quand P0 est prêt à s'exécuter (il a terminé l'instruction usleep), P1, P2 et P3 qui exécutent des calculs CPU intensif ne lui donnent pas tout de suite la main.
Quelle solution proposez-vous ? Justifiez.

Question 1.2 : Lecture de structures sans bavures... (1,5 point)

Une application a écrit 24 octets dans le fichier Q1/tableauEvenements.bin afin d'archiver un tableau de 8 événements. Chaque événement est codé à l'aide de 3 octets : le premier octet correspond au type de l'événement, les deux suivants forment un short correspondant à la date (exprimée en nombre de secondes écoulées depuis une certaine origine) de l'événement.
La commande od -t u1 Q1/tableauEvenements.bin permet de lister les octets contenus dans Q1/tableauEvenements.bin qui vous est fourni :
0000000   8 240   0   7 224   1   6 208   2   5 192   3   4 176   4   3
0000020 160   5   2 144   6   1 128   7
0000030


Le fichier Q1/tableauEvenements.bin contient donc le tableau d'événements suivant :
Rang (dans le tableau) de l'événement Type Date
0 8 L'octet de poids faible (respectivement fort) du short correspondant à la date de l'événement a pour valeur 240 (respectivement 0). Donc le short a pour valeur :  240 + 0 x 256 = 240 secondes
1 7 224 + 1 x 256 = 480
... ... ...

Dans cette question, on considère l'application Q1/lireTableauEvenements chargée de lire et afficher le tableau stocké dans tableauEvenements.bin. À l'exécution, cette application affiche :
Element numero : 0 | Type : 8 | Date : 1792
Element numero : 1 | Type : -32 | Date : -12282
Element numero : 2 | Type : 2 | Date :  960
Element numero : 3 | Type : 4 | Date :  772
Element numero : 4 | Type : -96 | Date : -28670
Element numero : 5 | Type : 6 | Date : 1920
Pb dans read: Success

qui n'est pas l'affichage attendu.

Modifiez Q1/lireTableauEvenements.c pour qu'il lise (et affiche donc) correctement le tableau stocké dans Q1/tableauEvenements.bin. Vous mettrez en commentaire de votre modification une explication de l'origine du problème.
Indication : un printf de la taille de la structure tableEvenement peut vous aider dans la résolution de cette question.

Question 1.3 : Parallélisation d'un algorithme de recherche d'un minimum (4 points)

Une application a besoin de rechercher la plus petite valeur dans un tableau d'entiers.
Comme ce tableau n'est pas trié, on est obligé de faire une recherche séquentielle. rechercheMin0 est une version non-parallélisée de ce code :
/* rechercheMin0.c */
int rechercheMin0(int tab[], int taille){
  int min, i;
  min = tab[0];
  for (i = 1; i < taille; i++) {
    if (min > tab[i]) {
      min = tab[i];
    }
  }
  return min;
}

Cette application va s'exécuter sur un processeur quadri-cœur. On décide d'exploiter ces cœurs en utilisant 4 threads, chacun recherchant le minimum sur un sous-ensemble du tableau.

L'objectif de cet exercice est d'étudier 3 implémentations (incorrectes) de cette parallélisation de la recherche du minimum :
  • rechercheMin1.c
    /* rechercheMin1.c */
    #include <pthread.h>
    #include <limits.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define NB_THREADS 4

    int *theTab;
    int theTaille;

    void *rechercheDansSousTableau(void *arg){
      int iDepart = (*(int*)arg)*theTaille/NB_THREADS;
      int min, i;
      min = theTab[iDepart];
      for (i=iDepart+1; i<iDepart+theTaille/NB_THREADS; i++) {
        if (min > theTab[i]) {
          min = theTab[i];
        }
      }
      pthread_exit((void*)&min);
    }

    int rechercheMin1(int tab[], int taille){
      int min=INT_MAX, i, rc;
      pthread_t thread[NB_THREADS];

      theTab = tab;
      theTaille = taille;
      for (i = 0; i < NB_THREADS; i++) {
        rc = pthread_create(&thread[i], NULL, rechercheDansSousTableau, (void*)(&i));
        if (rc != 0){
          perror("pthread_create");
          exit(EXIT_FAILURE);
        }
      }
      for (i = 0; i < NB_THREADS; i++) {
        int *pThreadMin;
        rc = pthread_join(thread[i], (void*)(&pThreadMin));
        if (rc != 0){
          perror("pthread_join");
          exit(EXIT_FAILURE);
        }
        if (min > *pThreadMin){
          min = *pThreadMin;
        }
      }
      return min;
    }
  • rechercheMin2.c
    /* rechercheMin2.c */
    #include <pthread.h>
    #include <limits.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define NB_THREADS 4

    int *theTab;
    int theTaille;
    int theMin;

    void *rechercheDansSousTableau(void *arg){
      int iDepart = ((int)arg)*theTaille/NB_THREADS;
      int min, i;
      for (i=iDepart; i<iDepart+theTaille/NB_THREADS; i++) {
        if (theMin > theTab[i]) {
          theMin = theTab[i];
        }
      }
      pthread_exit(NULL);
    }

    int rechercheMin2(int tab[], int taille){
      int i, rc;
      pthread_t thread[NB_THREADS];

      theTab = tab;
      theTaille = taille;
      theMin = INT_MAX;
      for (i = 0; i < NB_THREADS; i++) {
        rc = pthread_create(&thread[i], NULL, rechercheDansSousTableau, (void*)i);
        if (rc != 0){
          perror("pthread_create");
          exit(EXIT_FAILURE);
        }
      }
      for (i = 0; i < NB_THREADS; i++) {
        rc = pthread_join(thread[i], NULL);
        if (rc != 0){
          perror("pthread_join");
          exit(EXIT_FAILURE);
        }
      }
      return theMin;
    }
  • rechercheMin3.c
    /* rechercheMin3.c */
    #include <pthread.h>
    #include <limits.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define NB_THREADS 4

    int *theTab;
    int theTaille;
    int theMin;
    pthread_mutex_t mutex;

    void *rechercheDansSousTableau(void *arg){
      int iDepart = ((int)arg)*theTaille/NB_THREADS;
      int min, i;
      int rc;
      for (i=iDepart; i<iDepart+theTaille/NB_THREADS; i++) {
        if (theMin > theTab[i]) {
          rc = pthread_mutex_lock(&mutex);
          assert(rc == 0);
          theMin = theTab[i];
          rc = pthread_mutex_unlock(&mutex);
          assert(rc == 0);
        }
      }
      pthread_exit(NULL);
    }

    int rechercheMin3(int tab[], int taille){
      int i, rc;
      pthread_t thread[NB_THREADS];

      theTab = tab;
      theTaille = taille;
      theMin = INT_MAX;
      pthread_mutex_init(&mutex,NULL);
      for (i = 0; i < NB_THREADS; i++) {
        rc = pthread_create(&thread[i], NULL, rechercheDansSousTableau, (void*)i);
        if (rc != 0){
          perror("pthread_create");
          exit(EXIT_FAILURE);
        }
      }
      for (i = 0; i < NB_THREADS; i++) {
        rc = pthread_join(thread[i], NULL);
        if (rc != 0){
          perror("pthread_join");
          exit(EXIT_FAILURE);
        }
      }
      return theMin;
    }
  1. Dans le cas où l'application fait un seul appel à la fonction rechercheMin pendant son exécution, expliquez pourquoi chacune de ces trois implémentations risque de ne pas trouver la valeur minimum du tableau.
  2. Supposons que les problèmes identifiés précédemment sont corrigés. On considère maintenant une application constituées de deux threads qui, au même moment, appellent la fonction rechercheMin pendant leur exécution, mais avec des tableaux différents en paramètre. Expliquez pourquoi le résultat de l'une des exécutions de rechercheMin sera incorrect.

Question 2 : Conception de deux nouveaux outils de synchronisation (7 points)

L'objectif de cette question est de concevoir deux outils de synchronisation qui ont été intégrés à Java 5 : le Loquet et le CalculAsynchrone.
Pour chacune des questions ci-dessous, répondez sur votre copie ou bien dans le fichier Q2/reponse2.txt (dans ce dernier cas, indiquez sur votre copie « Réponse dans Q2/reponse2.txt »).

Question 2.1 : Conception de l'outil de synchronisation Loquet (4,5 points)

Un Loquet est un outil de synchronisation permettant de retarder l'exécution de threads tant que le Loquet n'est pas dans son état terminal. Un Loquet agit comme une porte : tant qu'il n'est pas dans son état terminal, la porte est fermée et aucun thread ne peut passer. Dans son état terminal, la porte s'ouvre et tous les threads peuvent entrer. Quand un Loquet est dans son état terminal, il ne peut changer d'état et reste ouvert à jamais.
Le Loquet permet donc qu'un ou plusieurs threads attendent qu'un ensemble d'événements se produisent. L'état du Loquet est formé d'un compteur initialisé (au moment de la création du Loquet avec la fonction nouveauLoquet()) avec un nombre positif qui représente le nombre d'événements à attendre. La fonction decrementLoquet() décrémente ce compteur pour indiquer qu'un événement est survenu. La fonction attenteLoquet() attend que le compteur passe à zéro, ce qui signifie que tous les événements se sont passés. Si le compteur est non nul lorsqu'elle est appelée, attenteLoquet() se bloque jusqu'à ce qu'il atteigne zéro. Si le compteur est nul, attenteLoquet() retourne immédiatement.
Indiquez de quel paradigme de synchronisation le Loquet se rapproche. Justifiez.
Écrivez, à l'aide de sémaphores (pour lesquels vous indiquerez les valeurs initiales), les algorithmes des fonctions :
  • nouveauLoquet(int valeurCompteur)
  • decrementLoquet()
  • attenteLoquet().

Question 2.2 : Application du Loquet : l'outil de synchronisation "CalculAsynchrone" (2,5 points)

L'outil de synchronisation CalculAsynchrone permet à un thread de créer un autre thread Ts chargé de calculer un résultat (nécessitant en général un temps de calcul long). Pendant que ce thread Ts s'exécute, l'outil de synchronisation CalculAsynchrone est dans l'état EN_COURS_D_EXECUTION. Une fois le calcul lancé, d'autres threads peuvent invoquer la fonction obtenirResultat() sur ce CalculAsynchrone. Le comportement de obtenirResultat() dépend de l'état du thread Ts. Si Ts est terminé (l'outil de synchronisation CalculAsynchrone est dans l'état FINI), obtenirResultat() renvoie immédiatement le résultat que Ts a calculé. Sinon obtenirResultat() se bloque jusqu'à ce que Ts ait calculé le résultat, puis renvoie le résultat.
Écrivez, à l'aide des outils de synchronisation qui vous semblent adaptés (en indiquant leur valeur initiale) :
  • les algorithmes des fonctions
    • nouveauCalculAsynchrone(pointeurSurFonctionDeCalculRenvoyantUnResultat) (où pointeurSurFonctiondeCalculRenvoyantUnResultat est un pointeur sur une fonction de calcul qui renvoie un résultat de type Résultat),
    • obtenirResultat() qui renvoie une valeur de type Resultat,
    • etatCalculAsynchrone() qui renvoie EN_COURS_D_EXECUTION ou FINI,
  • l'algorithme du thread Ts.

Question 3 : Appel procédural entre processus (6 points)

Dans cette question, on se propose de coder un appel procédural (cf. cours sur la synchronisation entre processus) entre un processus client et un processus serveur.
Le processus client invoque une procédure en fournissant un paramètre donnée qui est un entier. Le processus serveur répond à cette invocation en fournissant un paramètre résultat (qui, dans le cadre de cet exercice, est le double du paramètre donnée).
Pour chacune des questions ci-dessous, répondez sur votre copie ou bien dans le fichier Q3/reponse3.txt (dans ce dernier cas, indiquez sur votre copie « Réponse dans Q3/reponse3.txt »).

Question 3-1 : Implémentation basique (3 points)

Le principe de cette implémentation basique est que le processus serveur crée une zone de mémoire partagée dans laquelle le processus client (respectivement serveur) stocke son paramètre donnée (respectivement résultat). Le processus serveur crée aussi les sémaphores nécessaires à la synchronisation entre ces deux processus.
Complétez toutes les sections « /* A vous de completer */ » des fichiers client.c, serveur.c et client-serveur.h du répertoire Q3-1 pour mettre en œuvre cette implémentation basique.

Question 3-2 : Gestion de multiples processus client (1 point)

L'implémentation précédente ne traite pas correctement le cas où plusieurs processus client font simultanément un appel procédural vers le processus serveur.
  1. (0,5 point) Expliquez le problème et la solution que vous proposez ;
  2. Recopiez Q3-1/*.[hc] dans Q3-2 ;
  3. (0,5 point) Modifiez ces fichiers de manière à gérer correctement de multiples processus client.

Question 3-3 : Processus serveur multi-threadé (2 points)

On souhaite rendre le processus serveur multi-threadé pour qu'il puisse, grâce à 4 threads, traiter plusieurs appels procéduraux en parallèle.
Les implémentations réalisées dans les questions précédentes ne répondent pas à ce besoin.
  1. (1 point) Décrivez les grandes lignes de la solution qui vous semble la plus adaptée. NB : cette solution peut ou peut ne pas utiliser de la mémoire partagée et/ou des sémaphores.
    Veillez à expliquer vos motivations pour le choix de cette solution.
  2. Recopiez Q3-2/*.[hc] dans Q3-3 ;
  3. (1 point) Modifiez ces fichiers pour implémenter votre solution.



Page mise à jour le 21 juin 2010