< Gérer la mémoire - utiliser la pagination | TutoOS | Un système multi-tâches simple >
Le package contenant les sources est téléchargeable ici : kernel_PagingUserEnable.tgz
Pour naviguer dans l'arborescence : PagingUserEnable
Pour créer une tâche utilisateur, nous avons la vague idée que le noyau doive mettre à jour le répertoire et les tables de pages, qu'il doive créer une pile, et qu'il doive aussi réserver de la mémoire pour y copier le code et les données.
Celà suffit-il ? Non ! Car avant toute choses, le noyau doit savoir quelles pages en mémoire physique sont libres et lesquelles sont utilisées. Il faut donc un système de gestion des pages de la mémoire physique.
Le système gestion des pages mis en oeuvre ici repose sur l'utilisation d'un bitmap. Un bitmap est un tableau dont chaque élément est un bit. Dans notre cas, chaque bit correspond à une page en mémoire physique : le premier bit correspond à la première page, le deuxième bit correspond à la deuxième page, etc. Chaque bit renseigne sur le statut libre ou occupé de la page associée :
Note : Nous utilisons ici un bitmap pour gérer l'utilisation des pages physiques (page frame) mais d'autres solutions sont possible comme l'utilisation d'une pile d'adresses libres ou bien d'une liste chaînée de structures décrivant les pages allouables.
La première étape du gestionnaire est d'initialiser le bitmap de façon à marquer les pages déjà occupées / réservées. L'espace physique sera organisé de la façon suivante :
On remarque que la tâche utilisateur sera chargée physiquement à l'adresse 0x100000
. C'est un choix arbitraire et très simplifié qui nous servira à mettre au point la pagination dans le contexte d'une tâche utilisateur. Nous verrons dans les chapitres suivant comment mettre au point un vrai gestionnaire de mémoire.
Les pages utilisées par le noyau ainsi que celles utilisées par le hardware doivent être marquées comme étant prises :
Ce bout de code fait appel à deux macros :
PAGE(addr)
calcule, pour une adresse donnée, le numéro de page physique à laquelle elle appartient.
set_page_frame_used(page)
met à jour le bitmap pour indiquer qu'une page est utilisée
Mais il existe également d'autres macros et fonctions indispensables à la gestion des pages physiques :
get_page_frame()
release_page_frame()
Toutes ces fonctions sont dans le fichier mm.c et dans le fichier mm.h
Lors de la compilation d'une tâche utilisateur, il n'est pas possible de savoir à l'avance où elle résidera en mémoire physique. Or, à partir du moment où la tâche va manipuler des adresses, cette information est essentielle. Comment faire ? Une solution qui se base sur la pagination va être exposée en détail ici. Son principe est très simple...
Tout d'abord, la tâche est liée de façon à toujours s'exécuter à la même adresse. Le choix de cette adresse est arbitraire et dans notre cas, les tâches doivent être liées en tenant compte qu'elles seront chargées à l'adresse 0x40000000
. L'adresse 0x40000000
est une adresse virtuelle, mais ça, le compilateur n'en sait rien ! La tâche pourra être chargée à n'importe quel endroit de la mémoire physique. C'est le noyau qui, grâce au répertoire et aux tables de pages propres à cette tâche, se chargera de faire la correspondance entre l'espace d'adressage virtuel et la mémoire physique.
Par exemple, si la tâche est chargée physiquement en 0x100000
, le noyau (via le MMU), se chargera de faire la correspondance avec l'espace virtuel débutant en 0x40000000
. La tâche aura donc toujours l'impression de s'exécuter réellement à cette adresse :
L'utilisation de la pagination n'introduit pas de changements sur la façon dont sont implémentés les appels systèmes. Ceux-ci sont toujours implémentés en utilisant une interruption :
La seule différence par rapport au modèle segmenté concerne la façon d'adresser les données du noyau et de l'utilisateur. Mais nous verrons ci-dessous que celà va dans le sens d'une simplification !
Supposons par exemple que l'appel système doive écrire à l'écran une chaîne de caractère située en 0x40000100
.
Au moment de l'appel système, la tâche fournit en paramètre l'adresse de la chaîne en la plaçant dans le registre ebx
. Au moment de l'appel système, le noyau doit pouvoir :
0x40000000
)
printk
et la mémoire vidéo en 0xB8000
Pour pouvoir accéder aux données utilisateur, c'est très simple : il suffit que le noyau utilise le répertoire de pages de la tâche en question. Mais comment faire pour accéder au code et aux données du noyau ? La solution est très simple : pour pouvoir accéder aux données du noyau, il faut que ce répertoire de pages ait aussi les entrées adéquates pour accéder à l'espace virtuel du noyau. Autrement dit, le répertoire de pages de la tâche utilisateur comprend une partie qui lui permet d'accéder à son propre espace d'adressage, et une autre partie qui lui permet d'accéder à l'espace d'adressage du noyau ! :
Le traitement de l'appel système servant à afficher une chaîne est maintenant très simple car l'adresse passée en paramètre est directement accessible :
Bien que présent dans son espace d'adressage virtuel, la tâche ne doit pas pouvoir accéder à l'espace du noyau à tout moment : celà constituerait un gros problème de sécurité. Pour empécher la tâche en mode utilisateur d'accéder à l'espace noyau hors d'un appel système, il suffit de positionner le bit US des entrées du répertoire et des tables de pages à :
Nous voulons que la tâche accède au noyau uniquement via des services précis, en l'occurence des appels système. Le processeur, jusque là en mode utilisateur (ring 3), passe alors en mode noyau (ring 0). Pour distinguer ces deux états, on dit que la tâche s'exécute en mode utilisateur ou en mode noyau.
Pour servir d'exemple, la tâche à exécuter est une fonction très simple, presque identique à celle utilisée au chapitre traitant des appels systèmes. La seule différence concerne l'adressage :
0x40000000
0x100000
0x40000000
sera mappé sur le bloc de mémoire physique en 0x100000
grâce au répertoire et aux tables de pages.
Le code ci-dessus recopie les caractères à cet emplacement puis fait un appel système pour afficher la chaîne :
Dans le modèle d'organisation de la mémoire nous avons décidé que la tâche serait en 0x100000
en mémoire physique. Cette fonction occupe au maximum 100 octets, la fonction ci-dessous effectue la copie du code à la bonne adresse :
L'utilisation de la pagination repose sur le mode protégé qui implique l'utilisation de la segmentation. Il faut donc créer des descripteurs de segments pour le code, les données et la pile utilisateur. Pour simplifier le modèle, ces segments parcourent toute la mémoire :
Il faut également créer et initialiser le répertoire et les tables de pages propres à la tâche utilisateur. C'est le rôle de la fonction pd_create_task1()
. Elle utilise les fonctions d'allocation et de désallocation de pages de la mémoire physique pour y stocker les répertoires et les tables de pages :
Attention, cette implémentation est une simplification qui vise à illustrer la pagination mais il faut garder à l'esprit que le modèle mémoire utilisé ici est très rudimentaire. Nous verrons plus tard que dans un OS plus complet, qui offre un gestion plus fine de la mémoire, la gestion des répertoires et des tables de pages obéissent à des mécanismes plus complexes.
Le code principal du noyau dans le fichier kernel.c.
Pour vérifier avec bochs l'utilisation de la pagination pour la tâche utilisateur, il est possible de poser un point d'arrêt sur une adresse virtuelle. L'adresse ci-dessous correspond au point d'entrée de la tâche utilisateur. Ensuite, la directive info tab
indique comment le MMU traduit les adresses :
<bochs:1> lb 0x40000000 <bochs:2> c ... <bochs:3> info tab cr3: 0x00022000 0x00000000-0x003fffff -> 0x00000000-0x003fffff 0x40000000-0x40000fff -> 0x00100000-0x00100fff
< Gérer la mémoire - utiliser la pagination | TutoOS | Un système multi-tâches simple >