Portail informatique Année 2018 – 2019

PAAM – Programmation avancée des architectures multicoeurs

Ce TP a pour but de vous faire comprendre comment est gérée la mémoire dans un système d'exploitation et comment sont mis en œuvre les segments de mémoire partagée. Pour vous échauffer, nous vous proposons d'approfondir vos connaissances des macros. Le code que vous allez écrire dans cet exercice vous sera utile par la suite pour masquer quelques différences entre les interfaces POSIX (celle de Linux) et xv6. La réalisation de cet exercice ne nécessite pas xv6. Dans un fichier shm-posix.c, définissez la macro suivante : #define foo(a0, ...) a0 + __VA_ARGS__ Dans la fonction main de shm-posix.c, appelez foo(1, 2, 3) et observez le code généré par gcc -E. En vous inspirant de l'exemple précédent, définissez une macro nommée dprintf (pour debug printf) dans shm-posix.c permettant d'afficher un message sur la sortie standard. Cette macro doit avoir la même interface que printf, c'est à dire que le nombre de paramètres est variable (voir man 3 printf). Modifiez la fonction main de votre programme pour qu'elle appelle dprintf("la commande %s est lancée avec %d paramètres\n", argv[0], argc). Sur le même modèle que dprintf, ajoutez une macro error ayant l'interface de printf et affichant le message sur la sortie d'erreur standard. Votre macro doit aussi terminer le processus avec un exit. Testez le code suivant en compilant avec l'option -Wno-error=multistatement-macros : if(0 == 1) error("Je suis un chameau\n"); dprintf("Je suis un processus\n"); Vous risquez de tomber sur une erreur de compilation indiquant un problème avec les paramètres passés à printf. En examinant le code généré par gcc -E, vous pouvez remarquer que bien qu'aucun paramètre variadique ne soit passé, il reste une virgule. Vous pouvez corriger cela en utilisant ##__VA_ARGS__ au lieu de __VA_ARGS__.

Pour quelle raison ne voyez-vous aucun affichage ?

Pensez à compiler avec gcc -E pour voir le code réellement compilé par gcc
Pour corriger le problème identifié à la question précédente, il faut être capable de regrouper des instructions ensembles pour former un bloc. Vous pourriez regrouper le dprintf et le error avec des accolades, mais dans ce cas, l'utilisateur pourrait omettre le point virgule de fin de déclaration, ce qui pourrait rapidement le programme illisible. Pour forcer l'utilisateur de la macro à ajouter un point virgule, vous avez deux solutions. Vous pouvez utiliser un do ... while comme ici  do { déclaration1; déclaration1; } while(0)

Ou vous pouvez utiliser entourer le bloc entre accolades avec des parenthèses comme ici  ({ déclaration1; déclaration1; })

Corrigez le problème identifié à la question précédente.

Dans cet exercice, vous allez écrire un programme utilisant une mémoire partagée pour synchroniser deux processus. Dans un premier temps, ce programme s'exécute dans un environnement POSIX (c'est à dire sous Linux). La réalisation de cet exercice ne nécessite pas xv6.

Dans la suite du TP, utilisez à dprintf plutôt que printf pour afficher des informations, et error pour quitter suite à une erreur. Dans le programme shm-posix.c, créez un processus fils affichant un message avant de quitter. Le père doit attendre la fin du fils avant d'afficher un message. Modifiez votre programme pour que :
  • Avant la création du processus fils:
    • Le père créé un segment de mémoire partagée de taille 8192,
  • Puis, après la création du processus fils:
    • Le père projette ce segment dans son espace d'adressage à l'adresse 0x10000000 avant de détruire la projection,
    • Le fils projette ce même segment dans son espace d'adressage, mais à l'adresse 0x20000000 avant de détruire la projection,
    • Le père détruise le segment de mémoire partagée après la terminaison du fils.
Pensez à traiter les cas d'erreur.

Les fonctions suivantes vous seront utiles :
  • shm_open : ouvre ou crée un segment de mémoire partagée. Une utilisation typique est int fd = shm_open("my-key", O_RDWR | O_CREAT, 0777);
  • ftruncate : permet de spécifier la taille d'un segment de mémoire partagée préalablement ouvert. Une utilisation typique est ftruncate(fd, 65536);
  • mmap : permet de projeter le segment de mémoire partagée dans l'espace d'adressage du processus (plus généralement, mmap permet de projeter n'importe quel fichier). Une utilisation typique est : void* addr = (void*)0x10000000; addr = mmap(addr, 65536, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
Initialement, un segment de mémoire partagée ne contient que des 0. Utilisez cette propriété :
  • pour bloquer le père tant que le premier entier de la mémoire partagé contient 0,
  • faire en sorte que le fils débloque le père.
Utilisez soigneusement l'interface C11 que vous avez vu en cours 4 (atomic_load_explicit...). Pensez à effectuer des affichages pour vous garantir que votre synchronisation est correcte (gardez ces affichages qui vous servirons dans le troisième exercice).
Au lieu d'utiliser le premier entier de la mémoire partagé pour synchroniser le père et le fils, utilisez une variable globale. Pour quelle raison la synchronisation ne marche plus ? Rétablissez le fonctionnement du programme en utilisant le premier entier du segment de mémoire partagé. De façon similaire, bloquez le fils tant que le second entier vaut 0, et utilisez le père pour débloquer le fils après que le père ait été lui-même débloqué par le fils.

Félicitation, vous venez de mettre en œuvre un superbe appel de fonction inter-processus ! En effet, le fils est le client, envoie une requête au père qui exécute du code, pendant ce temps, le fils attend la réponse du père.

Le but de cet exercice est de créer une nouvelle interface pour manipuler les segments de mémoire partagée dans xv6. À cette étape, on ne s'occupe que des interfaces, leur mise en œuvre reste vide. Commencez par récupérer le code de xv6 : git clone -b tp9_base https://stark2.int-evry.fr/csc4508_xv6/ csc4508_tp9 git checkout tp9_base Copiez le programme shm-posix.c en shm.c dans les sources de xv6 et ajoutez le à la chaîne de compilation de xv6 (variable UPROGS du Makefile). Ensuite, commentez les fonctions qui s'occupent de créer ou manipuler la mémoire partagée, puis modifiez votre programme de façon à ce qu'il compile et s'exécute sans erreur avec le noyau xv6. Puisque le programme utilise des fonctions atomiques, il est nécessaire d'inclure stdatomic.h. Ce fichier ne fait pas partie de xv6, mais vous pouvez laisser le #include "stdatomic.h" et laisser le compilateur utiliser la version Linux du fichier. Il est probable que votre programme se termine sur un trap 14 (faute de segmentation). Si c'est le cas, ajoutez un exit() à la fin de votre programme. Ajoutez les appels systèmes suivants à xv6 :
  • int shm_create(int size) crée un segment de mémoire partagée de taille size et renvoie un identifiant vers ce segment. C'est l'équivalent d'un appel à la fonction POSIX shm_open suivie de ftruncate
  • int shm_attach(int id, void* addr) attache le segment id à l'adresse addr. Cette fonction est l'équivalent de la fonction POSIX mmap.
  • int shm_detach(int id) détache le segment id. Cette fonction est l'équivalent de la fonction POSIX munmap.
  • int shm_destroy(int id) détruit le segment id. Cette fonction est l'équivalent de la fonction POSIX shm_unlink
Chacune de ces fonctions doit renvoyer -1 en cas d'erreur. Ajoutez donc une mise en œuvre préliminaire de ces fonctions dans le fichier vm.c qui affiche fonction ??? not yet implemented avant de renvoyer -1.

Pour ajouter ces nouveaux appels systèmes, il faut :
  • Associer un numéro aux différentes fonctions. C'est ce numéro qui se transmis par le processus au système lors de l'appel système. Pour cela, il faut définir de nouvelles constantes dans syscall.h.
  • Côté processus, il faut ajouter le code permettant d'appeler les nouvelle fonctions systèmes. Il faut modifier :
    • le fichier user.h qui contient les signatures des appels systèmes,
    • le fichier usys.S qui contient le code (assembleur) de ces fonctions.
  • Côté système, il faut ajouter le code mettant en œuvre les nouvelles fonctions systèmes. Il faut modifier :
    • le fichier vm.c pour ajouter les définitions préliminaires des appels systèmes,
    • le fichier syscall.c qui contient la table associant les numéros des appels systèmes aux fonctions de mises en œuvre.
Décommentez le code qui manipule les segments de mémoire partagée de shm.c et adaptez le pour utiliser les appels systèmes de xv6. Vérifiez que votre programme a le comportement attendu (appel de shm_create affiche "not yet implemented" et renvoie -1, shm.c quitte avec un message adéquat).
Dans cet exercice, on vous demande de mettre en œuvre shm_create et shm_attach. Pour commencer, on vous rappelle que argint(n, &addr) permet de stocker le nième argument passé à l'appel système à l'adresse addr (utilisez aussi cette fonction dans shm_attach pour savoir à quel adresse attacher le segment).

Ensuite, pour mettre en œuvre ces fonctions, nous vous conseillons d'utiliser la structure de données suivantes (à ajouter à vm.c) pour représenter l'ensemble des segments de mémoire partagée : #define SHM_N 16 #define SHM_MAX 10 struct { struct spinlock lock; struct { char* pages[SHM_MAX]; // int npages; int nused; } shms[SHM_N]; } shms; Dans cette structure, shms.lock est un verrou permettant de protéger l'accès à la table des segments de mémoire partagée. shms.shms[id] décrit le segment de mémoire partagée d'identifiant id  :
  • pages contient les adresses virtuelles des pages partagées (voir la suite),
  • npages donne le nombre de pages du segment (vaut 0 si l'entrée n'est pas utilisée, c'est-à-dire s'il n'existe pas de segment d'identifiant id),
  • nused donne le nombre de fois où le segment a été attaché (ce champ nous servira dans l'exercice suivant).
Initialement, la structure est remplie avec des zéro. En particulier, on peut savoir que toutes les entrées du tableaux sont libres puisque, pour tout segment i, shms.shms[i].npages vaut 0. Enfin, pour la gestion mémoire, vous devez savoir que :
  • char* kalloc() alloue une nouvelle page physique et renvoie un pointeur vers une adresse virtuelle à laquelle la page est projetée. Cette adresse virtuelle n'est valide que dans l'espace d'adressage noyau.
  • int V2P(char* addr) renvoie l'adresse physique de la page associée à l'adresse virtuelle addr lorsque addr est une adresse renvoyée par kalloc.
  • mappages(myproc()->pgdir, vaddr, PGSIZE, paddr, PTE_W|PTE_U) permet de projeter la page d'adresse physique paddr à l'adresse virtuelle vaddr dans le processus courant en donnant au processus les droits en écriture sur cette page.
  • Pour calculer le nombre de pages mémoires permettant de stocker size octets, vous pouvez utiliser PGROUNDUP(size) / PGSIZE.

La fonction shm_create doit:

  • Calculer le nombre de pages mémoires à allouer
  • Trouver un shm libre (c'est à dire dont npages vaut 0)
  • Allouer les pages mémoires (avec kalloc) et stocker leur adresse dans le tableau pages
  • Remplir les pages allouées avec des 0

La fonction shm_attach doit projeter les pages du segment de mémoire partagée en mémoire et incrémenter le compteur de références nused.

Le but de cet exercice, est de mettre en œuvre les fonctions shm_detach et shm_destroy.

Pour commencer, vous aurez besoin pour détacher le segment id du processus p de savoir à quelle adresse ce segment avait été projeté. Pour cela, vous devez modifier la structure proc de façon à conserver les associations (id, addr)id est un identifiant de segment et addr l'adresse de projection. Modifiez la fonction shm_attach afin de stocker cette association.

Ensuite, pour supprimer une association entre une adresse virtuelle et une adresse physique dans un processus, il n'existe pas de fonction dans le noyau. Pour vous aider, le code suivant permet de marquer l'adresse virtuelle vaddr comme invalide dans la table des pages du processus courant.

pte_t* pte = walkpgdir(myproc()->pgdir, (char*)vaddr, 0); if(!pte || !(*pte & PTE_P)) panic("unmapped page"); *pte = 0;

Enfin, vous devez savoir que kfree permet de libérer une page.

Pour aller encore plus loin, vous pouvez :
  • Empêcher la destruction d'un segment si ce dernier est encore projeté dans un autre processus en utilisant le champ nused.
  • Détacher automatiquement les segments attachés un processus lorsqu'il se termine (fonction exit dans proc.c).
La solution se trouve dans la branche tp9_corrige de xv6.