print · rss · source

< Une méthode générique pour gérer les listes chaînées | TutoOS | Un premier shell >


Utiliser les 'Page Fault' pour allouer dynamiquement la mémoire

Les sources

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


Le problème : une pile non extensible

Dans l'implémentation actuelle, la mémoire est allouée à un processus au moment de sa création et celle-ci est immuable. Celà ne pose pas de problème pour la zone de code qui est de taille fixe. En revanche, celà en pose un pour la pile qui, par nature, devrait être extensible (du moins tant que de la mémoire est disponible) mais dont la taille est actuellement limitée à 4ko.

Allouer de la mémoire à la demande grâce aux défauts de page

Lorsqu'un processus essaye d'accéder à une adresse qui est hors de son espace d'adressage, une exception de type Page Fault ("défaut de page") est déclenchée. Le processeur fournit des indications concernant les causes de l'exception :

  • le registre CR2 est rempli avec l'adresse fautive
  • un code d'erreur, qui permet de connaître la cause de l'exception, est positionné sur la pile noyau

La fonction isr_PF_exc() qui gère les défauts de page se sert de ces indications pour :

  • si l'adresse est valide, modifier le répertoire de page du processus courant pour lui permettre l'accès à cette adresse. Après quoi le gestionnaire retourne et le processus peut reprendre la main à l'instruction qui avait déclenché le défaut de page.
  • si l'adresse n'est pas valide, afficher un message et terminer le processus de façon propre.

Idéalement, on devrait tester la validité d'une adresse en prenant en compte une cartographie fine de l'espace d'adressage utilisateur :

  • la zone de texte et de données en lecture seule
  • la zone du heap utilisateur
  • la pile utilisateur

Mais pour simplifier, le gestionnaire de défaut de page considère que toute les adresses de l'espace utilisateur (situé entre USER_OFFSET et USER_STACK) sont valides. L'algorithme implémenté est donc très simple :

Algorithme : isr_PF_exc
Paramètres : adresse fautive (faulting_addr)
Début

Si faulting_addr est dans l'espace utilisateur
Alors
Une nouvelle page physique est allouée
La liste des pages physiques utilisées par le processus en court est mise à jour
Le répertoire de page du processus en court est mise à jour
Sinon
Le processus en court sort en erreur

Fin

Le gestionnaire de défaut de page est implémenté dans le fichier interrupt.c par la fonction isr_PF_exc().

#include "types.h"
#include "list.h"
#include "screen.h"
#include "io.h"
#include "kbd.h"
#include "lib.h"
#include "process.h"
#include "schedule.h"
#include "mm.h"
#include "kmalloc.h"
#include "syscalls.h"

void isr_default_int(void)
{
        printk("interrupt\n");
}

void isr_GP_exc(void)
{
        printk("#GP\n");
        asm("hlt");
}

void isr_PF_exc(void)
{
        u32 faulting_addr, code;
        u32 eip;
        struct page *pg;

        asm("   movl 60(%%ebp), %%eax   \n \
                mov %%eax, %0           \n \
                mov %%cr2, %%eax        \n \
                mov %%eax, %1           \n \
                movl 56(%%ebp), %%eax   \n \
                mov %%eax, %2"

                : "=m"(eip), "=m"(faulting_addr), "=m"(code));

        printk("DEBUG: isr_PF_exc(): #PF on eip: %p. cr2: %p code: %p\n", eip, faulting_addr, code);

        if (faulting_addr >= USER_OFFSET && faulting_addr < USER_STACK) {
                pg = (struct page *) kmalloc(sizeof(struct page));
                pg->p_addr = get_page_frame();
                pg->v_addr = (char *) (faulting_addr & 0xFFFFF000);
                list_add(&pg->list, &current->pglist);
                pd_add_page(pg->v_addr, pg->p_addr, PG_USER, current->pd);
        }
        else {         
                printk("Segmentation fault on eip: %p. cr2: %p code: %p\n", eip, faulting_addr, code);
                sys_exit(1);
        }
}

void isr_clock_int(void)
{
        static int tic = 0;
        static int sec = 0;
        tic++;
        if (tic % 100 == 0) {
                sec++;
                tic = 0;
                //// putcar('.');
        }
        schedule();
}

void isr_kbd_int(void)
{
        uchar i;
        static int lshift_enable;
        static int rshift_enable;
        static int alt_enable;
        static int ctrl_enable;

        do {
                i = inb(0x64);
        } while ((i & 0x01) == 0);

        i = inb(0x60);
        i--;

        if (i < 0x80) {         /* touche enfoncee */
                switch (i) {
                case 0x29:
                        lshift_enable = 1;
                        break;
                case 0x35:
                        rshift_enable = 1;
                        break;
                case 0x1C:
                        ctrl_enable = 1;
                        break;
                case 0x37:
                        alt_enable = 1;
                        break;
                default:
                        putcar(kbdmap
                               [i * 4 + (lshift_enable || rshift_enable)]);
                }
        } else {                /* touche relachee */
                i -= 0x80;
                switch (i) {
                case 0x29:
                        lshift_enable = 0;
                        break;
                case 0x35:
                        rshift_enable = 0;
                        break;
                case 0x1C:
                        ctrl_enable = 0;
                        break;
                case 0x37:
                        alt_enable = 0;
                        break;
                }
        }

}

Il peut être testé avec le programme suivant qui essaye d'écrire dans l'espace utilisateur à une adresse non allouée :

#include "libc.h"
#include "syscalls.h"

int main(void)
{

        *((int*) 0xA0000000) = 0xdeadbeef;

        exit();

        /* never goes there */
        return 0;
}

Conséquences pour le code du noyau

Une conséquence importante de ce nouveau gestionnaire est qu'il n'est plus besoin d'allouer de la mémoire à un processus lors de sa création : cette allocation peut se faire à la demande par le biais des défauts de page. Celà permet de supprimer des portions de code qui sont maintenant inutiles. Par exemple, dans la fonction load_elf(), le code suivant :

if (p_entry->p_type == PT_LOAD) {
        v_begin = p_entry->p_vaddr;
        v_end = p_entry->p_vaddr + p_entry->p_memsz;

        /* Allocation memoire pour l'espace d'adressage utilisateur */
        for (v_addr = v_begin; v_addr < v_end; v_addr += PAGESIZE) {

                if (v_addr < USER_OFFSET) {
                        printk ("INFO: load_elf(): can't load executable below %p\n", USER_OFFSET);
                        return 0;
                }

                if (v_addr > USER_STACK) {
                        printk ("INFO: load_elf(): can't load executable above %p\n", USER_STACK);
                        return 0;
                }

                if (get_p_addr((char *) v_addr) == 0) {
                        pg = (struct page *) kmalloc(sizeof(struct page));
                        pg->p_addr = get_page_frame();
                        pg->v_addr = (char *) (v_addr & 0xFFFFF000);
                        list_add(&pg->list, pglist);
                        pd_add_page(pg->v_addr, pg->p_addr, PG_USER, pd);
                }
        }

        memcpy((char *) v_begin, (char *) (file + p_entry->p_offset), p_entry->p_filesz);

        if (p_entry->p_memsz > p_entry->p_filesz)
                for (i = p_entry->p_filesz, p = (char *) p_entry->p_vaddr; i < p_entry->p_memsz; i++)
                        p[i] = 0;
}

est remplacé par le code ci-dessous, bien plus court :

if (p_entry->p_type == PT_LOAD) {
        v_begin = p_entry->p_vaddr;
        v_end = p_entry->p_vaddr + p_entry->p_memsz;

        if (v_begin < USER_OFFSET) {
                printk ("INFO: load_elf(): can't load executable below %p\n", USER_OFFSET);
                return 0;
        }

        if (v_end > USER_STACK) {
                printk ("INFO: load_elf(): can't load executable above %p\n", USER_STACK);
                return 0;
        }

        memcpy((char *) v_begin, (char *) (file + p_entry->p_offset), p_entry->p_filesz);

        if (p_entry->p_memsz > p_entry->p_filesz)
                for (i = p_entry->p_filesz, p = (char *) p_entry->p_vaddr; i < p_entry->p_memsz; i++)
                        p[i] = 0;
}

À l'appel de la fonction memcpy(), les défauts de page provoqués par la copie entraineront la mise à jour automatique du répertoire de page du processus courant. C'est magique !

Attention, il y a cependant un petit piège car l'implémentation du gestionnaire de défaut de page est telle que current (qui pointe sur le processus en court d'exécution) doit toujours pointer sur le processus concerné par le défaut de page. Je vais illustrer ce point un peu compliqué par un exemple.
Supposons qu'un shell lance un nouveau processus. À l'exécution de la fonction load_task(), current pointe sur la structure du processus en court qui est ici le shell. L'appel à load_elf() charge en mémoire le code exécutable du nouveau processus et génére des défauts page. Ces défauts de page vont ajouter des informations dans la structure pointée par current, mais nous ne voulons pas mettre à jour la structure de processus du shell ! Il faut donc en préalable à cet appel faire pointer current sur le processus en court de création (attention, celà nécessite aussi que la fonction load_task() ne soit pas préemptée en court d'exécution) :

/*
 * On change d'espace d'adressage pour passer sur celui du nouveau
 * processus.
 * Il faut faire pointer 'current' sur le nouveau processus afin que
 * les defauts de pages mettent correctement a jour les informations
 * de la struct process.
 */

previous = current;
current = &p_list[pid];
asm("mov %0, %%eax; mov %%eax, %%cr3"::"m"(p_list[pid].pd->base->p_addr));

/*
 * Charge l'executable en memoire. En cas d'echec, on libere les
 * ressources precedement allouees.
 */

file = ext2_read_file(hd, inode);
e_entry = (u32) load_elf(file);
kfree(file);

Avant que la fonction ne retourne, le pointeur current doit être rétabli :

current = previous;
asm("mov %0, %%eax ;mov %%eax, %%cr3":: "m"(current->regs.cr3));

En prenant en considération ces remarques, le code du noyau a été modifié et les allocations mémoires pour les processus sont maintenant effectuées par le gestionnnaire de défauts de page :


< Une méthode générique pour gérer les listes chaînées | TutoOS | Un premier shell >

print · rss · source
Page last modified on December 15, 2008, at 10:20 PM