ucore lab2 练习2&3

三世轮回 提交于 2020-02-02 09:24:41

ucore lab2 练习2&3

回忆一下之前的练习1,我们填写了default_pmm.c中的一些函数,也实现了内存分配的first fit算法。但是,其实我们并不是很清楚,default_pmm.c这个文件在整个lab2中的位置,也就是说,这个文件中的函数,到底在哪些地方被调用了呢?想要解决这个问题,首先要搞清楚物理内存管理器pmm_manager。

1 物理内存管理器pmm_manager

pmm_manager在pmm.h中被声明:

struct pmm_manager {
    const char *name;
    void (*init)(void);
    void (*init_memmap)(struct Page *base, size_t n);
    struct Page *(*alloc_pages)(size_t n);
    void (*free_pages)(struct Page *base, size_t n);  
    size_t (*nr_free_pages)(void); 
    void (*check)(void); 
};

这里出现了函数指针,这是以前我们比较少接触的,它是实现面向对象编程的一种很重要的手段,因为它给提供了接口,而在实现这些接口的时候,可能会有不同的实现方法,这就是多态,而这正是面向对象的三大原则之一(抽象,继承,多态)。

而我们练习1做的事情,其实就是在实现这些接口。在default_pmm.c这个文件的最后,有这么一段代码:

const struct pmm_manager default_pmm_manager = {
    .name = "default_pmm_manager",
    .init = default_init,
    .init_memmap = default_init_memmap,
    .alloc_pages = default_alloc_pages,
    .free_pages = default_free_pages,
    .nr_free_pages = default_nr_free_pages,
    .check = default_check,
};

这段代码,一开始没有引起足够的重视,后来看到pmm.c这个文件屡次出现pmm_manager这些函数指针,又找不到这些函数指针的具体实现,后来在default_pmm.c的最后找到了这段代码,才发现在这里已经把这些函数指针实例化了。具体这些函数怎么实现,请参照练习1。

2 ucore的分页管理机制

解决了这个问题,我们开始完成练习2和3。之前的练习1,是完成页面的初始化,分配,回收等工作,而在页式(实际上在这里是段页式)管理系统中,管理这些页的是页表,一般是多级页表。在ucore中,页表有两级,为了区分一级页表和二级页表,有时候也会把一级页表称作页目录,把二级页表称作页表,以后不再对这两种说法加以区分。

还有一点需要说明一下,段页式管理系统中,有三类地址,虚拟地址,线性地址和逻辑地址。虚拟地址通过段表映射得到线性地址,线性地址通过多级页表映射得到物理地址。而在ucore中,段表映射采取的是对等映射,即虚拟地址和线性地址完全相等,而且这也不是这次实验的重点,因此不作讨论,这次我们着重探讨线性地址映射到物理地址的过程。

现在,先来熟悉一下ucore的分页管理机制:

在这里插入图片描述
值得注意的是,一个页目录和一个页表,也要占用一个页框(块)的内存。一个页目录的最大偏移量是10位,也就是说1个页目录有1K个页目录项,而ucore是32位地址,因此一个页目录的大小是4K,刚好是一个页框的大小(之前被这个问题困扰了很久,因为页框大小是4K,页目录项总共有1K,当时就很疑惑为什么这两个数不相等,后来才意识到一个页目录项大小是4B,因此有1K页目录项的话大小正好是4K)。

3 pmm.h pmm.c的一些函数

接下来,我们着重关注这些函数:get_pte, get_page, page_insert, page_remove, page_remove_pte。

pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
    pde_t *pdep = &pgdir[PDX(la)];
    if (!(*pdep & PTE_P)) {
        struct Page *page;
        if (!create || (page = alloc_page()) == NULL) {
            return NULL;
        }
        set_page_ref(page, 1);
        uintptr_t pa = page2pa(page);
        memset(KADDR(pa), 0, PGSIZE);
        *pdep = pa | PTE_U | PTE_W | PTE_P;
    }
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}
struct Page *
get_page(pde_t *pgdir, uintptr_t la, pte_t **ptep_store) {
    pte_t *ptep = get_pte(pgdir, la, 0);
    if (ptep_store != NULL) {
        *ptep_store = ptep;
    }
    if (ptep != NULL && *ptep & PTE_P) {
        return pte2page(*ptep);
    }
    return NULL;
}
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
    if (*ptep & PTE_P) {
        struct Page *page = pte2page(*ptep);
        if (page_ref_dec(page) == 0) {
            free_page(page);
        }
        *ptep = 0;
        tlb_invalidate(pgdir, la);
    }
}
void
page_remove(pde_t *pgdir, uintptr_t la) {
    pte_t *ptep = get_pte(pgdir, la, 0);
    if (ptep != NULL) {
        page_remove_pte(pgdir, la, ptep);
    }
}
int
page_insert(pde_t *pgdir, struct Page *page, uintptr_t la, 
	uint32_t perm) {
    pte_t *ptep = get_pte(pgdir, la, 1);
    if (ptep == NULL) {
        return -E_NO_MEM;
    }
    page_ref_inc(page);
    if (*ptep & PTE_P) {
        struct Page *p = pte2page(*ptep);
        if (p == page) {
            page_ref_dec(page);
        }
        else {
            page_remove_pte(pgdir, la, ptep);
        }
    }
    *ptep = page2pa(page) | PTE_P | perm;
    tlb_invalidate(pgdir, la);
    return 0;
}

3.1 get_pte

我们一个一个地去分析这些函数。首先看get_pte。

pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
    pde_t *pdep = &pgdir[PDX(la)];    
    	/*PDX(la)取出la的最高10位,也就是在页目录上的偏移量。因此pgdir[PDX(la)]就是la对
    	应的页目录项*/
    if (!(*pdep & PTE_P)) {    //包含la对应的页表项的页表不存在
        struct Page *page;
        if (!create || (page = alloc_page()) == NULL) {    
        	//create为false,不创建页表,或者分配空间给页表失败
            return NULL;
        }
        set_page_ref(page, 1);    //该页表被当前进程引用,计数为设为1
        uintptr_t pa = page2pa(page);    //得到该页表的物理起始地址
        memset(KADDR(pa), 0, PGSIZE);    //初始化该页表(即腾出空间)
        *pdep = pa | PTE_U | PTE_W | PTE_P;    //该页目录项的这些标志位设为1
    }
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];    //返回la对应的页表项
}

它的作用是得到某个线性地址la的页表项PTE,这一点从函数名和参数就可以看出来。get_pte的参数有三个,pgdir是页目录的起始地址。这是可以理解的,因为只有知道了页目录,才能去查找页表。la就是线性地址,我们现在就是要找到这个线性地址的页表项。crete可能会有点难理解,想要知道它的作用,就要认识到这一点,就是一个线性地址la,有可能当前的所有页表都不包含它对应的页表项的。因为我们不可能把所有的页表都列出来,那将会是非常庞大的,而页表不完全,也就导致了有些页表项没有列出来。因此,如果我们没有找到la对应的页表项,我们需要根据参数la来决定是否要创建一个新的页表,然后再写入对应的页表项。

这些就是get_pte的参数,那么这个函数做了哪些事情呢?想要找到一个la对应的页表项,首先要根据la的最高10位,找到对应的页目录项;然后根据中间10位,和之前找到的页目录项,找到对应的页表项。这些在ucore的分页管理机制那里已经表示得很清楚了。

接下来分析代码,一些比较容易懂的看注释就可以了,挑一些比较难的又比较重要的分析一下。之前我们提到,la对应的页表项可能不存在于现存的任何一张页表中,而存在不存在这一点是怎么判断出来的呢?

if (!(*pdep & PTE_P)) {    //包含la对应的页表项的页表不存在

为什么这样就可以判断?这就涉及到几个说法:页对齐和标志位

页对齐是什么意思呢?ucore的一个页的大小是4K bit,也就是12位。因此,把所有页的起始地址都统一成最低12位为0,是可行而且非常必要的,为什么必要之后会提及。事实上ucore也是这样的,页的起始地址,最低12位就是全0。而页目录和所有的页表,实际上也是一个页,而页目录项就是页表的起始地址,因此页目录项的最低12位自然也是全0(如果不考虑之后提到的标志位的话),这就是页对齐。

那么既然最低12位全0,事实上这些位就被浪费了,因为我们只需要高22位,就可以进行查找,可事实上低12位还是被保留了下来,它们有什么作用吗?事实上这最低12位就起到了标志位的作用。而PTE_P,我们可以在mmu.h找到它的宏定义:

/* page table/directory entry flags */
#define PTE_P           0x001                   // Present
#define PTE_W           0x002                   // Writeable
#define PTE_U           0x004                   // User
#define PTE_PWT         0x008                   // Write-Through
#define PTE_PCD         0x010                   // Cache-Disable
#define PTE_A           0x020                   // Accessed
#define PTE_D           0x040                   // Dirty
#define PTE_PS          0x080                   // Page Size
#define PTE_MBZ         0x180                   // Bits must be zero
#define PTE_AVAIL       0xE00                   // Available for software use
                                                // The PTE_AVAIL bits aren't used by the kernel or interpreted by the
                                                // hardware, so user processes are allowed to set them arbitrarily.

#define PTE_USER        (PTE_U | PTE_W | PTE_P)

也就是说,当页目录项的最低位(也就是PTE_P位)为0时,该页目录项对应的页表不存在,或者说还未创建;当页表项的最低位为0是,该页表项对应的页不存在。因此,页目录项只需要与标志位作按位与运算,就可以判断该页目录项对应的页表是否存在。

接下来的操作就比较简单了,注释已经说得比较明确了,只是最后返回的语句可能有点不好理解:

return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];    //返回la对应的页表项

*pdep是la对应的页目录项,PDE_ADDR(*pdep) 把该目录项的最低12位置0,因为有些标志位为1,最低12位置0后才是页表的起始地址,KADDR(PDE_ADDR(*pdep)) 把页表的起始地址从物理地址转成逻辑地址,(pte_t *)KADDR(PDE_ADDR(*pdep)) 把这个逻辑地址(即一级指针)转成页目录项类型,PTX(la)取la的中间10位,也就是页表上的偏移量,((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)] 这个和一般的数组差不多,通过偏移量在页表上作偏移计算,最后找到要的页表项,而返回值要求的是页表项的指针,因此通过取址符号取地址。

3.2 get_page

//get_page - get related Page struct for linear address la using PDT pgdir
struct Page *
get_page(pde_t *pgdir, uintptr_t la, pte_t **ptep_store) {
    pte_t *ptep = get_pte(pgdir, la, 0);    //找到la对应的页表项
    if (ptep_store != NULL) {
        *ptep_store = ptep;    //把页表项存到ptep_store
    }
    if (ptep != NULL && *ptep & PTE_P) {
        return pte2page(*ptep);    //返回页表项对应的Page实例
    }
    return NULL;
}

get_page的作用是得到线性地址la对应的struct Page的实例。并且把la对应的页表项存到参数ptep_store。这个函数比较好理解,不做过多解释。

3.3 page_remove_pte

static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
    if (*ptep & PTE_P) {    //页表项存在
        struct Page *page = pte2page(*ptep);    //页表项对应的页
        if (page_ref_dec(page) == 0) {    
            //页引用次数减1,若此时引用次数为0,则释放该页
            free_page(page);
        }
        *ptep = 0;    //该页表项清零
        tlb_invalidate(pgdir, la);    //快表中该项失效
    }
}

这个函数的作用是移除la对应的页表项,并且把对应的页引用次数减1。

3.4 page_remove

//page_remove - free an Page which is related linear address la and has an validated pte
void
page_remove(pde_t *pgdir, uintptr_t la) {
    pte_t *ptep = get_pte(pgdir, la, 0);
    if (ptep != NULL) {
        page_remove_pte(pgdir, la, ptep);
    }
}

3.5 page_insert

//page_insert - build the map of phy addr of an Page with the linear addr la
// paramemters:
//  pgdir: the kernel virtual base address of PDT
//  page:  the Page which need to map
//  la:    the linear address need to map
//  perm:  the permission of this Page which is setted in related pte
// return value: always 0
//note: PT is changed, so the TLB need to be invalidate 
int
page_insert(pde_t *pgdir, struct Page *page, uintptr_t la, uint32_t perm) {
    pte_t *ptep = get_pte(pgdir, la, 1);    //la对应的页表项
    if (ptep == NULL) {    //没有la对应的页表项
        return -E_NO_MEM;
    }
    page_ref_inc(page);    //页引用次数+1
    if (*ptep & PTE_P) {
        struct Page *p = pte2page(*ptep);    //找到之前的映射关系
        if (p == page) {    //之前的映射关系和现在一致
            page_ref_dec(page);
        }
        else {
            page_remove_pte(pgdir, la, ptep);    //移除之前的映射关系
        }
    }
    *ptep = page2pa(page) | PTE_P | perm;
    tlb_invalidate(pgdir, la);
    return 0;
}

这个函数用来建立线性地址la与页的联系,也就是说,给定一个la和结构体Page的一个实例,要建立它们的映射关系。要注意的是,之前的la可能已经和其它页有映射关系,那么在建立新的映射关系的时候,就要移除之前的映射关系。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!