|
|
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;
}
-
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.
- 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.
- (0,5 point) Expliquez le problème et la solution que
vous
proposez ;
- Recopiez Q3-1/*.[hc]
dans Q3-2 ;
- (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 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.
- Recopiez Q3-2/*.[hc]
dans Q3-3 ;
- (1 point) Modifiez ces fichiers pour implémenter votre solution.
|