Portail informatique 2019-2020

Systèmes d'exploitation

Avant de commencer le tp, téléchargez cette archive.

Écrire le programme hello_pthread.c qui crée 4 threads. Chaque thread affiche "Hello World !".

Le thread principal, après avoir créé les threads, attend leur terminaison.

Tout au long du module, nous allons travailler sur un projet fil rouge inspiré d'un cas réel. Il s'agit de mettre en place un serveur web hébergeant un réseau social du type Facebook.

Pour commencer, récupérez la base du serveur:

  • git clone -b tp1_base https://stark2.int-evry.fr/asr3/csc4508_facebook/

Cette base de code est composée de:

  • main.c : lance le serveur avec les paramètres choisis par l'utilisateur ;
  • server.c : reçoit les requêtes HTTP et les connexions réseau ;
  • process_requests.c : traite les requêtes: en fonction de la page demandée, une réponse (c'est-à-dire du HTML) est généré pour être affiché dans le navigateur de l'utilisateur.

Après avoir compilé le serveur, démarrez-le avec ./server -p 8080, et connectez vous depuis un navigateur web sur l'adresse http://localhost:8080. Le serveur plante en affichant "error: parse_url is not implemented !".

Vous pouvez également tester le serveur avec le script test_suite/test_suite.sh. Pour lancer la suite de test:

$ cd test_suite $ ./test_suite.sh tp1 Testing help (url: http://localhost:8080/help) ... [...]
La fonction parse_url est chargée de lire les différents champs d'une URI sous la forme "/display?user=plip&foo=plop&bar=plup" et de remplir plusieurs champs de la structure struct page_request passée en paramètre :
  • url : le nom de la page demandée (dans l'exemple: display) ;
  • parameters : la liste des couples (clé, valeur) des arguments. Dans l'exemple: {{ "user", "plip"}, {"foo", "plop"}, {"bar", "plup"}} ;
  • nb_param : le nombre de paramètres ;

Dans un premier temps, on cherche à détecter le nom de la page, et les différents arguments. La détection de la clé et la valeur des arguments sera traitée dans une question ultérieure.

Pour tester, connectez vous à cette adresse : http://localhost:8080/display?user=plip&foo=plop&bar=plup

À l'aide la fonction strtok, implémentez la fonction parse_url. Une URI est sous la forme : /page?param1&param2&:...&paramn, où le nombre de paramètres peut varier de 0 à MAX_NB_PARAMETERS. Attention, il peut arriver que l'URI ne contienne que "/"

Notre implémentation de la fonction parse_url ne prend pas en compte les cas compliqués. Dans la vraie vie, on n'implémenterait bien sûr pas la fonction parse_url. On utiliserait plutot la bibliothèque fournie par le W3C.

Nous allons maintenant extraire les couples (clé, valeur) des paramètres. Pour cela, vous avez à votre disposition la fonctionparse_key_value qui traite une chaîne sous la forme "key=value" et remplit une structure struct key_value.

Modifiez la fonction parse_url afin d'appeler parse_key_value lorsqu'un paramètre est détecté. Vous devriez tomber sur un problème de réentrance.

Corrigez le problème de réentrance en utilisant strtok_r au lieu de strtok.

La fonction handle_get est appelée à chaque fois qu'un client demande l'affichage d'une page. En fonction de la page demandée, le traitement effectué peut être long et retarder le traitement des autres connexions entrantes.

Modifiez la fonction handle_get afin qu'elle crée un nouveau thread chargé du traitement. Pensez à détacher le thread (man pthread_attr_setdetachstate) pour libérer automatiquement ses ressources dès qu'il se termine.

Puisque les requêtes des clients sont traitées par des threads différents, les traitements effectués doivent pouvoir être fait de manière concurrente.

Analysez le code source pour déterminer les structures de données partagées, et protégez-les en utilisant des mutex ou des opérations atomiques.

Le corrigé de cet exercice est disponible dans la branche tp1_corrige du dépôt git.

Le programme exo-gdb.c crée 4 threads qui chacun écrit un message ("Hello from thread x") dans une des cases du tableau array. Après la terminaison des threads, le thread principal affiche le contenu du tableau.

On s'attend à ce que le programme affiche:

Thread 0 wrote: Hello from thread 0 Thread 1 wrote: Hello from thread 1 Thread 2 wrote: Hello from thread 2 Thread 3 wrote: Hello from thread 3

Lancez le programme pour constater que l'affichage obtenu n'est pas celui qu'on attend.

$ ./exo-gdb Thread 0 wrote: Thread 1 wrote: Thread 2 wrote: Hello from thread 2 Thread 3 wrote: Hello from thread 3

Pour déterminer la cause du problème, utilisons gdb (pour vous aider, les principales commandes utiles pour gdb sont regroupées ici) :

  • Chargez le programme dans gdb (vous aurez peut être besoin de modifier le Makefile afin que les symboles de debug soient inclus dans l'exécutable)
  • Ajoutez un breakpoint à la ligne 11, puis lancez le programme.
  • Une fois le breakpoint atteint, affichez le code source situé autour du point d'arrêt, et affichez les valeurs des variables.
  • Affichez la backtrace du thread courant.
  • Affichez la liste des threads, puis pour chacun des threads, affichez les valeurs des variables. Vous devriez remarquer un problème. Corrigez le problème.

Il est nécessaire d'ajouter l'option -g dans les CFLAGS du Makefile.

$ gdb ./exo-gdb (gdb) b exo-gdb.c:11 Breakpoint 1 at 0x118a: file exo-gdb.c, line 11. (gdb) r Starting program: /tmp/tp1/exo-gdb/exo-gdb [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New Thread 0x7ffff7db4700 (LWP 26251)] [New Thread 0x7ffff75b3700 (LWP 26252)] [New Thread 0x7ffff6db2700 (LWP 26253)] [Switching to Thread 0x7ffff7db4700 (LWP 26251)] Thread 2 "exo-gdb" hit Breakpoint 1, thread_function (arg=0x7fffffffdcd4) at exo-gdb.c:11 11 snprintf(array[my_id], 1024, "Hello from thread %d", my_id); (gdb) list 6 int nb_threads=4; 7 char array[4][1024]; 8 9 void* thread_function(void* arg){ 10 int my_id = *(int*)arg; 11 snprintf(array[my_id], 1024, "Hello from thread %d", my_id); 12 13 sleep(1); 14 return NULL; 15 } (gdb) p my_id $1 = 1 (gdb) bt #0 thread_function (arg=0x7fffffffdcd4) at exo-gdb.c:11 #1 0x00007ffff7f80fa3 in start_thread (arg=) at pthread_create.c:486 #2 0x00007ffff7eb182f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95 (gdb) info threads Id Target Id Frame 1 Thread 0x7ffff7db5740 (LWP 26247) "exo-gdb" clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:78 * 2 Thread 0x7ffff7db4700 (LWP 26251) "exo-gdb" thread_function (arg=0x7fffffffdcd4) at exo-gdb.c:11 3 Thread 0x7ffff75b3700 (LWP 26252) "exo-gdb" thread_function (arg=0x7fffffffdcd4) at exo-gdb.c:11 4 Thread 0x7ffff6db2700 (LWP 26253) "exo-gdb" clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:78 (gdb) thread 2 [Switching to thread 2 (Thread 0x7ffff7db4700 (LWP 26251))] #0 thread_function (arg=0x7fffffffdcd4) at exo-gdb.c:11 11 snprintf(array[my_id], 1024, "Hello from thread %d", my_id); (gdb) p my_id $3 = 1 (gdb) thread 3 [Switching to thread 3 (Thread 0x7ffff75b3700 (LWP 26252))] #0 thread_function (arg=0x7fffffffdcd4) at exo-gdb.c:11 11 snprintf(array[my_id], 1024, "Hello from thread %d", my_id); (gdb) p my_id $4 = 1

Les threads 2 et 3 ont le même identifiant my_id ! En examinant le code, on voit que la valeur de my_id est calculée à partir de i. Il y a une race condition: entre l'appel à pthread_create et l'instruction my_id = * (int*)arg, la valeur de i peut changer !

Pour corriger ce problème, une solution peut être de créer un tableau dans lequel sont stockés les arguments:

int args[nb_threads]; for(int i=0; i<nb_threads; i++) { args[i] = i; pthread_create(&tid[i], NULL, thread_function, &args[i]); }

Dans cet exercice, on se propose d'écrire un serveur de « M. et Mme » :

  • le client lit, sur sa ligne de commande, le nom de famille de « M. et Mme ». Il l'envoie au serveur.
  • le serveur cherche ce nom de famille dans le fichier mEtMme.txt :
    • s'il le trouve, il renvoie le nom du fils, de la fille ou bien des enfants;
    • sinon il renvoie le message "Désolé, je ne le connais pas";
  • le client affiche le message reçu du serveur.

Pour cela, vous disposez d'une architecture client/serveur dans laquelle :

  • le client ouvre une file de messages dont le nom est fonction de son pid (de manière à ne pas interférer avec d'autres clients potentiels).
  • le client se connecte au serveur via une autre file de messages (défini par le serveur).
  • la requête du client contient non seulement le « M. et Mme » cherché, mais aussi le nom de la file de messages qui a été créé en 1. par le client et sur lequel le client attend la réponse du serveur.

Actuellement, un seul thread reçoit les requêtes et les traite. Modifiez le fichier serveur.c pour qu'à chaque nouvelle requête un thread soit créé pour la traiter. La fonction search_key n'est pas thread-safe. Identifiez le problème et corrigez-le. Au lieu de créer un thread à chaque nouvelle requête, on souhaite maintenant que les requêtes soient traitées par un pool de threads. Modifiez le serveur pour qu'il crée un pool de 10 threads au démarrage, et utilisez un sémaphore.