Portail informatique 2019-2020

Systèmes d'exploitation

Le but de ce TP est de mettre en œuvre l'équivalent de la fonction mmap dans xv6.

Avant de commencez, assurez vous d'avoir terminé le dernier TP sur xv6 Vous pouvez également vous baser sur le code disponible dans la branche tp13_base du dépôt git :

Dans xv6 ajoutez les appels systèmes suivants :
  • void* mmap(void* addr, int size, int prot, int flag, int fd, int offset). Cette fonction met exactement en œuvre mmap, mais pendant la première partie du TP, vous ignorerez les arguments prot et flag.
  • et void munmap(void* addr, int size)

Ces fonctions doivent être ajoutées dans un nouveau fichier nommé mmap.c.

Pour le moment, la fonction mmap doit afficher ses arguments sur le terminal avant de renvoyer le pointeur -1 signifiant qu'une erreur s'est produite, et la fonction munmap doit afficher ses arguments sur le terminal. Pour simplifier la gestion des erreurs, créer un fichier mmap.h dans lequel vous ajouterez la macro suivante : #define MAP_FAILED ((void*)-1)
Ajoutez un petit programme permettant de tester la fonction mmap. Votre programme doit :
  • ouvrir le fichier README en lecture écriture (pensez à inclure fcntl.h pour avoir accès aux drapeaux O_RDONLY, ou O_RDWR),
  • calculer la taille du fichier avec la fonction fstat (pensez à inclure stat.h pour avoir la définition de la structure stat),
  • projeter tout le fichier à l'adresse 0x10000000,
  • afficher le contenu de la zone mémoire projetée sur le terminal,
  • "déprojeter" le fichier.
Pour chaque appel système, vérifiez la valeur de retour et arrêtez le programme en cas d'erreur. Pour l'instant, le programme devrait se terminer sur un erreur puisque la fonction mmap renvoie une erreur.
Le corrigé de cet exercice se trouve dans la branche tp13-exo1.
Dans cet exercice, vous réalisez une première mise en œuvre simple de la fonction mmap. Pour commencer, avec PGROUNDDOWN, alignez addr sur une frontière de page. Dans mmap, vérifiez que fd correspond bien à un fichier ouvert par le processus courant. La liste des fichiers ouvert par un processus est stockée dans le tableau ofile de struct proc. Pour vérifier que le fichier est bien ouvert, vous pouvez vous inspirer du code de argfd se trouvant dans sysfile.c. Dans mmap, affichez la taille du fichier sur le terminal. Vérifiez que ce que vous affichez est cohérent. Vérifiez que les arguments offset et size de mmap sont cohérents. En cas d'erreur, affichez un petit message et renvoyez -1. En vous inspirant de votre réalisation de mémoire partagée dans xv6 effectuée en TP9, allouez les pages physiques permettant de projeter le fichier en mémoire, et associez les à l'adresse addr (paramètre de mmap). À cette étape, on ne vous demande pas de gérer les cas d'erreurs de kalloc et mappages. Pour associer une page physique à une adresse virtuelle, il faut utiliser la fonction mappages qui est définie dans vm.c. Comme, mappages est définie static, elle est inutilisable depuis mmap.c. Pour résoudre ce problème, supprimez le mot-clé static de la déclaration de mappages et ajoutez la signature de mappages dans mmu.h. On vous demande maintenant de gérer les erreurs dans kalloc ou mappages. En cas d'erreur, il faut :
  • supprimer les entrées dans la table des pages qui ont déjà été associées,
  • libérer les pages physiques déjà allouées.
Pour vous aider à mettre en œuvre cette gestion d'erreur, ajoutez la fonction suivante à vm.c, et ajoutez sa définition dans defs.h : char* unmappage(pde_t *pgdir, const void* vaddr) { pte_t* pte = walkpgdir(myproc()->pgdir, (char*)vaddr, 0); if(!pte || !(*pte & PTE_P)) panic("unmapped page"); char* res = P2V(PTE_ADDR(*pte)); *pte = 0; return res; }

Cette fonction supprime l'association correspondant à l'adresse virtuelle vaddr de la table des pages pgdir. Cette fonction renvoie aussi l'adresse virtuelle dans le noyau à laquelle est associée la page physique qui vient d'être dissociée de vaddr (cette adresse est celle qui avait été renvoyée par kalloc).
Utilisez la fonction readi (fichier fs.c) pour copier le contenu fichier dans la zone mémoire que vous venez d'allouer. Modifiez aussi le retour de mmap de façon à renvoyer addr. Vérifiez que votre programme de test affiche correctement le contenu du fichier. Pour que les tests que vous allez effectuer aux questions aillent plus vite, n'hésitez pas à n'afficher que les 86 premiers caractères du fichier. Le corrigé de cet exercice se trouve dans la branche tp13-exo2.
On souhaite maintenant projeter de façon paresseuse le fichier. Techniquement, dans mmap, au lieu de projeter le fichier, vous devez uniquement enregistrer l'association entre l'espace d'adressage virtuel (addr, size) et le fichier (fd, offset, size) dans une structure. Ensuite, comme aucune page n'est physiquement associée à cette structure, un accès à la zone va générer une faute de page (trap 14). Vous devez donc modifier la façon de gérer ces fautes dans xv6 pour projeter la page demandée au besoin. Dans la suite du TP, on définit un segment (struct segment) comme une zone mémoire virtuelle associée à un fichier. Techniquement, un segment associe une adresse virtuelle addr à (i) une inode ip, (ii) un offset offset, (iii) une taille size, (iv) une protection et (v) un drapeau. Définissez une structure décrivant les segments d'un processus dans proc.h, et ajoutez un tableau de segments à un processus, en limitant la taille du tableau à 16 (pensez à utiliser une macro). Dans mmap, sans modifier le comportement fonctionnel que vous avez actuellement, enregistrez le segments dans la table des segments du processus. Modifiez la façon de projeter un fichier de façon ne pas donner accès en lecture (PTE_U)/écriture(PTE_W) aux pages virtuelles. En cas de faute de page (T_PGFLT), ajoutez simplement ces droits pour le moment. Pour cela, vous devez modifier le code de trap. Notez que l'adresse fautive à laquelle un processus accède est enregistrée dans le registres cr2 au moment de la faute. Vous pouvez retrouver cette adresse avec la fonction int rcr2(). Vous aurez sans doute besoin d'utiliser la fonction walkpgdir pour retrouver la page associée à l'adresse fautive. Or cette fonction est définie static dans vm.c. Supprimez le mot-clé static et déclarez la signature de la fonction dans mmu.h. Modifiez votre code de façon à charger paresseusement les pages lorsqu'elles sont accédées. D'après vous, que peut-il se passer si le processus ferme le fichier associé au segment ? Pourquoi, dans mmap, au lieu de stocker directement l'inode dans le segment, il faut en fait stocker le résultat de l'appel à idup(ip) dans le segment (où ip est le pointeur vers l'inode) ? Le processus pourrait fermer le fichier avant d'accéder à au segment de mémoire. À ce moment, les lectures paresseuses seraient effectuée sur une inode fermée, ce qui ne serait pas fonctionnel. En appelant idup, on incrémente le compteur d'ouverture de l'inode, ce qui nous assure que l'inode restera en mémoire même si le processus la ferme. Ajoutez des définitions pour les protections PROT_READ et PROT_WRITE dans mmap.h, et traitez correctement ces protections. Lors de la faute, si l'accès a été effectué en lecture ou écriture, le processeur positionne un code d'erreur (stocké dans tf->err). Si le bit 1 est actif (en numérotant à partir de 0, c'est-à-dire si tf->err & 2 n'est pas égal à 0), la faute est causée par un accès en écriture, sinon, c'est que la faute est causée par un accès un lecture. De cette façon, avant de charger la page, vous pouvez savoir si le processus a des droits suffisants. On vous demande maintenant de gérer la concurrence, c'est-à-dire le cas où deux cœurs essayent de charger paresseusement une page de façon concurrente. Le corrigé de cet exercice se trouve dans la branche tp13-exo3. Lors d'un fork(), on souhaite maintenant pouvoir hériter des segments de son père. Cet exercice est optionnel. De façon à pouvoir gérer le partage de segments entre un père et son fils, il faut restructurer le code. Pour cela, au lieu de directement stocker les segments dans les processus, on définit un tableau global de segments de taille maximal 1024. Ensuite, dans un processus, au lieu de directement stocker des segments, on stocke des pointeurs vers des segments se trouvant dans ce tableau global segments.

Modifiez le code en conséquence. Pensez que, pour allouer un segment, il faut trouver une entrée libre. Pour cela, vous pouvez considérer que si l'adresse du segment vaut 0, c'est que l'entrée est libre.
Modifiez la fonction fork() de façon à ce qu'un père et son fils partagent leurs segments. Si vous n'aviez pas encore mis de verrous, pensez qu'un père et son fils peuvent maintenant simultanément essayer de projeter de façon paresseuse une page d'un fichier. Ajoutez un drapeau MAP_SHARED à mmap.h. Lorsqu'un segment est noté comme MAP_SHARED, les segments sont partagés entre un père et son fils comme c'est actuellement le cas. Sinon, le fils doit dupliquer le segment du père. Modifiez votre programme en conséquence.
Le but de cet exercice est d'être capable de supprimer des projections. Cet exercice est optionnel. Mettez en œuvre munmap. Pensez que vous ne pouvez supprimer réellement un segment qu'une fois que plus aucun processus ne l'utilise. Pensez aussi que vous devez écrire sur disque les pages qui ont été modifiée. Pour cela, vous devez ajouter la définition de la constante PTE_D = 0x40 dans mmu.h, et savoir que ce bit est activé dans la table des pages dès que le processeur modifie le contenu de la page. Modifiez exit() de façon à automatiquement supprimer les projections d'un processus avant qu'il se termine. Le but de cet exercice est de compléter notre mise en œuvre. Cet exercice est optionnel. Ajoutez un drapeau MAP_ANONYMOUS à mmap.h. Lorsqu'un segment est marqué comme étant de la mémoire anonyme, les arguments fd et offset sont ignorés, et la mémoire doit être initialement remplie avec des zéros. Modifier le code de xv6 de façon à ce que vos segments soient aussi utilisés pour construire l'image initiale d'un processus. Un processus doit posséder 5 segments initiaux (voir la fin du fichier proc.h) :
  • Un segment text contenant le code du programme,
  • Un segment donnée contenant les données initialisées du programme,
  • Un segment bss contenant les données non initialisées, c'est-à-dire initialisées avec des 0, du programme,
  • Un segment de pile de taille fixe,
  • un segment de tas qui peut grandir.

Tous ces segments doivent être définis comme n'étant pas du type MAP_SHARED de façon à ce qu'ils soient dupliqués automatiquement lors d'un fork().

Pour construire ces segments, vous devez analyser et modifier la fonction exec (fichier exec.c), en particulier à partir de la ligne 42.

Modifiez aussi la fonction fork() pour ne plus appeler copyuvm qui s'occupe de copier la mémoire d'un processus (la mémoire des segments qui vous venez de définir). À la place, vous devez toujours appeler setupkvm() qui permet de créer une nouvelle table des pages dans laquelle le noyau est bien projeté. Si votre code est correct, en fonction du drapeau MAP_SHARED, les segments du père seront tous soient copiés dans le fils, soient partagés avec le fils.
Modifiez mmap de façon à ce que, si addr soit égal à 0, mmap trouve automatiquement une zone libre dans l'espace d'adressage du processus.
On souhaite maintenant mettre en œuvre un fork() rapide. Le principe de ce fork() rapide est de ne pas copier la mémoire du père au moment du fork(), mais uniquement lorsque soit le père, soit le fils la modifie. N'hésitez pas à poser des questions à vos enseignants pour savoir comment concevoir votre solution. Cet exercice est optionnel. La solution se trouve dans la branche tp13_corrige de xv6.