< Booter avec Grub | TutoOS | Lire et écrire sur un dique IDE >
Le package contenant les sources est téléchargeable ici : kernel_AdvMemory.tgz
Pour naviguer dans l'arborescence : AdvMemory
Nous avons vu que le noyau gère deux types de mémoire : la mémoire physique, réellement disponible et utilisable, et la mémoire virtuelle. A tout moment, le noyau a besoin de savoir quels sont les espaces de mémoire physique et virtuelle qui sont utilisés et libres et celà passe par la mise en place :
Cette étape du développement du noyau est assez délicate en raison des contraintes liées à la pagination. Le but de cette partie est néanmoins de présenter un système de gestion de la mémoire le plus simple possible. Mais contrairement aux chapitres précédents, peu de code est présenté ici. L'accent est avant tout mis sur les enjeux et la logique sous-tendant la mise en place d'une gestion de la mémoire.
Le noyau connaît la taille de la mémoire physique disponible grâce à Grub.
Dans l'implémentation de Pépin, les 8 premiers méga octets de mémoire physique sont réservés à l'usage du noyau et contiennent :
Le reste de la mémoire physique est librement disponible pour le noyau et les applications :
L'espace d'adressage compris entre le début de la mémoire et l'adresse 0x40000000
correspond à l'espace du noyau alors que l'espace compris entre l'adresse 0x40000000
et la fin de la mémoire correspond à l'espace utilisateur :
L'espace d'adressage noyau, qui occupe 1 giga octets de mémoire virtuelle, est commun à toutes les tâches (cette notion est essentielle ! Elle est expliquée en détail dans la partie traitant du déroulement d'une tâche dans une espace paginé). Ceci est implémenté très simplement en faisant pointer les 256 premières entrées du répertoire de page de la tâche sur le répertoire de pages du noyau :
Les 8 premiers méga octets de mémoire virtuelle et de mémoire physique sont mappés à l'identique. Les raisons ont déjà été expliquées dans les chapitres précédent.
La gestion de la mémoire physique est basée sur l'utilisation d'un bitmap qui contient le statut des différentes pages. Les fonctions qui permettent de manipuler ce bitmap sont les suivantes :
get_page_frame()
renvoit l'adresse d'une page libre et la marque comme utilisée. Cette fonction est utilisée quand le noyau a besoin d'une page physique libre.
release_page_frame()
libère une page précedement utilisée.
set_page_frame_used()
marque une page comme étant utilisée.
L'utilisation de la pagination nécessite qu'à chaque adresse physique soit associée une adresse virtuelle, puisque c'est celle-ci qui sera directement manipulée par les instructions du processeur. Pour pouvoir faire cette association, il faut gérer les adresses virtuelles. Deux gestionnaires d'adresses virtuelles, répondant à des besoins différents, sont utilisés par Pépin.
La première fonction d'allocation de mémoire pour le noyau s'apparente à la fonction malloc()
de la bibliothèque C standard. Il s'agit de la fonction kmalloc()
qui permet d'allouer au noyau un nombre arbitraire d'octets. La fonction kfree()
s'apparente à la fonction free()
de la bibliothèque standard et libère une zone de mémoire précédement allouée par kmalloc()
.
Le Heap est une zone extensible, segmentée en blocs de données, et dans laquelle vient piocher kmalloc()
. Le bloc d'octets disponibles pris dans le heap par kmalloc()
est organisé tel que dans le schéma ci-dessous avec un en-tête et une zone de données. L'en-tête occupe 4 octets :
Quand kmalloc()
fait une demande de mémoire, le noyau parcourt le heap afin de trouver un bloc libre suffisament grand. Si un tel bloc existe, la fonction renvoit l'adresse de la zone de données de ce bloc. Si le bloc trouvé est trop grand, celui-ci est scindé en deux et un second bloc libre est créé. Si le heap ne contient pas de bloc suffisament grand, la fonction ksbrk()
est appelée par kmalloc()
pour étendre le heap.
A noter qu'au démarrage, le Heap est initialisé et il comprend un seul "bloc".
Dans le détail, la fonction ksbrk()
étend le heap en ajoutant à son espace d'adressage une page virtuelle. Parrallèlement à ça, la fonction demande une page de mémoire physique avec get_page_frame()
puis elle associe la nouvelle adresse virtuelle à cette page en mémoire physique en mettant à jour le répertoire de pages.
Astuce : toutes ces fonctions peuvent être testées dans l'espace utilisateur sous Unix avant d'être implémentées au niveau du noyau. Celà simplifie grandement le développement !
La fonction kmalloc()
répond à l'essentiel des besoins d'allocation mémoire du noyau. Il y a cependant un cas particulier d'allocation non supportée par cette fonction, c'est quand le noyau a besoin d'une page alignée sur 4096 octets, par exemple pour créer une nouvelle table de pages.
Pour répondre à ce besoin, le noyau utilise un entrepôt de données particulier : le heap de pages. Le Heap de pages est est une zone d'environs 256 Mo au sein de laquelle l'allocation se fait par page (de 4096 octets). L'ensemble des pages disponibles aurait pu être implémenté à l'aide d'une liste chaînée, mais j'ai préféré définir une liste des "zones" libres, une zone représentant ici une simple plage mémoire :
Les fonctions de manipulation du Heap de page sont :
get_page_from_heap()
, qui renvoit une page libre
release_page_from_heap()
, qui libère une page utilisée
Nous avons donc vu que le noyau utilise deux fonctions, kmalloc()
et get_page_from_heap()
, pour avoir de la mémoire. Mais que se passe-t-il quand il n'y a plus assez de mémoire physique ou virtuelle ? Dans une implémentation propre :
J'ai fait un autre choix pour Pépin : en cas d'échec, ces fonctions affichent un message sur la console et stoppent le système ! C'est assez radical, certe, mais celà permet de conserver au code de Pépin un maximum de simplicité et de concision. Gardez cependant à l'esprit que celà n'est pas une bonne pratique de la programmation !
Comment modifier une entrée du répertoire de pages courant ou d'une table de pages de ce dernier ? Cette question peut sembler assez étrange mais elle cache en réalité un vrai problème que je vais essayer d'exposer brièvement.
Nous savons qu'avec la pagination, toutes les adresses utilisées sont virtuelles. Celà rend difficile la manipulation directe d'une adresse physique. Supposons par exemple que je veuille modifier une donnée située à l'adresse physique @p. Je ne peux directement utiliser cette adresse ! Il faut nécessairement que je passe par une adresse virtuelle @v associée à @p grâce au répertoire de pages. On ne peut utiliser directement une adresse physique !
Supposons maintenant que je veuille mettre à jour le répertoire de page courant, ce qui est une opération assez fréquente du noyau. La mise à jour du répertoire en tant que tel ne devrait pas poser trop de problème. En revanche, cette mise à jour suppose la plupart du temps celle d'une table de pages. Nous savons que le répertoire de pages contient les adresses des tables de pages. Jusque là, tout à l'air très simple, mais il y a en réalité un énorme problème... le répertoire contient les adresses physiques des tables de pages ! Dans ce cas, comment accéder à une table de pages pour la modifier ?
Plusieurs solutions existent. L'une d'elle est d'associer à chaque répertoire de pages une structure ou un tableau contenant les adresses virtuelles de ses tables de pages. L'inconvénient de cette solution est qu'elle nécessite la mise à jour d'une structure pour chaque répertoire de pages. Une autre solution est de réserver la dernière entrée du répertoire de pages en la faisant pointer sur le répertoire de page lui-même. Celà semble à première vue assez étrange, mais cette astuce permet en fait de manipuler le répertoire ou les tables de pages très facilement. C'est cette solution qui est exposée ci-dessous.
Le mécanisme utilisé par la pagination pour traduire une adresse linéaire en une adresse physique a déjà été vu. Cette adresse se décompose en trois index (ici x, y et z) :
Admettons que nous voulions modifier l'entrée de la table de page qui sert l'adresse virtuelle x.y.z.
Grâce à la dernière entrée du répertoire de pages qui est initialisée de façon à pointer sur le répertoire de page lui-même, l'utilisation de l'adresse 0x3FF
.x.y permet de modifier l'entrée proprement dite.
L'utilisation de 0x3FF
en début d'adresse fait que le répertoire de page va être utilisé comme si c'était une table de page. Ensuite, la xième entrée pointe sur la table de page et la yème entrée pointe sur l'endroit à modifier proprement dit :
Ce schéma et cette explication ne sont pas forcement très facile à comprendre en raison de la complexité même du système de pagination. Pour que tout devienne plus clair, je vous conseille de faire mentalement plusieurs fois le cheminement conduisant à la traduction d'adresse en regardant bien les schémas précédents.
Pour modifier l'entrée du répertoire de pages correspondant à l'adresse virtuelle x.y.z, il faut utiliser l'adresse 0x3FF
.0x3FF
.x. La logique est la même que précédement avec juste un niveau supplémentaire de récursion :
Lors de la création d'une tâche utilisateur, un répertoire de pages dédié est créé. Nous avons vu qu'un répertoire peut être accompagné de 1024 tables de pages de 4 Mo chacune. Afin d'optimiser l'espace utilisé en mémoire, les tables sont seulement créées en cas de besoin, lors de la mise à jour d'une entrée du répertoire. La structure struct page_directory
conserve la trace de ces allocations afin de pouvoir tout supprimer proprement quand une tâche se termine :
La pagination permet de gérer des pages de 4 Mo. Dans ce cas, l'entrée du répertoire de pages pointe directement sur la page allouée (sans passer par une table de pages). Ce mode d'adressage nécessite :
Les entrées du répertoire de pages est au format suivant :
Quand la pagination est activée, la MMU se base sur le répertoire et les tables de pages pour traduire les adresses virtuelles en adresses physiques. Un cache, le Translation lookaside buffer (TLB) est utilisé pour augmenter la vitesse de traduction des adresses. Mais quand une entrée de répertoire ou d'une table de page est modifiée, le cache n'est pas automatiquement mis à jour. Par conséquent, si la MMU continue d'utiliser le cache pour l'entrée en question, le résultat de la traduction d'adresses sera erroné. La mise à jour du cache doit donc être forcée par l'utilisateur et nous disposons pour celà de plusieurs moyens :
invlpg
avec en paramètre une adresse virtuelle réinitialise l'entrée associée du TLB
La structure struct process
contient toutes les informations associées à une tâche. Outre les registres du processeur et le pid, cette structure permet de garder une trace des pages allouées pour cette tâche :
< Booter avec Grub | TutoOS | Lire et écrire sur un dique IDE >