print · rss · source

< Les appels systèmes | TutoOS | Gérer la mémoire - utiliser la pagination pour une tâche utilisateur >


Gérer la mémoire - un noyau qui implémente la pagination

Sources

Le package contenant les sources est téléchargeable ici : kernel_PagingEnable.tgz
Pour naviguer dans l'arborescence : PagingEnable


Mémoire virtuelle et mémoire physique

Présentation

Nous avons déjà vu que dans le système de segmentation, une adresse physique est calculée à partir d'un sélecteur de segment et d'un offset. Avec la pagination activée, l'adresse obtenue par la segmentation est une adresse linéaire qui sera ensuite traduite en une adresse physique :

La pagination est un mécanisme d'adressage de la mémoire qui permet :

  • de s'affranchir des limites en mémoire de l'ordinateur en utilisant le disque dur comme extension
  • d'affecter à chaque tâche son propre espace d'adressage
  • d'allouer et de désallouer dynamiquement de la mémoire de façon simple

Quand la pagination est activée, le processeur découpe l'espace d'adressage linéaire (ou virtuel) et la mémoire physique en pages de taille fixe (généralement 4ko ou 4Mo) qui peuvent être associées librement. La traduction des adresses linéaires en adresses physiques est réalisée par le processeur grâce à plusieurs structures :

  • le répertoire de pages (Page Directory) est un tableau dont les entrées pointent vers des tables de pages. Son adresse est stockée dans le registre CR3
  • les tables de pages (Page Table) sont des tableaux dont les entrées sont des pointeurs vers des pages

Le schéma ci-dessous illustre l'association entre des pages en mémoire linéaire (ou virtuelle) et en mémoire physique par l'unité de pagination de la MMU (Memory Management Unit) :

Le fonctionnement

La traduction d'une adresse linéaire en une adresse physique se fait en plusieurs étapes :

  1. le processeur utilise le registre CR3 pour connaître l'adresse physique du répertoire de pages
  2. les 10 premiers bits de l'adresse linéaire forment un offset, entre 0 et 1023, qui pointe sur une entrée de ce répertoire de pages
  3. cette entrée du répertoire contient l'adresse physique d'une table de pages
  4. les 10 bits suivants de l'adresse linéaire forment un offset qui pointe sur une entrée de cette table de pages
  5. l'entrée sélectionnée dans la table de page pointe sur une page de 4ko
  6. les 12 derniers bits de l'adresse linéaire forment un offset, d'une valeur comprise entre 0 et 4095. Ce déplacement permet de pointer sur une adresse précise en mémoire.

Les entrées des répertoires et des tables de pages

Les entrées de ces deux tableaux se ressemblent beaucoup. Seuls les champs en grisé seront utilisés dans nos premières implémentations :

  • le bit P indique si la page ou la table pointée est en mémoire physique
  • le bit R/W indique si la page ou la table est accessible en écriture (bit à 1)
  • le bit U/S est à 1 pour permettre l'accès aux tâches non-privilégiées
  • le bit PWT et le bit PCD sont liés à la gestion du cache des pages, que nous ne verrons pas pour le moment
  • le bit A indique si la page ou la table a été accédée
  • le bit D (table de pages seulement) indique si la page a été écrite
  • le bit PS (répertoire de pages seulement) indique la taille des pages (bit à 0 pour 4ko et à 1 pour 4Mo)
  • le bit G est lié à la gestion du cache des pages
  • le champ Avail. est librement utilisable

On peut noter que les adresses physiques du répertoire de pages ou de la table des pages sont... sur 20 bits ! En fait, ces adresses doivent être alignées sur 4ko, ce qui signifie que les 12 bits de poids faible doivent être à 0. Le processeur forme cette adresse à partir des 20 bits de poids fort et la complète avec 12 bits à 0. Nous avions déja vu ce type de mécanisme avec les sélecteurs de segment de la GDT.

Un peu d'arithmétique

  • Un répertoire ou une table de pages occupent en mémoire 1024*4 = 4096 octets = 4k
  • Une table de pages peut adresser en tout 1024 * 4k = 4 Mo
  • Un répertoire de pages peut adresser en tout 1024 * (1024 * 4k) = 4 Go

Notes sur le cache

Le TLB (Translation Lookaside Buffer) est un cache des répertoires et des tables des pages utilisés afin d'accélérer la traduction d'adresse. Quand un répertoire ou une table des pages est modifiée, les pages concernées doivent être immédiatement invalidées dans le TLB. Dans un premier temps, nous n'aurons pas besoin de nous préoccuper de ces détails.

Activer la pagination

Pour activer la pagination, il suffit de passer le bit 31 du registre CR0 à 1 :

asm("  mov %%cr0, %%eax; \
       or %1, %%eax;     \
       mov %%eax, %%cr0"
\
       :: "i"(0x80000000));

Bien entendu, pour que celà fonctionne, il faut au préalable avoir initialisé un répertoire et au moins une table des pages !

Une première implémentation très simple

Pour une première implémentation, nous n'allons pas créer de tâche utilisateur. La pagination s'appliquera donc uniquement au noyau.

Une organisation simple de la mémoire avec l'identity mapping

Dans cette première implémentation, nous allons activer la pagination pour le noyau tel que les 4 premiers Mo de mémoire virtuelle coincident avec les 4 premiers Mo de mémoire physique :

Ce modèle est simple : la première page en mémoire virtuelle correspond à la première page en mémoire physique, la deuxième page en mémoire virtuelle correspond à la deuxième en mémoire physique et ainsi de suite...

Nous avons vu dans les chapitres précédent que notre noyau est tout petit et qu'il loge dans moins de 64 ko, nous allons adopter l'organisation suivante :

  • le code et les données du noyau occupent l'espace entre l'adresse 0x0 et 0x10000
  • la pile du noyau utilise l'espace entre l'adresse 0x10000 et 0x20000
  • le répertoire de pages sera chargé à l'adresse 0x20000
  • pour adresser 4 Mo, une seule table de pages suffit. Elle sera en 0x21000

Initialiser le répertoire et les tables de pages

Les entrées du répertoire et de la table seront initialisés avec les valeurs suivantes :

  • Le bit 0 et le bit 1 mis à 1 indiquent que les pages / tables pointées sont présentes en mémoire et qu'elles sont en lecture et en écriture.
  • Le bit 2 est à 0 pour indiquer que leur accès est réservé au noyau.
  • Le bit 7 du répertoire est à 0 pour indiquer que les pages font 4ko.

La fonction init_mm() initialise le répertoire et les tables de pages du noyau et active la pagination :

#include "types.h"

#define PAGING_FLAG     0x80000000      /* CR0 - bit 31 */

#define PD0_ADDR 0x20000        /* addr. page directory kernel */
#define PT0_ADDR 0x21000        /* addr. page table[0] kernel */

/* cree un mapping tel que vaddr = paddr sur 4Mo */
void init_mm(void)
{
        u32 *pd0;       /* kernel page directory */
        u32 *pt0;       /* kernel page table */
        u32 page_addr;
        int i;

        /* Creation du Page Directory */
        pd0 = (u32 *) PD0_ADDR;
        pd0[0] = PT0_ADDR;
        pd0[0] |= 3;
        for (i = 1; i < 1024; i++)
                pd0[i] = 0;


        /* Creation de la Page Table[0] */
        pt0 = (u32 *) PT0_ADDR;
        page_addr = 0;
        for (i = 0; i < 1024; i++) {
                pt0[i] = page_addr;
                pt0[i] |= 3;
                page_addr += 4096;
        }

        asm("   mov %0, %%eax    \n \
                mov %%eax, %%cr3 \n \
                mov %%cr0, %%eax \n \
                or %1, %%eax     \n \
                mov %%eax, %%cr0"
:: "i"(PD0_ADDR), "i"(PAGING_FLAG));
}

Que fait exactement cette fonction ?

pd0 = (u32 *) PD0_ADDR;

Cette instruction fait pointer la variable pd0 sur le répertoire de pages à l'adresse physique 0x20000. Notez que pd0 peut aussi être vu comme un tableau de 1024 entrées.

pd0[0] = PT0_ADDR;
pd0[0] |= 3;

Ensuite, on initialise la première entrée du répertoire en pd0[0] pour la faire pointer sur la première table de pages. Cette table est située juste après le répertoire, à l'adresse physique 0x21000.
Pourquoi avoir choisi la première page de table et pas une des 1023 autre ? Tout simplement parceque l'un des points clef de notre schéma d'adressage est que l'adresse 0 virtuelle doit correspondre à l'adresse 0 en mémoire physique et ainsi que pour toutes les adresses dans la plage adressée. Le choix d'une autre table aurait été possible, mais celà aurait généré un décalage entre les adresses en mémoire virtuelle et celles mémoire physique.
Les deux premiers bits sont mis à 1 pour indiquer que les pages / tables pointées sont présentes en mémoire et qu'elles sont accessibles en lecture et en écriture.

for (i = 1; i < 1024; i++)
        pd0[i] = 0;

Les autres entrées du répertoire de pages sont mises à zéro. On n'utilise donc qu'une seule table de page, ce qui suffit pour adresser 4 Mo.

/* Creation de la Page Table[0] */
pt0 = (u32 *) PT0_ADDR;
page_addr = 0;
for (i = 0; i < 1024; i++) {
        pt0[i] = page_addr;
        pt0[i] |= 3;
        page_addr += 4096;
}

La table de page est initialisée de façon à ce que chacune de ses entrées pointe sur une page mémoire successive. La première page adresse la mémoire à partir de l'adresse 0 et les autre pages se succèdent.

Une nouvelle exception pour gérer les Page Fault

init_idt_desc(0x08, (u32) _asm_exc_PF, INTGATE, &kidt[14]); /* #PF */

Lorsque le processeur tente d'accéder à une page qui n'est pas présente en mémoire physique, il déclenche une exception de type Page Fault. L'adresse qui a provoqué le défaut de page est stockée dans le registre CR2. A partir de cette adresse et selon le contexte d'exécution qui a provoqué l'exception, la routine de traitement devrait au choix :

  • terminer le programme en renvoyant une erreur de type segmentation fault
  • charger en mémoire la page qui était temporairement stockée sur le disque dur dans la zone de swap
  • allouer une page

Dans notre cas, le noyau va simplement afficher un message d'erreur et stopper.

Exécuter le noyau

L'affichage du message et des petits points montre que malgré le passage en mode paginé, le noyau fonctionne normalement. L'annexe sur le mode debug de Bochs indique quelques commandes utiles pour vérifier la bonne prise en compte du répertoire et des tables de pages.


< Les appels systèmes | TutoOS | Gérer la mémoire - utiliser la pagination pour une tâche utilisateur >

print · rss · source
Page last modified on November 18, 2010, at 07:19 PM