Operating systems

Portail informatique

TP noté 2020-2021

Durée : 3 h

Tous documents autorisés, le barème est donné à titre indicatif.

Les exercices 2, 3 et 4 sont indépendants les uns 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.

Modalités

Préparation (environ 5mn)

cd someDirectory wget http://www-inf.telecom-sudparis.eu/COURS/CSC4508/Supports/CF/2020-2021-tsp/cf1/wNbK5DhwrA/TPNote2021.tgz tar xvfz TPNote2021.tgz mv TPNote2021 ${USER}_TPNote2021 cd ${USER}_TPNote2021

Livraison

La « livraison » de votre travail en fin de TP noté se fera par remontée sous Moodle (rubrique « TP noté de 3 heures ») du fichier d'extension tgz constitué de la manière suivante :

cd votreRepertoireDeTravailPourCSC4508 tar cvfz ${USER}.tgz ${USER}_TPNote2020

Analyse de trace à la volée (9 points, environ 1h15)

Le répertoire exercice2 contient un outil d'analyse de performance qui intercepte les appels aux fonctions pthread_mutex_* et génère des événements. Le répertoire contient également un programme qui traite les événements générés.

Le but de l'exercice est d'implémenter un mécanisme permettant au générateur d'événements de communiquer avec le programme d'analyse pour que les événements soient analysés pendant l'exécution de l'application.

Concrètement, le programme run_app.c va ouvrir un tube nommé, puis créer un processus fils qui exécutera :

LD_PRELOAD=liblock.so <application>
liblock intercepte les appels aux fonctions pthread_mutex_*, et génère des événements (avec la fonction record_event) qui sont écrits dans le tube nommé. Le programme run_app.c lit les événements depuis le tube et les traite.

Pour cet exercice, les fichiers suivants sont fournis:

  • run_app.c : lance l'application et analyse les événements
  • liblock.c : intercepte les appels aux fonctions pthread_mutex_* et génère des événements
  • Makefile : permet de compiler run_app et liblock.so
    • test/dummy_thread.c : programme de test qui fait quelques appels à pthread_mutex_*
    • test/Makefile : permet de compiler dummy_thread et de lancer run_app (en faisant make run)
    • test/trace_file.dat : trace contenant des événements. Ce fichier peut être utilisé si vous n'arrivez pas à faire la partie création de processus, ou manipulation du tube.

Pour l'instant, run_app crée un tube nommé, puis appelle la fonction run_application qui positionne LD_PRELOAD et lance l'application dans le processus courant.

Modifiez la fonction main du programme run_app.c pour créer un processus fils. Une fois créé, le processus fils appelle run_application(argc-1, &argv[1]). Le processus père appelle la fonction process_requests, puis attend que le processus fils termine avant de lui-même se terminer.

Dans run_app.c, implémentez la fonction process_requests. Cette fonction doit:

  • Ouvrir le tube nommé PIPE_NAME (défini dans liblock.h) en lecture seule.
  • Dans une boucle infinie, lire un struct lock_event depuis le tube
  • Si le champ function de l'événement lu est supérieur ou égal à NB_FUNCTIONS, fermer le tube nommé et sortir de la fonction.
  • Sinon, appeler la fonction process_event.
Pour tester votre fonction (en attendant que le processus fils écrive dans le tube), vous pouvez ouvrir le fichier trace_file.dat à la place du tube. Vous devriez voir s'afficher:
[0] T#139813270715264 enters mutex_init(0x557b8ffc1120) [44585] T#139813270715264 leaves mutex_init(0x557b8ffc1120) [1418776] T#139813270710016 enters mutex_lock(0x557b8ffc1120) [1450563] T#139813270710016 leaves mutex_lock(0x557b8ffc1120) [51468645] T#139813270710016 enters mutex_unlock(0x557b8ffc1120) [51484972] T#139813270710016 leaves mutex_unlock(0x557b8ffc1120) [...]

Modifiez maintenant liblock.c pour que les événements générés dans record_event soient écrits dans le tube nommé. Pour cela:

  • Lors du chargement de la bibliothèque, ouvrez le fichier PIPE_NAME en écriture.
  • A la fin de la fonction record_event, écrivez le struct lock_event généré dans le tube
  • Dans le destructeur de la bibliothèque, appelez record_event(function_entry, NB_FUNCTIONS, NULL), puis fermez le tube.
Vous pouvez maintenant tester les communications inter-processus. En lançant make run depuis le répertoire test, vous devriez voir s'afficher:
[thread #0] loop 0 [0] T#139736839699328 enters mutex_init(0x56265385d120) [27781] T#139736839699328 leaves mutex_init(0x56265385d120) [404395] T#139736839694080 enters mutex_lock(0x56265385d120) [409315] T#139736839694080 leaves mutex_lock(0x56265385d120) [50428542] T#139736839694080 enters mutex_unlock(0x56265385d120) [50444394] T#139736839694080 leaves mutex_unlock(0x56265385d120) [thread #1] loop 0 [100509714] T#139736831301376 enters mutex_lock(0x56265385d120) [100523202] T#139736831301376 leaves mutex_lock(0x56265385d120) [150524888] T#139736831301376 enters mutex_unlock(0x56265385d120) [150538694] T#139736831301376 leaves mutex_unlock(0x56265385d120) [...]

Dans le fichier run_app.c, la fonction get_thread recherche dans le tableau threads l'entrée dont le champs tid correspond à celui de event.

Modifiez la fonction get_thread pour que, lorsqu'aucune entrée n'est trouvée, la fonction agrandisse le tableau threads, initialise le nouveau struct thread_info, et retourne son adresse.

Une fois cette fonction implémentée, la commande make run lancée depuis le répertoire test devrait afficher:
[...] [thread #1] loop 9 [1902223612] T#140068279822080 enters mutex_lock(0x55cb83930120) [1902239305] T#140068279822080 leaves mutex_lock(0x55cb83930120) [1952240940] T#140068279822080 enters mutex_unlock(0x55cb83930120) [1952253638] T#140068279822080 leaves mutex_unlock(0x55cb83930120) End of thread #1 End of child process ======================= Summary: 3 threads Thread 140068288220032 took 1 different locks. Lock duration: avg:41796 ns, min: 41796 ns, max: 41796 ns (1 calls) Thread 140068288214784 took 1 different locks. Lock duration: avg:13215 ns, min: 8993 ns, max: 16614 ns (20 calls) Thread 140068279822080 took 1 different locks. Lock duration: avg:14995 ns, min: 10909 ns, max: 56369 ns (20 calls)

Synchronisation (5.5 points, environ 45 minutes)

Le but de cet exercice est d'écrire un programme de type producteur/consommateur. Des threads produisent des données et d'autres threads les consomment. Pour mettre en œuvre la communication, vous utiliserez une pile.

1 points

Commencez par créer un répertoire exercice3 dans le répertoire racine du contrôle (celui dans lequel se trouve déjà exercice2). Dans ce répertoire, écrivez un programme stack.c qui :

  • Prend deux paramètres de type entier que nous appellerons n et t. n indique le nombre de données que doit produire un producteur. t donne le nombre de threads de type producteurs et le nombre de threads de type consommateur (au total, il faut donc créer 2*t threads). On vous rappelle que la fonction atoi permet de convertir une chaîne de caractères en entier.
  • Lance t threads exécutant la fonction void* prod(void* arg) et tthreads exécutant la fonction void* cons(void* arg).
  • Attend que les t producteurs et les t consommateurs termine.

À cette étape, les fonctions prod et cons doivent être laissées vides.

1.5 point

On va représenter la pile des messages échangés entre les producteurs et les consommateurs avec une liste chaînée. Pour cela, il faut définir une structure struct node contenant un pointeur vers le nœud suivant appelé next et une valeur entière appelée data (il s'agit de la donnée produite). Il faut aussi déclarer une variable de type struct node appelée stack et initialisée à NULL.

Modifiez votre programme de façon à ce que l'argument arg reçu par la fonction void* prod(void* arg) soit le nombre d'éléments à produire (paramètre n du programme). Un producteur doit déposer en tête de stack n nouveaux nœuds. La valeur data de chaque nœud peut être quelconque.

Pensez que plusieurs threads peuvent essayer de déposer en concurrence des éléments dans la pile. Pour vérifier que votre code est correct, vous le lancerez avec comme paramètre n=10000 et t=5.

1 points

De façon similaire, un consommateur doit retirer n éléments, et cet argument doit être transmis à la fonction cons. Un consommateur retire un élément en commençant par la tête de la liste. Il n'y a donc pas besoin de parcourir la liste chaînée, il suffit de retirer le premier élément.

Attention, lorsque vous retirez un élément, la pile peut être vide. Dans ce cas, il ne faut pas décrementer le nombre d'éléments restant à dépiler.

À la fin de la fonction main, après avoir attendu la terminaison des threads, vérifiez que stack vaut NULL. Si votre programme est correct, ça devrait être le cas.

2 points, plus difficile

Lorsque la pile est vide, un consommateur tourne en boucle en attendant qu'une donnée soit déposée dans la pile. Ce fonctionnement est inefficace puisqu'un consommateur utilise le CPU pour rien. Pour cette raison, modifiez votre programme de façon à ce qu'un consommateur s'endorme sur une variable condition lorsque la pile est vide.

Comme le code demandé à cette question est plus complexe, il vous est conseillé de copier le fichier stack.c en stack-cond.c et de réaliser cet exercice dans le fichier stack-cond.c.

XV6 - Ordonnancement (5.5 points, environ 45 minutes)

Dans cet exercice, on vous demande du code xv6. Il ne faut pas joindre l'ensemble des sources à votre archive (les dépôts sur moodle ne passeraient pas). À la place, commencez par cloner xv6 en dehors du répertoire dans lequel vous avez fait les autres exercices avec la commande suivante :

git clone https://gitlab.inf.telecom-sudparis.eu/csc4508/xv6-tsp.git

Ensuite, pour générer un patch dans le répertoire dans lequel vous avez fait les autres exercices, il faut lancer la commande :

git diff origin/master > ${PATH_TO_CF}/exercice4/diff.patch

${PATH_TO_CF} est le chemin vers le répertoire contenant les codes que vous avez réalisé pour les autres exercices.

2 points

Ajoutez le champ int prio à la structure struct proc, et ajoutez l'appel système int set_priority(int pid, int priority). Cet appel système vérifie que le processus PID existe (c'est à dire que son état n'est pas UNUSED). Ensuite, l'appel système change la priorité du processus dont le PID est pid et affiche le message "Process <pid> now runs with priority <priority>".

3.5 points, plus difficile

Le but à cette question est d'élire les processus possédant la même priorité en FIFO, en commençant d'abord par élire les processus ayant la plus haute priorité. Pendant un cycle, tous les processus doivent être élu une fois. Pour cela, vous devez modifier la fonction elect(struct proc* old) de façon à mettre en œuvre cet algorithme. En détails :

  • Si old est différent de 0, il faut élire le processus prêt qui suit old dans la table des processus et ayant la même priorité. Si un tel processus n'existe pas (old est le dernier du tableau), il faut choisir un processus qui a la priorité suivante (la priorité la plus grande strictement inférieur à celle de old). Si plusieurs processus ont cette priorité, il faut choisir celui que se trouve en premier dans le tableau.
  • Si old est égal à 0 ou, dans le cas où old est différent de 0, aucun processus n'a été trouvé avec la procédure décrite précédemment (aucun processus de plus petite priorité que old n'a été trouvé), il faut choisir le processus ayant la plus haute priorité. Si deux processus ont la priorité maximale, il faut choisir celui qui se trouve en premier dans le tableau.