我的目标

没有达到。让每个任务有4GB虚拟内存的努力失败了。仅仅开启了分页机制和页面失效异常。非常失望。

下载代码


分页

386处理器为每个任务提供了独立地内存空间和内存保护的机制,最好的放最后,每个任务都可以存取4GB的内存。

这些机制可以分成两部分:分段和分页。分段从第一节教程中就开始使用了,它允许每个任务有独立地代码、数据和堆栈模块。分页允许按照需要将内存映射到磁盘上,我们准备在这一节中使用它。

因为我们不可能真让每个任务都有4GB物理内存,所以我们必须用其它东西来虚拟内存空间,这个过程被处理器的分页机制处理。它把每个段分成多个页面(我们准备用4KB大小的页),每个页可以存储在磁盘或内存中。操作系统通过页目录和页表跟踪它们的页状态。页目录存储着关于页表的信息,页表存储着关于页的信息。

当允许分页时,处理器以如下的步骤转换一个给定的地址,

处理器使用的数据结构是页目录和页表,它们都是包含32位项的数组。

Page table entry format

第一个图是页表项的格式,第二个图是页目录项的格式。如你所见,它们的格式很相似,先看一下相同的部分,

位0P页或页表是否在内存中。当p=0是,页不在内存,页面失效异常被触发
位1R/W一页或多页(是页目录项的时候)是只读(=0)还是可写(=1)
位2U/S一页或多页(是页目录项的时候)的权限,当它们在管理者级别时(=0),只有PL0-2能访问它们;在用户级别时(=3),每一个任务都可以访问它们
位3, 4, (6), 7, 8XIntel保留,设为0
位5A一页或多页是否被存取
位9-11用户定义我们准备使用位11来指示如果它不在内存的话,是否存在于磁盘中

在页目录中的页表的物理地址存储了页表地址的最高20位。因为只有20位,所以页表必须4KB对齐。页表项的31-12位存储了它指向页面的地址的最高20位,因为它有20位,所以它可以表示2^20 = 1M个页面,也就是1M*4K = 4GB的内存空间。页表项中的D位表示了页面内容是否被改变,当把此页换出时它是有用的,如果此页还没有被修改过并且本页是从磁盘载入的,那么我们可以仅仅把此页抛弃而不必再把它写入磁盘。

为了转换逻辑地址到物理地址,逻辑地址被分成三部分,

位31-22页目录项的索引,我们可以得到它指向的页表的物理地址
位21-12页表项的索引,我们可以得到它指向的页的物理地址
位11-0页中的偏移

例如,我们有逻辑地址0x3E837B0A,我们检查他的头10位,是0x0FA,所以它指向页目录的第0x0FA项,假如它开始于0x0005C000,我们再接着看这项的头20位以得到页表的地址,假如是0x0003F000,接着我们再看逻辑地址的第二个10位0x037,计算开始于0x0003F000的页表的第0x037项的头20位,我们就可以得到页面的物理地址,假如是0x0001B000,接着我们获得物理地址的最后12位0xB0A,最终我们把它们加到一起得到物理地址0x0001B000+0xB0A = 0x0001BB0A。

但是这里有个问题,我们怎样才能找到头呢?答案是为页目录准备的新寄存器:CR3,它保存了当前使用的页目录的物理地址,所以它也被称为PDBR。

page translation

CR3必须在允许分页前被装入,它的值可以被MOV指令改变或被任务切换时TSS结构中的CR3域改变。

一旦处理器遇到一个不存在的页或页表或者权限冲突,页面无效异常程序被执行。CR2存储了导致这个异常的逻辑地址,错误码被压入栈,格式如下,

error code for page fault

异常处理程序通常做如下步骤,

处理器把最近使用的页目录项和页表项存在一个叫做快表(TLBs)的缓存中,因此只有当要访问的项在快表中不存在时才会访问页目录和页表。只要我们修改了页目录或页表的内容,快表必须被无效,它才会丢弃旧有内容, 但是我们不能直接访问快表,所以我们通过MOV一个新值到CR3的办法,或者任务切换也可以。

看一下代码段,定义了一些常量

08/include/kernel.h

#define PAGE_DIR        ((HD0_ADDR+HD0_SIZE+(4*1024)-1) & 0xfffff000)

把页目录放在IDT之后,页目录必须4KB对齐。

#define PAGE_SIZE       (4*1024)

#define PAGE_TABLE      (PAGE_DIR+PAGE_SIZE)

把页表放在页目录之后。

#define MEMORY_RANGE    (4*1024*1024)

Skelix使用4MB内存。

08/mm.c

static char mmap[MEMORY_RANGE/PAGE_SIZE] = {PG_REVERSED, };

这是物理内存位图。

void

mm_install(void) {

    unsigned int *page_dir = ((unsigned int *)PAGE_DIR);

    unsigned int *page_table = ((unsigned int *)PAGE_TABLE);

    unsigned int address = 0;

    int i;

    for(i=0; i<MEMORY_RANGE/PAGE_SIZE; ++i) {

        /* attribute set to: kernel, r/w, present */

        page_table[i] = address|7;

        address += PAGE_SIZE;

    };

初始化0-4MB内存的页表项。

    page_dir[0] = (PAGE_TABLE|7);

因为一个页目录项可以表示4MB内存,所以我们只需要设置页目录的第一项就可以了。

    for (i=1; i<1024; ++i)

        page_dir[i] = 6;

随后的1023个页目录项,1024项共可以表示4GB内存空间。

    /* set lower 1MB memory to used */

    for (i=(1*1024*1024)/PAGE_SIZE-1; i>=0; --i)

        mmap[i] = PG_REVERSED;

因为内核使用最低的1MB内存,所以我们把这些页面保留,不允许换出,让它们总是存于内存。

    __asm__ (

        "movl    %%eax,    %%cr3\n\t"

        "movl    %%cr0,    %%eax\n\t"

        "orl     $0x80000000,%%eax\n\t"

        "movl    %%eax,    %%cr0"::"a"(PAGE_DIR));

}

通过设定CR0第31位,我们开启了分页,简单吧。

我们可以简单的通过搜索mmap找到一个未分配的页。

unsigned int

alloc_page(int type) {

    int i;

 

    for (i=(sizeof mmap)-1; i>=0 && mmap[i]; --i)

        ;

    if (i < 0) {

        kprintf(KPL_PANIC, "NO MEMORY LEFT");

        halt();

    }

    mmap[i] = type;

    return i;

}

 

void *

page2mem(unsigned int nr) {

    return (void *)(nr * PAGE_SIZE);

}

 

void

do_page_fault(enum KP_LEVEL kl,

              unsigned int ret_ip, unsigned int ss, unsigned int gs,

              unsigned int fs, unsigned int es, unsigned int ds, 

              unsigned int edi, unsigned int esi, unsigned int ebp,

              unsigned int esp, unsigned int ebx, unsigned int edx, 

              unsigned int ecx, unsigned int eax, unsigned int isr_nr, 

              unsigned int err, unsigned int eip, unsigned int cs, 

              unsigned int eflags,unsigned int old_esp, unsigned int old_ss) {

    unsigned int cr2, cr3;

    (void)ret_ip; (void)ss; (void)gs; (void)fs; (void)es; 

    (void)ds; (void)edi; (void)esi; (void)ebp; (void)esp; 

    (void) ebx; (void)edx; (void)ecx; (void)eax; 

    (void)isr_nr; (void)eip; (void)cs; (void)eflags; 

    (void)old_esp; (void)old_ss; (void)kl;

    __asm__ ("movl %%cr2, %%eax":"=a"(cr2));

    __asm__ ("movl %%cr3, %%eax":"=a"(cr3));

    kprintf(KPL_PANIC, "\n  The fault at %x cr3:%x was caused by a %s. "

            "The accessing cause of the fault was a %s, when the "

            "processor was executing in %s mode, page %x is free\n", 

            cr2, cr3,

            (err&0x1)?"page-level protection voilation":"not-present page", 

            (err&0x2)?"write":"read", 

            (err&0x4)?"user":"supervisor",

            alloc_page(PG_NORMAL));

}

异常处理程序只是输出一些关于本异常的信息。

动态分配内存,new_task这样改变,

static void

new_task(unsigned int eip) {

    struct TASK_STRUCT *task = page2mem(alloc_page(PG_TASK));

    memcpy(&(task->tss), &(TASK0.tss), sizeof(struct TSS_STRUCT));

 

    task->tss.esp0 = (unsigned int)task + PAGE_SIZE;

    task->tss.eip = eip;

    task->tss.eflags = 0x3202;

    task->tss.esp = (unsigned int)page2mem(alloc_page(PG_TASK))+PAGE_SIZE;

 

    task->priority = INITIAL_PRIO;

    task->ldt[0] = DEFAULT_LDT_CODE;

    task->ldt[1] = DEFAULT_LDT_DATA;

 

    task->next = current->next;

    current->next = task;

    task->state = TS_RUNABLE;

}

现在,让我们把mm_install加入到08/init.c,不要忘记修改08/exceptions.c中相应的行,接着尝试存取4MB内存以上的空间。

08/init.c

    idt_install();

    pic_install();

    mm_install();      /* &&&&& Her it is */

    kb_install();

08/exceptions.c

void

page_fault(void) {

    __asm__ ("pushl    %%eax;call    do_page_fault"::"a"(KPL_PANIC));

    halt();

}

最后,在Makefile中把mm.o加入到KERNEL_OBJS中去。

08/Makefile

KERNEL_OBJS= load.o init.o isr.o timer.o libcc.o scr.o kb.o task.o kprintf.o hd.o \

        exceptions.o fs.o mm.o

making process of tutorial08 page fault exception


你可以自由使用我的代码,如有疑问请联系我