|
|
Institut National des Télécommunications
Télécom INT 2è année
TP Noté CS21 du 22/06/04
(2è session)
(Corrigés)
Modalités
Durée : 1 heure 30
Tous documents autorisés
Les questions sont indépendantes les unes des autres.
Le barême est donné à titre indicatif des poids entre les différentes
questions. Seront notés :
- La clarté du code,
- Le fait que le code compile (sans warning),
- Le fait que le code répond correctement à la question posée,
- Le fait que le retour de chaque appel système, s'il y en a un,
est testé et géré (traitement d'erreur)
,
- Le fait que les ressources système réservées pendant l'exécution
du programme sont libérées à la fin de l'exécution.
La «livraison» de votre travail en fin de TP noté se fera par remise de
votre copie à l'enseignant et par
envoi d'un mail à frederique.silber-chaussumier@int-evry.fr
(sujet «CS21:TPNoteSession2») avec en pièce jointe le fichier
d'extension
tgz constitué de la manière suivante :
cd votreRepertoireDeTravailPourCS21 tar cvfz $USER.tgz TPNote2004Session2
Préparation
Question 1 : O_SYNC (4 points)
man 2 open décrit le rôle de l'option O_SYNC
dans l'appel système open .
Q1.1 : Pourquoi n'utilise-t-on pas cette option en général ?
---beginCorr
Avec cette option, chaque écriture sur le fichier donne lieu à une
écriture immédiate sur disque : on ne profite pas des regroupements
possibles d'écriture sur disque. Les performances sont donc mauvaises
---endCorr
Q1.2 : Donner un exemple d'application où cette option est requise.
---beginCorr
Toute application utilisant une base de données où le programme indique
à la base de données que les données doivent être commitées sur disque : ces données
doivent
donc être effectivement présentes sur disque (principe de Durabilité des bases de données)
même
si la machine plante quelques instants après (et donc ses caches disque
sont
perdus). Les écritures sur disque doivent donc être synchrones avec les
appels
système write .
NB : des mécanismes spécifiques (journalisation) sont mis en oeuvre
dans les bases de données pour que cet aspect synchrone ne pénalise pas
trop les performances de la base de données.
---endCorr
NB : Vous pouvez répondre sur votre copie ou bien dans le fichier Q1/reponse.txt
(dans ce dernier cas, indiquez sur votre copie "Réponse dans Q1/reponse.txt ").
Question 2 : Concurrence d'accès (8 points)
Le module suivant est utilisé pour gérer une liste chaînée de données.
#include <stdlib.h>
/* Structure utilisee pour stocker
les
differents elements de la liste */
typedef struct element_t{
/* Donnees stockees dans cet
element de la liste chainee */
char data[256];
/* Pointeur vers l'element suivant
de la liste chainee (NULL si pas */
/* d'element suivant)
*/
struct element_t *next;
} element_t;
/* Pointeur sur le premier element de la liste
chainee
*/
static element_t *first=NULL;
/* Ajoute l'element unElement en tete de liste chainee */
void addFirst(element_t *unElement){
/* Comme unElement va etre insere en tete de liste,
*/
/* son successeur sera le premier element actuel de la
liste */
unElement->next = first;
/* La nouvelle tete de liste est unElement */
first = unElement;
}
Dans un programme mono-thread,
ce module donne entière satisfaction.
En revanche, dans le cas d'un programme contenant deux threads (qui appellent à certains
moments la fonction addFirst ), chacun ayant une politique
d'ordonnancement FIFO, l'un
ayant une priorité statique égale à 1, l'autre ayant une priorité
statique égale à 2, on constate qu'addFirst perd des
éléments.
Q2.1 : Pourquoi ?
---beginCorr
Soit thread1
(respectivement thread2 ) le thread de
priorité 1 (respectivement 2).
- Supposons que
thread1 appelle addFirst
avec l'élément element1 en paramètre : thread1
exécute la première instruction d'addFirst . Il fait donc element1->next
= first;
- Supposons maintenant que
thread2 se
réveille juste à ce moment-là (par exemple, l'entrée-sortie qu'il avait
en cours
s'est terminée). Comme thread2 a une priorité
statique supérieure à celle de thread1 , thread2
prend la main. S'il appelle addFirst avec l'élément element2 ,
il déroule tout le code d'addFirst . Il effectue donc : element2->next=first
et first=element2
- Quand
thread2 relâche la main (par
exemple, pour faire une autre entrée-sortie), thread1
reprend la main et termine son exécution d'addFirst . Il
fait donc first=element1 .
On constate qu'element2 est perdu.
---endCorr
Q2.2 : Aurait-on ce bug si les deux threads avaient la même priorité
statique (comprise entre 1 et 99) ? Justifier votre réponse.
---beginCorr
Non. En effet, même si thread2 se réveille, il
ne prend pas la main dans la mesure où il n'a pas une priorité
supérieure
à celle de thread1 : thread1
ne peut plus être interrompu pendant son exécution d'addFirst
(en effet, la politique d'ordonnancement de ces deux threads est FIFO :
thread1 ne relâchera la main que
volontairement).
---endCorr
Q2.3 : Proposer, dans ses grandes lignes, une solution pour le bug d'addFirst
quand les deux threads ont une priorité statique différente.
---beginCorr
Il suffit de rajouter un sémaphore pour garantir l'exclusion mutuelle
dans la manipulation de la liste chaînée :
/* on suppose que mutex est un semaphore initialise à 1
*/
/* Ajoute l'element unElement en tete de liste chainee */
void addFirst(element_t *unElement){
/* Entree en section critique */
P(mutex);
/* Comme unElement va etre insere en tete de liste,
*/
/* son successeur sera le premier element actuel de la
liste */
unElement->next = first;
/* La nouvelle tete de liste est unElement */
first = unElement;
/* Sortie de section critique */
V(mutex);
}
---endCorr
NB : Vous pouvez répondre sur votre copie ou bien dans le fichier Q2/reponse.txt
(dans ce dernier cas, indiquez sur votre copie "Réponse dans Q2/reponse.txt ").
Question 3 : Tube versus mémoire
partagée
(8 points)
On souhaite comparer les performances des communications à travers un
tube et celles à travers une zone de mémoire partagée synchronisée
selon un paradigme
producteur/consommateur.
Pour ce faire, on se propose de coder le programme Q3/tube.c
(respectivement Q3/memoirePartagee.c ) selon l'algorithme
suivant
- Création d'un tube (respectivement d'un tampon et des entités
liées au paradigme producteur/consommateur)
- Création d'un enfant
- Déclaration d'une variable
s de type structure_t
- Initialisation de tous les octets de
s à '\0'
- répéter
NBECHANGES fois
- Envoyer
s à l'enfant via le tube (respectivement
la zone de mémoire partagée)
- Fin Répéter
- Attendre décès de l'enfant
Dans ce programme, le fils effectue l'algorithme suivant :
- Définir une variable
s de type structure_t
- répéter
NBECHANGES fois
- Lire la valeur de
s envoyée par le père via le
tube (respectivement la zone de mémoire partagée)
- Fin Répéter
Question 3.1 : Codage de Q3/tube.c
Le programme Q3/memoirePartagee.c est déjà fourni.
Compléter les lignes /*** A completer ***/ de Q3/tube.c
pour qu'il fonctionne conformément à l'algorithme ci-dessus.
NB : même si vous n'arrivez pas à répondre à cette question, vous
pouvez quand même travailler sur la question Q4.2.
---beginCorr
tube.corrige.c
---endCorr
Question 3.2 : Analyse
Dans le fichier Q3/commentaire.txt :
- Q3.2.1. Indiquer le résultat de la commande
/usr/bin/time
appliqué à Q3/memoirePartagee et Q3/tube
(ou Q3/tubeSiQ31KO
si vous n'avez pas pu répondre à la question Q3.1)
---beginCorr
Résultat dépendant de la machine. A titre indicatif, sur craie
(machine à 1 Ghz) sous Linux RedHat 7.2, on a les résultats suivants :
$ /usr/bin/time tube
0.72user 2.29system 0:03.31elapsed 90%CPU (0avgtext+0avgdata
0maxresident)k
0inputs+0outputs (101major+38minor)pagefaults 0swaps
$ /usr/bin/time pc
1.89user 2.31system 0:04.62elapsed 90%CPU (0avgtext+0avgdata
0maxresident)k
0inputs+0outputs (101major+35minor)pagefaults 0swaps
---endCorr
- Q3.2.2. Les résultats obtenus militent en faveur de l'utilisation
systématique des tubes. En remarquant que le nombre d'octets contenus
dans un tube n'est pas aisément paramétrables (recompilation du noyau
requise), citer une situation où on préférera utililiser de la mémoire
partagée plutôt que des tubes (même si les performances sont moins
bonnes).
---beginCorr
On préfère utiliser de la mémoire partagée quand:
- On a besoin de
garanties sur le nombre d'octets stockables dans le "tampon" utilisé
pour les communications entre le père et l'enfant. Avec de la mémoire
partagée, on est en mesure de spécifier avec précision le nombre de
cases dans le tampon et le nombre d'octets dans chaque case. Avec les
tubes, ce n'est pas possible.
- Un processus externe a besoin de lire le contenu du "tampon"
(par exemple, pour voir les données en attente). Avec un tube, on ne
sait pas faire.
---endCorr
- Q3.2.3. La différence de temps d'exécution peut surprendre.
Expliquer pourquoi : on ne demande pas d'expliquer la différence de
temps d'exécution en elle-même (il faudrait analyser le code du noyau
Linux pour comprendre la gestion des tubes et de la mémoire partagée),
mais on demande d'expliquer en quoi la différence de temps d'exécution
est surprenante.
NB :
cette question est plus difficile que les autres. Il est donc
recommandé de ne l'aborder que quand vous avez traité les autres
questions.
---beginCorr
L'utilisation de
la mémoire partagée est nettement moins performante que l'utilisation
du
tube. Vu que la différence se fait surtout au niveau du temps user
(1.89-0.72=1.27 seconde), il semble qu'une grande partie du temps
soit perdue dans les opérations de recopie entre la mémoire du
processus
et la mémoire partagée, ce qui est surprenant puisque, quand on utilise
des tubes, on fait aussi des opérations de recopie mémoire.
---endCorr
|