|
|
Communication inter-processus
Synchronisation entre processus
Exercice 1 : Un programme d'impression simple
Le programme imprime0.c
simule une
application d'impression qui attend de la part de l'utilisateur des
noms de fichier à
imprimer et les envoie à l'imprimante (l'impression
étant
simulée par un sleep de quelques secondes).
Telle qu'elle est écrite, l'utilisateur doit attendre qu'un
fichier soit imprimé pour pouvoir donner le nom d'un autre
fichier.
Modifier l'application de sorte qu'à chaque fois que
l'utilisateur a entré un nom de fichier, un enfant est
forké pour s'occuper de l'impression de ce fichier.
Exercice 2 : Un seul enfant pour imprimer
On reprend le programme imprime0.c
évoqué à l'exercice 1. Cette fois, on
souhaite
modifier ce programme pour qu'il commence par forker un enfant
à
qui il enverra, via un tube, les noms des fichiers à
imprimer.
Ecrire ce programme modifié.
Exercice 3 : Un seul processus pour imprimer
Le programme réalisé à l'exercice 2
présente l'inconvénient que si on lance plusieurs
fois
l'application en parallèle, on a plusieurs enfants qui sont
en
charge des impressions. Pour pallier cet inconvénient, on
souhaite créer :
- une application serveur en charge de créer
un tube
nommé et d'attendre sur ce tube nommé les noms de
fichier
à imprimer
- une application cliente capable de se connecter, via
le
tube nommé, au serveur et de lui envoyer les noms de fichier
à imprimer.
Coder ces applications
Exercice 3bis : Un seul processus pour imprimer (le retour)
Reprenez l'exercice 3, mais en utilisant une file de messages au lieu d'un tube nommé.
Exercice 4 : Synchronisation des enfants dans
l'impression
Le programme développé dans l'exercice 1
présente
l'inconvénient que les enfants peuvent tous
accéder
à l'imprimante en même temps, ce qui risque de
mélanger les impressions.
Au lieu d'utiliser un enfant dédié à
l'impression
comme il a été fait dans l'exercice 2, ajouter au
programme de l'exercice 1 l'utilisation d'un sémaphore pour
veiller à ce qu'un seul enfant imprime à la fois.
Exercice 5 : Producteur/consommateur et impression
On souhaite pouvoir connaître la liste des fichiers qui sont
en
attente d'impression au niveau du serveur d'impression
développé
à l'exercice 3. On ne peut pas interroger le contenu du tube
nommé. On décide donc de le remplacer par une
zone de
mémoire partagée pouvant contenir au maximum 10
noms de
fichiers, cette zone étant gérée selon
un
paradigme producteur/consommateur.
Reprendre le code développé à
l'exercice 3 pour
qu'il travaille selon ce nouveau principe.
NB : l'application de consultation sera
développée
à l'exercice 6.
Exercice 6 : Lecteur/rédacteur et impression
Développer l'application de consultation de la liste des
fichiers évoquée à l'exercice 5.
Pour ce faire, commencer par rappeler les différents
paradigmes
présents dans cet exercice. Quels est(/sont) le(s)
rôle(s
) tenu(s) par les clients, le serveur et l'application de consultation.
NB : si la liste des fichiers est en train de changer (ajout de
fichiers par des clients ou retrait par le serveur), l'application de
consultation doit attendre. De plus, si une (ou plusieurs)
application(s) de consultation est (sont) en train de consulter la
liste des fichiers, les clients ou le serveur doivent attendre avant de
la modifier cette liste.
Exercice 7 : Correction d'un problème de
famine
Dans la question 7 de l'exercice EntréesSorties/7, un
problème de famine a été
détecté.
Modifier l'application pour corriger ce problème de famine.
Exercice 8 : Des chiffres et des lettres... et des tubes
L'application simple.c
:
- lit le flux de caractères arrivant sur
l'entrée standard
- sépare les chiffres et les lettres
- effectue l'opération appropriée
en fonction
du type de caractère, à savoir :
- sommer les chiffres
- réaliser un spectre de
fréquence pour les
lettres
- affiche le résultat obtenu
Pour cela, le programme est compsé de trois
entités :
- un distributeur (en charge de la
répartition des
caractères) ;
- un additionneur (opérant sur les chiffres)
;
- un compteur (opérant sur les lettres).
Adapter simple.c pour que
les fonctions
d'additionneur et de compteurs soient assurées par des
processus
enfants d'un processus père qui assure la fonction de
distributeur. Les communications entre les processus se font par tube.
Exercice 9 : Des chiffres et des lettres... et des
files de
message
Reprendre l'exercice 8, mais avec des communications par files de
message.
Exercice 10 : Des chiffres et des lettres... et de la
mémoire
partagée
Reprendre l'exercice 8, mais avec des communications par
mémoire
partagée.
Exercice 11 : Des chiffres et des lettres... et de la
mémoire
partagée avec des sémaphores
Reprendre l'exercice 8, mais avec des communications par
mémoire
partagée et en utilisant des sémaphores pour
faire la
synchronisation entre les différents processus.
Exercice 12 : Le repas des philosophes
5 philosophes se retrouvent à un restaurant chinois pour
déjeûner. Le restaurateur leur propose une table
sur
laquelle il dispose 5 bols et 5 baguette (une entre chaque bol). Chaque
philosophe passe son repas à penser (pendant une
durée
aléatoire), puis à manger un peu (pendant une
durée aléatoire, puis à penser... NB :
pour
pouvoir manger, un philosophe doit pouvoir prendre 2
baguettes :
celle à sa gauche et celle à sa droite.
Question 1
Écrire l'algorithme utilisé par chaque philosophe
pour
pouvoir penser, manger... sans qu'il y ait d'interblocage entre les
philosophes, ni de famine.
Question 2
Programmer cet algorithme.
Exercice 13 (ou 12 bis pour les superstitieux...) :
Expériences
autour d'un serveur
Web
Préparation
Se placer dans un répertoire de travail
tar xvfz serveurWeb.tgz
cd ServeurWeb
Introduction
Le
code présent dans le répertoire
Question1
contient un serveur Web
très
simplifié : quand on l'interroge, ce serveur se contente de
renvoyer
une page Web contenant le nombre de fois qu'on l'a interrogé
(et donc
qu'il a répondu).
Avant de rentrer dans le vif du sujet, faites fonctionner ce serveur :
- dans
un terminal, positionnez-vous dans le répertoire Question1, tapez make pour
compiler, puis server
pour lancer l'exécution du serveur ;
NB :
- par
défaut, le serveur se met en attente sur le port 8080 ;
- si vous
souhaitez qu'il utilise un autre numéro de port, tapez la
commande :
server -p numeroPort
- dans un navigateur Web, tapez l'adresse
http://localhost:8080
(ou un autre numéro de port, si vous avez
démarré
votre serveur avec un autre numéro de port) ;
- le navigateur Web affiche : « compteurReponse = 1 »
(le serveur indique qu'il a été
interrogé une fois et donc qu'il a
répondu une fois) ;
- si
vous cliquez plusieurs fois sur le bouton
« Rafraîchir / Actualiser la
page courante » du navigateur, votre navigateur
interroge à chaque fois
le serveur qui renvoie donc successivement : « compteurReponse
= 2 », « compteurReponse
= 3 »...
NB : avec Firefox, pour
une raison indéterminée (et qui ne fait pas l'objet du présent
exercice), un clic sur le bouton « Rafraîchir /
Actualiser la
page courante » peut éventuellement faire sauter le
compteurReponse de plusieurs unités. Si ce comportement vous gêne,
utilisez le navigateur textuel lynx
(commande lynx
http://localhost:8080, le rafraichissement se faisant avec les
touches Control
et R, l'arrêt avec la
touche « q ») ;
- arrêtez votre serveur (à l'aide
des touches Control
et C).
Pour fonctionner, ce serveur utilise 4 modules : codeEtudiant.c,
common.c,
main.c
et server.c.
Mais, dans les questions suivantes, vous serez amené
à
modifier seulement codeEtudiant.c
(c'est pourquoi c'est le seul module sur lequel vous avez les droits en
écriture). En effet, ce module héberge 3
fonctions qui interagissent
avec le reste du code :
- la procédure init
réalise toutes les
initialisations de votre module avant que le serveur web ne se mette en
attente de requêtes Web. Ainsi, dans le code livré
dans le répertoire Q3-1,
cette
procédure initialise à 0
une variable appelée compteurReponse
;
- la
procédure gestion_connexion_entrante
est appelée lorsque le serveur web
reçoit une requête Web. Cette procédure
(qui est appelée avec un
paramètre connexion_fd)
est chargée de confier à un thread
la responsabilité d'appeler la
procédure
handle_connection
(définie dans un des autres modules que vous n'avez pas
à modifier)
avec la valeur de ce paramètre connexion_fd.
Ainsi,
dans le code livré dans le répertoire Q3-1, cette
procédure crée un
thread thread_gestion_connexion_entrante
(avec en paramètre la
valeur de connexion_fd).
Et ce thread
se charge, quand il
prend la main, d'appeler handle_connection
avec la valeur
de connexion_fd
;
- la fonction gestion_compteurReponse
est
chargée d'effectuer les traitements liés
à la variable compteurReponse
lorsque le serveur répond à une requête
HTTP. Elle modifie la valeur de
compteurReponse,
puis retourne la valeur
courante de compteurReponse.
Ainsi, dans le code livré dans le répertoire Q3-1, cette
fonction incrémente compteurReponse,
puis retourne sa valeur.
Voici l'algorithme principal que déroule le serveur Web que vous mettez en oeuvre dans cet exercice :
main(){
Appeler la procédure init() de codeEtudiant.c
Tant que VRAI faire
connexion_fd = attente_connexion_TCP_d_un_navigateur()
Appeler gestion_connexion_entrante(connexion_fd) de codeEtudiant.c
fait
}
Les threads que vous manipulez (et qui sont créés, dans la question 1, par gestion_connexion_entrante()) ont la responsabilité d'appeler la procédure handle_connection(connexion_fd). Voici l'algorithme de cette procédure:
handle_connection(int connection_fd){
char reponse_HTTP[256];
requete_HTTP = lire_octets(connection_fd);
sprintf(reponse_HTTP, "compteurReponse = %d", résultat appel
à gestion_compteurReponse() de codeEtudiant.c);
ecrire_octets(connection_fd, reponse_HTTP);
}
Les questions 1 à 6 sont destinées
à
améliorer le fonctionnement de ce serveur. Noter que seront
évalués :
- la clarté du code,
- le fait que le code compile (sans warning),
- le fait que le retour de chaque appel
système, s'il y en a un,
est testé avec appel à
perror() ,
puis exit(EXIT_FAILURE)
en cas de problème détecté,
- le fait que le code répond correctement
à la question posée.
Question 1 : Empêcher les pertes
d'incrémentation de compteurReponse
(3,5 points)
La
variable compteurReponse
peut être manipulée par plusieurs threads
travaillant en
parallèle. On peut donc potentiellement perdre des
incrémentations de
cette variable. Pour tenter de l'observer, utilisons ab, l'outil
de benchmark d'Apache,
qui permet d'envoyer un certain nombre de
requêtes vers un serveur, dans un laps de temps
très court. Effectuez les opérations
suivantes :
- dans un terminal, démarrez le serveur ;
- dans
un autre terminal, tapez la commande :
ab -n 1000 -c 6 http://localhost:8080/
(où 1000
désigne le nombre d'invocations au serveur et 6
désigne le
nombre de
connexions simultanées faites au serveur). ab
vous indique les performances de votre serveur.
NB :
- Si jamais ab
vous affiche le message Benchmarking
localhost (be patient)...apr_socket_recv: Connection refused (111),
arrêtez le server et redémarrez le en lui ajoutant l'option -i 4 pour lui spécifier
d'utiliser le protocole IPv4
(exemple de ligne de commande : server
-i 4).
- Le programme ab
disponible sur les machines des salles TP a tendance à afficher un
nombre important de Failed
requests: . En fait, il a contacté à chaque fois server !
- dans un navigateur, tapez
l'adresse http://localhost:8080
: à cause de ces pertes d'incrémentation
potentielle, il est possible que le
navigateur affiche une valeur de compteurReponse
inférieure à la valeur attendue,
c'est-à-dire
1001 (1000
requêtes réalisées par ab
et 1 requête réalisée
par votre navigateur) ;
- arrêtez votre serveur.
Dans
le répertoire Question1,
modifiez codeEtudiant.c
pour garantir qu'il n'y
aura aucune perte d'incrémentation de compteurRequete.
Question 2 : Sauvegarde de compteurReponse
sur disque (3,5 points)
Jusqu'à
présent, lorsque l'on arrête le serveur, la valeur
courante de
compteurReponse
est perdue. On se propose donc de mémoriser sur disque sa
valeur dans un fichier sauvegardeCompteurReponse
:
- recopiez Question1/codeEtudiant.c
dans Question2
;
- modifiez Question2/codeEtudiant.c
de sorte que :
- au
démarrage du serveur :
- si le fichier sauvegardeCompteurReponse
n'existe pas, il est créé (avec les droits de
lecture-écriture pour vous-même, le groupe et les
autres,
avant modification par le umask)
et compteurReponse
est initialisé à 0 ;
- si le fichier sauvegardeCompteurReponse
existe, compteurReponse
est initialisé à la valeur
mémorisée dans ce fichier ;
- à chaque appel de gestion_compteurReponse,
on sauvegarde la valeur de compteurReponse
dans le fichier.
NB
:
- la valeur de compteurReponse
sera stockée dans le fichier sauvegardeCompteurReponse
sous forme
binaire
ou textuelle, selon votre préférence.
- le fichier sauvegardeCompteurReponse
sera ouvert avec ou sans l'option O_SYNC,
selon votre préférence. En revanche, vous
veillerez
à justifier (dans un commentaire au niveau de l'instruction
d'ouverture du fichier) votre décision par rapport
à
l'utilisation ou la non-utilisation de O_SYNC.
Question 3 : Améliorer les
performances de
la sauvegarde sur disque (4 points)
En
utilisant ab,
on constate que le serveur réalisé à
la question 2 voit
ses performances chuter par rapport à celui de la
question 1. C'est
pourquoi, dans cette question, on met en œuvre
une architecture
logicielle préservant les performances de la
question 1
tout en
conservant la sauvegarde de compteurReponse
abordée à la question 2. Le principe de
cette
architecture est de confier l'écriture sur
disque à un thread
spécifique qui
écrit, chaque seconde, la valeur courante de compteurReponse
dans le
fichier sauvegardeCompteurReponse.
Certes, si le serveur tombe, on perd le
décompte de requêtes effectuées depuis
la dernière seconde où ce thread
spécifique s'est activé, mais on
préserve les performances du serveur !
- recopiez Question2/codeEtudiant.c
dans Question3
;
- modifiez Question3/codeEtudiant.c
de sorte que :
- au
démarrage du serveur :
- si le fichier sauvegardeCompteurReponse
n'existe pas, il est créé (avec les droits de
lecture-écriture pour vous-même, le groupe et les
autres)
et compteurReponse
est initialisé à 0 ;
- si le fichier sauvegardeCompteurReponse
existe, compteurReponse
est initialisé à la valeur
mémorisée dans ce fichier ;
- on crée ce thread
spécifique ;
- ce thread
spécifique exécute une boucle sans fin dans
laquelle :
- il
écrit la valeur courante de compteurReponse
dans sauvegardeCompteurReponse
;
- il dort pendant une seconde ;
- à chaque appel de gestion_compteurReponse,
on se contente d'incrémenter compteurReponse
de 1 (sans faire aucune écriture dans le fichier sauvegardeCompteurReponse).
NB
: le fichier sauvegardeCompteurReponse
sera ouvert avec ou sans l'option O_SYNC,
selon votre préférence. En revanche, vous
veillerez à justifier (dans
un commentaire au niveau de l'instruction d'ouverture du fichier) votre
décision par rapport à l'utilisation ou la
non-utilisation de O_SYNC.
Question 4 : Utilisation d'un pool de threads (4,5
points)Dans le code livré à la question Q3.1, la procédure handle_connection() crée systématiquement un thread avec pthread_create(), thread
qui est détaché de sorte que, quand il se termine, il se
libère automatiquement (dit autrement, le système
récupére la place mémoire qu'il occupait).
Donc, à chaque fois que votre navigateur se connecte à votre serveur Web, votre serveur Web crée un thread pour traiter votre requête, puis le libère. Ce n'est pas très efficace.
L'objectif de cette question 4 est d'éviter cette
opération de création/suppression qui est coûteuse
en temps machine.
- recopiez Question3/codeEtudiant.c
dans Question4
;
- modifiez Question4/codeEtudiant.c
de sorte que :
- au
démarrage du serveur, outre les traitements mis en
œuvre à la question
Question3, on crée 20
threads
qui sont mises dans un pool de threads en attendant du travail (vous
trouverez des conseils d'implémentation de pools de threads dans la
note du transparent 3.1 (Principe) du cours « Eléments d'architecture
client-serveur ») ;
- lors de l'appel à gestion_connexion_entrante,
on confie le traitement de cette connexion à l'un des threads inactifs de
ce pool ;
- lorsqu'un thread
a terminé son appel à handle_connection,
il se remet en attente de travail.
Voici quelques indications/explications:
Dans la procédure init() de codeEtudiant.c, vous allez créer un
tube (il est totalement inutile que ce soit un tube nommé) et faire 20
pthreads_create() et 20 pthread_detach().
Chacun de ces threads va dérouler l'algorithme suivant :
Tant que VRAI faire
connexion_fd_local_au_thread = read sur le tube
handle_connection(connexion_fd_local_au_thread);
fait
L'algorithme de gestion_connexion_entrante(int connexion_fd) devient simplement:
ecrire connexion_fd sur tube
Ainsi (NB : pour suivre ces explications, il est recommandé de
faire un schéma sur un papier avec le serveur, le tube et ses 20
threads), quand le serveur démarre, il appelle init() qui crée le
tube et les 20 threads. Il attend qu'un navigateur se connecte à lui
(cf. instruction attente_connexion_TCP_d_un_navigateur() du main() du
serveur, évoquée ci-dessus). Pendant ce temps, les 20 threads démarrent
et se bloquent tous sur l'instruction de lecture sur le tube.
Quand un navigateur se connecte au serveur, le serveur appelle
gestion_connexion_entrante(int connexion_fd) qui écrit donc la valeur
de connexion_fd sur le tube.
L'un des threads va donc trouver quelque chose à lire sur le tube
et va le ranger dans connexion_fd_local_au_thread. Il se débloque donc
du read sur le tube et appelle
handle_connection(connexion_fd_local_au_thread). Quand ce thread a
fini, il se remet en attente de lecture sur le tube.
Noter que si, pendant ce temps, un autre navigateur a contacté le
serveur, le serveur a écrit une autre valeur de connexion_fd sur le
tube, valeur qui a été lue sur le tube par un autre thread, etc.
Question 5 : Utilisation de deux serveurs sur la
même machine (4,5
points)
On souhaite tester les performances lorsque la machine
héberge deux serveurs Web, l'un étant
à l'écoute du port 8080, tandis que l'autre est
à l'écoute du port 8082. ces deux serveurs
partagent la même variable compteurReponse
et
le même fichier sauvegardeCompteurReponse
dans lequel ils sauvegardent la valeur de la
variable compteurReponse.
- recopiez Question4/codeEtudiant.c
dans Question5
;
- modifiez Question5/codeEtudiant.c
de sorte que :
- au
démarrage de la première instance de serveur (par
exemple, celui qui sera à l'écoute du port 8080),
tous les objets système nécessaires soient
créés et initialisés ;
- la
variable compteurReponse
est partagée entre les deux instances de serveurs et aucune
incrémentation n'est perdue ;
- la variable compteurReponse
est
sauvegardée dans le fichier sauvegardeCompteurReponse
par
les deux instances de serveurs ;
- NB : le pool de threads de chaque instance de
serveur est spécifique à chaque serveur : il
n'est pas partagé.
Question 6 (bonus) : Autre implémentation du
pool
de threads
mis en œuvre à la question 4 (bonus de 2
points)
Lors de la question 4, il est probable que vous avez
implémenté le pool de threads
à l'aide d'un tube.
Si c'est effectivement le cas, implémentez maintenant ce pool avec un
paradigme producteur/consommateur utilisant des conditions (au sens pthread_cond_t).
Si ce n'est pas le cas, implémentez ce pool avec un tube.
- recopiez Question5/codeEtudiant.c
dans Question6
;
- modifiez Question6/codeEtudiant.c
pour mettre en œuvre cette nouvelle implémentation.
|