当前位置: 首页 > 文档资料 > Jos 学习笔记 >

6

优质
小牛编辑
134浏览
2023-12-01

接下来做part2,先上一张开启分页后的地址变换图:(完整的图在 http://pdos.csail.mit.edu/6.828/2011/lec/x86_translation_and_registers.pdf

然后再放一张具体的地址变换的图:

好当我们把这两张图也牢记于心的时候就可以开始实验的part2了。

1、实验要求

完成以下几个函数:

        pgdir_walk()
        boot_map_region()
        page_lookup()
        page_remove()
        page_insert()

然后通过mem_init()里面的check_page函数就算过关了。

虽然要求比较简单,但实现起来可真不容易。

2、原理

(1)地址变换

首先从硬件机制说起,当cpu拿到一个地址并根据这个地址访问主存时,在x86体系架构下要经过至少两级的地址变换,第一级成为段式地址变换而第二级成为页式地址变换。(为什么?主要是从安全、兼容老的os、考虑现代os等原因)

最原始的地址叫做虚拟地址,根据规定,将前16位作为段选择子,后32位作为偏移。根据段选择子查找gdt/ldt,查到的内容替加上偏移,此时的地址就变成了线性地址。

线性地址前10位被称作页目录入口(page directory entry也就是pde),其含义为该地址在页目录中的索引,中间10位为页表入口(page table entry,也就是pte),代表在页表中的索引,最后12位是偏移。

当一个线性地址进入页式地址变换机制时,首先cpu从cr3寄存器里得到页目录(page directory)在主存中的地址,然后根据这个地址加上pde得到该地址在页目录中对应的项。无论是页目录的项还是页表的项均是32位,前20位为地址,后12位为标志位。当获取了相应的页目录项之后,根据前20位地址得到页表所在地址,加上偏移pte得到页表项,取出前20位加上线性地址本身的后12位组成物理地址,整个变换过程结束。这也就是以上两个图所描述的功能。

值得注意的是,这个过程完全是由硬件实现,在这个部分的实验中要做的是初始化并维护页目录与页表,当页目录与页表维护好了,然后使cr3装载新的页目录,一切就交由硬件去处理地址变换了。

这里还有两个点需要说下:

1、为什么是20位就能表示页表所在地址?因为页表的分配以页为单位,换句话分配的过程是分配一个页面,而这个页面所有内容都用来当做页表,而页正好是4k,所以页表地址必定是4k对齐。

2、一个页表项对应一页(也就是4K内容),因为其后面有12位的偏移。一个页表有1024个页表项,因为一个页表大小为4K,一个页表项为32位(4B)。一个页目录项对应一个页表,对应4K*1024=4M空间的映射。一个页目录有1024个页目录项,对应4M*1024=4G地址的映射,正好是32位地址空间。

3、一个理发师只为不自己理发的人理发,当然我们在这里不套路罗素悖论。段页地址变换只为虚拟地址进行地址变换,而不为它本身的地址进行变换。换句话说变换过程中所出现的任何地址都是物理地址,在编写代码的时候尤其要注意这一点。

(2)JOS的相关部分

之前的日志里已经说过,JOS的机制中,虽然使用段式地址变换,但和没使用完全一样,因为只定义了一个段,其长度为4G,换句话说,经过段式地址变换后的内容和之前完全一样,虚拟地址和线性地址完全一样。

在part2中也就是mem_init()执行环境里,JOS已经开启了页式地址变换,但变换的比较粗糙,只是简单的把0--4M物理地址分别映射到了0--4M物理地址和0xf0000000开始的4M 地址处,之前也已经详细说过这个问题。但同时也感谢一下这种“粗糙”的变换方式,让我们在编程填充页表和页目录的时候方便了许多。(为什么?因为要往里填充物理地址,存在一个把当前符号地址转化成物理地址的过程,因为变换的粗糙,所以这个过程相对简单)。

值得注意的是,在代码里我们的所有地址均为虚拟地址,不同通过任何方法直接得到某个符号实际存在于物理内存中的地址,只有通过算才能得到我们需要的物理地址。

3、实现

(1)基本数据类型与函数说明

  • pde_t 代表一个页目录项
  • pte_t 代表一个页表项
  • pgdir_walk() 用于查找某个虚拟地址是否有页表项,如果没有也可以通过此函数创建,值得注意的是,有页表项并不代表已经被映射。
  • boot_map_region() 映射一个虚拟地址区间到一个物理地址区间,貌似在本部分没用到。
  • page_insert() 将一个虚拟地址映射到一个Page数据结构,也就是映射到某个物理地址。
  • page_lookup() 查找一个虚拟地址对应的Page数据结构,若没有映射返回空。
  • page_remove() 解除某个虚拟地址的映射。

(2)具体实现

pte_t *  
pgdir_walk(pde_t *pgdir, const void *va, int create)  
{  
    cprintf("pgdir_walk\r\n");  
    // Fill this function in  
    pte_t* result=NULL;  
    if(pgdir[PDX(va)]==(pte_t)NULL)  
    {  
        if(create==0)  
        {  
            return NULL;  
        }  
        else  
        {  
            struct Page* page=page_alloc(1);  
            if(page==NULL)  
            {  
                return NULL;  
            }  
            page->pp_ref++;  
            pgdir[PDX(va)]=page2pa(page)|PTE_P|PTE_W|PTE_U;  
            result=page2kva(page);  
        }  
    }  
    else  
    {  
        //cprintf("%u ",PGNUM(PTE_ADDR(pgdir[PDX(va)])));  
        result=page2kva(pa2page(PTE_ADDR(pgdir[PDX(va)])));  
    }  
        return &result[PTX(va)];  


}

思路:

首先明确一点,返回地址是虚拟地址,要不然没有任何意义(在程序中拿到物理地址也没法用)。

查找页目录表,根据宏PDX取得页目录项(相关宏定义在mmu.h中),如果不为空,取出该项内容的前20位(PTE_ADDR宏),这是物理地址,通过此物理地址查找对应的Page结构(pa2page宏),然后获得此Page的虚拟地址(page2kva宏)。

此时的地址为页表的虚拟地址,根据偏移得到页目录项,在返回此页目录项地址。

如果前20位不为空,检查create,如果为0,返回null。否则新分配一个Page作为页表,然后自增Page 的引用,让该页目录项的前20位为页表物理地址(page2pa得到物理地址),并设置一些权限符号(不加通不过最后的检测函数),在通过此页表的虚拟地址得到相应页表项的虚拟地址并返回。

boot_map_region暂时略过,等到用的时候再说。

struct Page *  
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)  
{  
    cprintf("page_lookup\r\n");  
    // Fill this function in  
    pte_t* pte=pgdir_walk(pgdir,va,0);  
    if(pte==NULL)  
    {  
        return NULL;  
    }  
    if(pte_store!=0)  
    {  
        *pte_store=pte;  
    }  

    if(pte[0] !=(pte_t)NULL)  
    {  
        //cprintf("%x \r\n",pte[PTX(va)]);  
        return pa2page(PTE_ADDR(pte[0]));  

    }  
    else  
    {  
        return NULL;  
    }  
}

首先使用pgdir_walk查找pte,如果为空则说明没有映射,返回NULL。

否则根据pte_store是否为0,先把此pte的地址存到pte_store里。

接着看这个页表项是否为0(更正确的写法应该是pte[0] & PTE_U,也就是查找PTE_U这一位是否为0,但貌似我这么写也没出错),如果为0,说明地址没有被映射(有页表项不代表被映射),返回NULL。

否则返回这个页表项的前20位所组成的物理地址所对应的Page结构。

void  
page_remove(pde_t *pgdir, void *va)  
{  
    cprintf("page_remove\r\n");  
    pte_t* pte=0;  
    struct Page* page=page_lookup(pgdir,va,&pte);  
    if(page!=NULL)  
    {  
        page_decref(page);  
    }  

    pte[0]=0;  
    tlb_invalidate(pgdir,va);  
}

这个函数逻辑很简单,首先查找此va对应的物理页面,如果此页面不为空,说明va已经映射到了物理页面,减少这个物理页面的引用次数(次数为0就释放这个页面了,相关逻辑封装在了page_decref中),然后置相应的页表项为0,并通知tlb失效。tlb是个高速缓存,用来缓存查找记录增加查找速度。

int  
page_insert(pde_t *pgdir, struct Page *pp, void *va, int perm)  
{  
    cprintf("page_insert\r\n");  
    // Fill this function in  
    pte_t* pte;  
    struct Page* pg=page_lookup(pgdir,va,NULL);  
    if(pg==pp)  
    {  
        pte=pgdir_walk(pgdir,va,1);  
        pte[0]=page2pa(pp)|perm|PTE_P;  
            return 0;  
    }  
    else if(pg!=NULL )  
    {  
        page_remove(pgdir,va);  
    }  
    pte=pgdir_walk(pgdir,va,1);  
    if(pte==NULL)  
    {  
        return -E_NO_MEM;  
    }  
    pte[0]=page2pa(pp)|perm|PTE_P;  
    pp->pp_ref++;  
    return 0;  
}

首先查找该va是否已经映射到了某个物理页面,如果映射到了,则解除映射。

使用pgdir_walk查询该地址的pte(若不存在则建立,第三个参数传入1),如果pte为空,说明没有额外的空间分配页表,因此返回-E_NO_MEM。

否则将该pte的内容填充成相应物理页面的物理地址,增加这个物理页面的引用次数。

存在一个问题,若此时的va已经映射到了物理页面,而这个页面恰好又是函数传入的pp,则这段代码不能正常的工作。

假设这个物理页面恰好是pp,则会调用page_remove尝试释放这个pp,又假设这个pp的引用次数正好又是1,则这个pp就会被释放掉,进入空闲页面的链表中。

但问题是这个pp不是空闲页面,虽然之后又为其建立了映射,但将Page结构从空闲链表中取出来的逻辑仅仅包含在page_alloc函数中,因此我们又不能简单的将其取出来,这要就会导致一个非空闲的页面(其引用是1)却出现在了空闲页面的链表中。

虽然实验中再三要求不使用特例的方式进行实现,但我实在找不到更美的方式(把remove逻辑或者page_alloc部分逻辑拿出来在这个函数里重新实现一次诸如此类的方法),所以我还是用了特例进行实现,若发现引用了同一个页面,就直接改pte即可。

基本到此这部分实验就结束了,运行后能通过: