天衣有缝

冠盖满京华,斯人独憔悴~
posts - 35, comments - 115, trackbacks - 0, articles - 0
   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

 

8课:内存管理    下载源代码

 

声明:转载请保留:

译者:http://www.cppblog.com/jinglexy

原作者:xiaoming.mo at skelix dot org

MSN & Email: jinglexy at yahoo dot com dot cn

 

 

目标

 

抱歉,其实还没有实现。在任务分配独立的4G地址空间上调试失败了,现在只使能了分页机制,页异常。大量的工作未实现,有兴趣的同学可以搜索buddyslab的相关资料,经典的内存管理算法。


分页

 

386处理器的内存管理单元可以实现任务独立地址空间,任务间内存保护。每个任务可以拥有独立的4G虚拟地址空间。内存映射是内存管理很重要的一步,可以分为两部分:分段和分页。前面的课程中已经讨论过分段机制了,通过分段可以隔开不同的代码,数据,堆栈等;分页单元把虚拟地址映射成物理地址,还可以用来实现虚拟内存(和硬盘分区进行交换),现在我们来了解一下它。

 

对于每个任务,我们无法分配4G的物理内存,所以使用了一些机制来管理内存:及虚拟内存机制。该机制有处理器的分页部分来实现,首先我们将内存分成一些块,每个块大小为4k,通常我们称之为一个页帧。操作系统通过页目录和页表来管理这些页帧。页目录是相当于第一级页表,其中的每一项再管理一个下级页表。(更详细过程请参考intelIA 32/64手册)

当分页机制开启时,处理器把任务中的虚拟地址转换成物理地址,步骤如下:
1.
查找段选择子在GDT LDT 中的描述符,做一些权限检查,看看能否访问

2.以描述符中的基址相加页目录基址得到一个线性地址

3.在页表中索引虚拟地址所对应的页表项,得到页地址

4.查找偏移得到实际物理地址。

如果实际物理页不存在(可能交换到硬盘中去了),则引发异常,可以在这个异常里面做想要做的事情(加载硬盘中的交换页,或者kill这个程序:Segment Fault,等等)

处理器使用的页目录或者页表,都是由32 位的项组成:

页目录项:

 31                    12    11    9    876   5   43    2     1     0

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

┃   指向页表的物理地址  ┃ 用户定义 ┃  X  ┃ A┃ X ┃ U/S┃ R/W┃ P ┃

┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

 

页表项:

 31                    12    11    9   87  6  5   43    2     1     0

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

┃   指向页帧的物理地址  ┃ 用户定义 ┃ X┃D┃ A┃ X ┃ U/S┃ R/W┃ P ┃

┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

 

从上面可以知道,页目录项和页表项的结构很类似,下面逐个说明一下其中的域:

 

Bit  0

P

存在位(present),为0 表示该页帧或页表不在内存中。如果访问该项将发生异常。

Bit  1

R/W

表示页表或页帧指向的内存只读(=0),或可写(=1

Bit  2

U/S

表示页表或页帧的权限,当特权级为0时,只有ring02的特权级可以访问它,否则所有的ring3任务都可以访问。这个域非常重要。

Bits 3, 4, (6), 7, 8

X

Intel 保留位,设置为0就行了

Bit  5

A

该页是否已访问

Bits 9-11

用户定义

我们使用第11位,表示该页帧是否被交互到硬盘上了

 

页目录的每一项:即页表的物理地址,它的高20 位地址表示有个页帧的起始地址,正好和4k对齐。2^20可以表示1M范围,每个页帧大小是4k,所以可以索引1M * 4K地址空间。页目录项中还有一个D 位,它用来表示一个页帧是否已修改,linux用它来表示一个页面释放是脏页面,这个位非常有用,当一个页帧交换到硬盘上后,如果该页帧还没有被修改,而且是已经从硬盘交换出来的,则简单取消以后的交换。

 

为了将逻辑地址转换成物理地址,逻辑地址被分成3 部分:

Bits 31-22

页目录项的索引下标,由它可以得到页表的物理地址

Bits 21-12

页表项的索引下标,由它可以得到页帧的物理地址

Bits 11-0

相对页帧起始地址的偏移

 

举例来说,我们有一个逻辑地址:0x3E837B0A。前提条件:CR3寄存器指向的页目录地址是 0x0005C000,这个寄存器存储了当前页目录所使用的页帧的物理地址,通常也叫做 PDBR

 

先取它的高10位, 就是0x0FA,由它可以索引到页目录的第0x0FA项,我们取得这一项的值,假设得到的地址值是0x0003F000。然后我们取虚拟地址的中间10位,就是0x037,再取出0x0003F000指向页帧的第0x037项的值,假设是0x0001B000。这个地址就是我们要找的虚拟地址对应的物理地址的页帧的起始地址,最后加上偏移值(低12位),即0xB0A,得到实际的物理地址是:0x0001BB0A

相关的知识可以参考 Intel IA 32/64手册。

CR3寄存器必须在分页机制开启前就装载好,可以使用MOV 指令或者在任务切换时使用TSS中的CR3域的值。当处理器访问不存在的页帧时,发生一个异常,CR2 寄存器存引发异常的逻辑地址,同时错误码也会压入到堆栈中,错误码格式如下:

 31                                                 3   2     1     0

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

┃                    未使用                         ┃ U/S┃ R/W┃ P ┃

┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

 

异常处理例程通常采取如下的步骤:

查找一个空闲的页帧或从硬盘中将页帧交换出来,重新设置正确的页目录项或页表项的值,刷新TLB。处理器通常保存最近最多访问的页目录或页表项到一个cache中,以避免每次都进行虚拟地址到物理地址的转换,这个cache就叫做TLB。只有我们改动了页目录或页表项,就应当刷新TLB。方法很简单,就是重新加载CR3 寄存器。


现在我们来看看代码段,内存管理通常少不了大量的宏定义:

08/include/kernel.h

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

物理内存安排:IDT(在0x40000),接下来是GDT,接下来是HD0使用,然后才是页目录,

所以这个宏看起来有点长。


08/include/mm.h
#define PAGE_SIZE    (4*1024)                    /*
页帧粒度 */
#define PAGE_TABLE    (PAGE_DIR+PAGE_SIZE)       /*
页表物理地址 */
#define MEMORY_RANGE (4*1024)                    /* skelix
只管理4M 内存暂时 */


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) {
        /*
页表项属性设置为: kernel, r/w, present */
        page_table[i] = address|7;
        address += PAGE_SIZE;
    };

    // 上面循环初始化了0~4M对应的所有页表项


    page_dir[0] = (PAGE_TABLE|7);

    // 页目录项只需要第一个就可以了,因为只有4M内存

    for (i=1; i<1024; ++i)
        page_dir[i] = 6;

    // 其他的1023个页目录项设置为空,如果这1024项都设置,可访问4G内存空间

    // 设置01M内存为已使用。
    for (i=(1*1024*1024)/PAGE_SIZE-1; i>=0; --i)
        mmap[i] = PG_REVERSED;

    // 因为内核只用到了低于1M的内存,所以保留它们,这样就不会被交换出去了



    __asm__ (
        "movl    %%eax,    %%cr3\n\t"        //
加载页目录基址到寄存器
        "movl    %%cr0,    %%eax\n\t"
        "orl    $0x80000000,    %%eax\n\t"
        "movl    %%eax,    %%cr0"::"a"(PAGE_DIR));    //
开启分页机制,CR0的最高位
}

 

通过mmap位图,我们可以清楚的知道内存的使用情况,这样就可以分配空闲页帧了,如下:
08/mm.c

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));
}

页异常函数,它什么也没有做,知识显示一些错误信息。

现在我们来动态的分配一些内存,我们修改一下任务函数:
08/init.c

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->tss.cr3 = PAGE_DIR;
    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;
}

自己分配的任务数据结构和任务堆栈,是不是很有成就感:)

 

 

最后在init.c中添加初始化代码:
08/init.c

void
init(void) {
    char wheel[] = {'\\', '|', '/', '-'};
    int i = 0;

    idt_install();
    pic_install();
    mm_install();      /* 
初始化函数调用 */
    kb_install();
    timer_install(100);
    set_tss((unsigned long long)&TASK0.tss);
    set_ldt((unsigned long long)&TASK0.ldt);
    __asm__ ("ltrw    %%ax\n\t"::"a"(TSS_SEL));
    __asm__ ("lldt    %%ax\n\t"::"a"(LDT_SEL));

    kprintf(KPL_DUMP, "Verifing disk partition table....\n");
    verify_DPT();
    kprintf(KPL_DUMP, "Verifing file systes....\n");
    verify_fs();
    kprintf(KPL_DUMP, "Checking / directory....\n");
    verify_dir();

    sti();
    new_task((unsigned int)task1_run);
    new_task((unsigned int)task2_run);
    __asm__ ("movl %%esp,%%eax\n\t" \
             "pushl %%ecx\n\t" \
             "pushl %%eax\n\t" \
             "pushfl\n\t" \
             "pushl %%ebx\n\t" \
             "pushl $1f\n\t" \
             "iret\n" \
             "1:\tmovw %%cx,%%ds\n\t" \
             "movw %%cx,%%es\n\t" \
             "movw %%cx,%%fs\n\t" \
             "movw %%cx,%%gs" \
             ::"b"(USER_CODE_SEL),"c"(USER_DATA_SEL));
    __asm__ ("incb 0xeeffeeff");         /* 
测试:触发一个异常 */
    for (;;) {
        __asm__ ("movb    %%al,    0xb8000+160*24"::"a"(wheel[i]));
        if (i == sizeof wheel)
            i = 0;
        else
            ++i;
    }
}

 

异常处理例程中什么也没做,访问内存出错则死机:

08/exceptions.c

void
page_fault(void) {
    __asm__ ("pushl    %%eax;call    do_page_fault"::"a"(KPL_PANIC));
    halt();
}

最后把mm.o 添加到 Makefile 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

 

 

Feedback

# re: 自己动手写内核(第8课:内存管理)(原创)  回复  更多评论   

2007-06-08 13:24 by 星梦情缘
嘿嘿,有点意思

只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理