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可能已经和其它页有映射关系,那么在建立新的映射关系的时候,就要移除之前的映射关系。
来源:CSDN
作者:小狗过河
链接:https://blog.csdn.net/weixin_43590611/article/details/103921977