print · rss · source

< Gérer la mémoire - utiliser la pagination | TutoOS | Un système multi-tâches simple >


Gérer la mémoire - utiliser la pagination pour une tâche utilisateur

Sources

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


Préalable : gérer la mémoire physique

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 :

u8 mem_bitmap[RAM_MAXPAGE / 8]; /* bitmap allocation de pages */

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.

Initialisation du bitmap

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 :

/* Initialisation du bitmap de pages physiques */
for (pg = 0; pg < RAM_MAXPAGE / 8; pg++)
        mem_bitmap[pg] = 0;

/* Pages reservees pour le noyau */
for (pg = PAGE(0x0); pg < PAGE(0x20000); pg++)
        set_page_frame_used(pg);

/* Pages reservees pour le hardware */
for (pg = PAGE(0xA0000); pg < PAGE(0x100000); pg++)
        set_page_frame_used(pg);

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 :

  • réserver une page libre : get_page_frame()
  • désallouer une page occupée pour la rendre libre : release_page_frame()

Toutes ces fonctions sont dans le fichier mm.c et dans le fichier mm.h

#include "types.h"


#define __MM__
#include "mm.h"


/*
 * Parcours le bitmap a la recherche d'une page libre et la marque
 * comme utilisee avant de retourner son adresse physique.
 */

char* get_page_frame(void)
{
        int byte, bit;
        int page = -1;

        for (byte = 0; byte < RAM_MAXPAGE / 8; byte++)
                if (mem_bitmap[byte] != 0xFF)
                        for (bit = 0; bit < 8; bit++)
                                if (!(mem_bitmap[byte] & (1 << bit))) {
                                        page = 8 * byte + bit;
                                        set_page_frame_used(page);
                                        return (char *) (page * PAGESIZE);
                                }
        return (char *) -1;
}

/* Cree un mapping tel que vaddr = paddr sur 4Mo */
void init_mm(void)
{
        u32 page_addr;
        int i, pg;

        /* Initialisation du bitmap de pages physiques */
        for (pg = 0; pg < RAM_MAXPAGE / 8; pg++)
                mem_bitmap[pg] = 0;

        /* Pages reservees pour le noyau */
        for (pg = PAGE(0x0); pg < PAGE(0x20000); pg++)
                set_page_frame_used(pg);

        /* Pages reservees pour le hardware */
        for (pg = PAGE(0xA0000); pg < PAGE(0x100000); pg++)
                set_page_frame_used(pg);

        /* Prend une page pour le Page Directory et une pour la Page Table[0] */
        pd0 = (u32*) get_page_frame();
        pt0 = (u32*) get_page_frame();

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

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

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

/* Cree un repertoire de page pour une tache */
u32 *pd_create_task1(void)
{
        u32 *pd, *pt;
        u32 i;

        /* Prend et initialise une page pour le Page Directory */
        pd = (u32*) get_page_frame();
        for (i = 0; i < 1024; i++)
                pd[i] = 0;

        /* Prend et initialise une page pour la Page Table[0] */
        pt = (u32*) get_page_frame();
        for (i = 0; i < 1024; i++)
                pt[i] = 0;

        /* Espace kernel */
        pd[0] = pd0[0];
        pd[0] |= 3;

        /* Espace u */
        pd[USER_OFFSET >> 22] = (u32) pt;
        pd[USER_OFFSET >> 22] |= 7;

        pt[0] = 0x100000;
        pt[0] |= 7;

        return pd;
}
#include "types.h"

#define PAGESIZE        4096
#define RAM_MAXPAGE     0x10000

#define VADDR_PD_OFFSET(addr)   ((addr) & 0xFFC00000) >> 22
#define VADDR_PT_OFFSET(addr)   ((addr) & 0x003FF000) >> 12
#define VADDR_PG_OFFSET(addr)   (addr) & 0x00000FFF
#define PAGE(addr)              (addr) >> 12

#define PAGING_FLAG 0x80000000  /* CR0 - bit 31 */
#define USER_OFFSET 0x40000000
#define USER_STACK  0xE0000000

#ifdef __MM__
u32 *pd0;                       /* kernel page directory */
u32 *pt0;                       /* kernel page table */
u8 mem_bitmap[RAM_MAXPAGE / 8]/* bitmap allocation de pages (1 Go) */
#endif

struct pd_entry {
        u32 present:1;
        u32 writable:1;
        u32 user:1;
        u32 pwt:1;
        u32 pcd:1;
        u32 accessed:1;
        u32 _unused:1;
        u32 page_size:1;
        u32 global:1;
        u32 avail:3;

        u32 page_table_base:20;
} __attribute__ ((packed));

struct pt_entry {
        u32 present:1;
        u32 writable:1;
        u32 user:1;
        u32 pwt:1;
        u32 pcd:1;
        u32 accessed:1;
        u32 dirty:1;
        u32 pat:1;
        u32 global:1;
        u32 avail:3;

        u32 page_base:20;
} __attribute__ ((packed));


/* Marque une page comme utilisee / libre dans le bitmap */
#define set_page_frame_used(page)       mem_bitmap[((u32) page)/8] |= (1 << (((u32) page)%8))
#define release_page_frame(p_addr)      mem_bitmap[((u32) p_addr/PAGESIZE)/8] &= ~(1 << (((u32) p_addr/PAGESIZE)%8))

/* Selectionne une page libre dans le bitmap */
char *get_page_frame(void);

/* Initialise les structures de donnees de gestion de la memoire */
void init_mm(void);

/* Cree un repertoire de page pour une tache */
u32 *pd_create_task1(void);

Comment organiser l'espace d'adressage d'une tâche utilisateur

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 :

Comment organiser l'espace noyau, pour gérer les appels systèmes

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 :

init_idt_desc(0x08, (u32) _asm_syscalls, TRAPGATE, &kidt[48]); /* appels systeme - int 0x30 */

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 :

  • accéder aux données de cette tâche (dans notre exemple, code et données débutent en 0x40000000)
  • accéder aux données et aux fonctions du noyau, comme par exemple la fonction 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 :

#include "types.h"
#include "lib.h"

void do_syscalls(int sys_num)
{
        char *u_str;

        if (sys_num == 1) {
                asm("mov %%ebx, %0": "=m"(u_str) :);
                printk(u_str);
        } else {
                printk("unknown syscall %d\n", sys_num);
        }

        return;
}

Note concernant la sécurité

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 à :

  • 0 (espace privilégié) pour les pages du noyau
  • 1 (espace non-privilégié) pour les pages de l'espace utilisateur

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.

Créer une tâche

Une tâche très simple

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 :

  • Selon notre organisation de la mémoire, la tâche va s'exécuter à l'adresse virtuelle 0x40000000
  • Dans notre exemple, la tâche sera chargée physiquement à l'adresse 0x100000
  • Celà signifie que le bloc de mémoire virtuelle commençant en 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 :

void task1(void)
{
       char *msg = (char*) 0x40000100; /* le message sera stocké en 0x100100 */
       msg[0] = 't';
       msg[1] = 'a';
       msg[2] = 's';
       msg[3] = 'k';
       msg[4] = '1';
       msg[5] = '\n';
       msg[6] = 0;

       asm("mov %0, %%ebx; mov $0x01, %%eax; int $0x30" :: "m" (msg));
       while(1);
       return; /* never go there */
}

Charger la tâche

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 :

memcpy((u32*) 0x100000, &task1, 100); /* copie de 100 instructions */

Créer et initialiser le répertoire et les tables de pages de la tâche utilisateur

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 :

init_gdt_desc(0x0, 0xFFFFF, 0xFF, 0x0D, &kgdt[4]); /* ucode */
init_gdt_desc(0x0, 0xFFFFF, 0xF3, 0x0D, &kgdt[5]); /* udata */
init_gdt_desc(0x0, 0x0,     0xF7, 0x0D, &kgdt[6]); /* ustack */

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 :

u32 *pd_create_task1(void)
{
        u32 *pd, *pt;
        u32 i;

        /* Prend et initialise une page pour le Page Directory */
        pd = (u32*) get_page_frame();
        for (i = 0; i < 1024; i++)
                pd[i] = 0;

        /* Prend et initialise une page pour la Page Table[0] */
        pt = (u32*) get_page_frame();
        for (i = 0; i < 1024; i++)
                pt[i] = 0;

        /* Espace kernel */
        pd[0] = pd0[0];
        pd[0] |= 3;

        /* Espace u */
        pd[USER_OFFSET >> 22] = (u32) pt;
        pd[USER_OFFSET >> 22] |= 7;

        pt[0] = 0x100000;
        pt[0] |= 7;

        return pd;
}

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.

Exécuter la tâche utilisateur

Le code principal du noyau dans le fichier kernel.c.

#include "types.h"
#include "lib.h"
#include "gdt.h"
#include "screen.h"
#include "io.h"
#include "idt.h"
#include "mm.h"

void init_pic(void);
int main(void);

void _start(void)
{
        kY = 16;
        kattr = 0x0E;

        /* Initialisation de la GDT et des segments */
        init_gdt();

        /* Initialisation du pointeur de pile %esp */
        asm("   movw $0x18, %ax \n \
                movw %ax, %ss \n \
                movl $0x20000, %esp"
);

        main();
}

void task1(void)
{
        char *msg = (char *) 0x40000100;        /* le message sera stocké en 0x100100 */
        msg[0] = 't';
        msg[1] = 'a';
        msg[2] = 's';
        msg[3] = 'k';
        msg[4] = '1';
        msg[5] = '\n';
        msg[6] = 0;

        asm("mov %0, %%ebx; mov $0x01, %%eax; int $0x30"::"m"(msg));

        while (1);
        return;                 /* never go there */
}

int main(void)
{
        u32 *pd;

        printk("kernel : gdt loaded\n");

        init_idt();
        printk("kernel : idt loaded\n");

        init_pic();
        printk("kernel : pic configured\n");

        hide_cursor();

        /* Initialisation du TSS */
        asm("   movw $0x38, %ax \n \
                ltr %ax"
);
        printk("kernel : tr loaded\n");

        init_mm();
        printk("kernel : paging enable\n");

        pd = pd_create_task1();
        memcpy((char *) 0x100000, (char *) &task1, 100);        /* copie de 100 instructions */
        printk("kernel : task created\n");

        kattr = 0x47;
        printk("kernel : trying switch to user task...\n");
        kattr = 0x07;
        asm ("   cli \n \
                movl $0x20000, %0 \n \
                movl %1, %%eax \n \
                movl %%eax, %%cr3 \n \
                push $0x33 \n \
                push $0x40000F00 \n \
                pushfl \n \
                popl %%eax \n \
                orl $0x200, %%eax \n \
                and $0xFFFFBFFF, %%eax \n \
                push %%eax \n \
                push $0x23 \n \
                push $0x40000000 \n \
                movw $0x2B, %%ax \n \
                movw %%ax, %%ds \n \
                iret"
: "=m"(default_tss.esp0) : "m"(pd));

        while (1);
}

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 >

print · rss · source
Page last modified on October 15, 2010, at 11:46 AM