实验二
该实验主要是动态分区分配、分页以及页表的相关内容。
练习1:实现 first-fit 连续物理内存分配算法(需要编程)
在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
- 你的first fit算法是否有进一步的改进空间
1-1 思路:
该问题是实现基于分页的动态分区分配算法,大致过程为:内存管理器顺着双向链表进行空闲区域的搜索,如果找到大小合适的区域,则将该空闲区域进行分配。但与单纯的动态分区分配不同,由于是基于分页的,所以当搜索的分区大小大于所需的内存时,就可以将分区分成两部分,一部分提供给申请的部分,剩下的则形成新的空闲区。
1-2 数据结构、宏以及用到的函数
//定义页的数据结构
struct Page {
int ref;
//ref表示的是,这个页被页表的引用记数,也就是映射此物理页的虚拟页个数。
//如果这个页被页表引用了,即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系,
//就会把Page的ref加一;反之,若页表项取消,即映射关系解除,就会把Page的ref减一。
uint32_t flags;
//flags表示此物理页的状态标记,有两个标志位状态,为1的时候,代表这一页是free状态,
//可以被分配,但不能对它进行释放;如果为0,那么说明这个页已经分配了,不能被分配,但是可以被释放掉。
unsigned int property;
//property用来记录某连续空闲页的数量,这里需要注意的是用到此成员
//变量的这个Page一定是连续内存块的开始地址(第一页的地址)。
list_entry_t page_link;
//page_link是便于把多个连续内存空闲块链接在一起的双向链表指针,
//连续内存空闲块利用这个页的成员变量page_link来链接比它地址小和大的其他连续内存空闲块,
//释放的时候只要将这个空间通过指针放回到双向链表中。
};
//双向链表
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
//定义宏
free_area_t free_area;
#define free_list (free_area.free_list) //维护所有空闲的内存块,是一个双向链表,在最开始时它的prev和next都指向自身。
#define nr_free (free_area.nr_free) //空闲页的数目
//所调用函数
assert(expression) //这里的含义是对表达式进行判断,如果为假则报错
PageReserved() //判断是否为保留页
set_page_ref() //更改映射到该页的虚拟页数
SetPageProperty() //设置连续页数
1-3 修改函数
default_init_memmap函数:该函数完成对空闲块列表的构建和初始化的功能。
修改方法:不难发现,应当在for循环中每初始化一次就将指针p自加,但没有连接到双向链表上,故需要增加连接的操作(提示中写到使用list_add_before),最后设置base(头部)的property与nr_free,设置的原因详见数据结构。
static void
default_init_memmap(struct Page *base, size_t n) {
//该函数用于初始化n个空闲块
//base为基础页,n为生成物理页的个数
assert(n > 0);
//若为假则报错,退出程序
struct Page *p = base;
for (; p != base + n; p ++) { //初始化n块物理页
assert(PageReserved(p)); //检查是否为保留页,如果是,初始化
p->flags = p->property = 0; //flag = 0表示可以分配,property = 0 表示连续空页为0
set_page_ref(p, 0); //映射到该页的虚拟页为0
list_add_before(&free_list, &(p->page_link));//插入空闲页的链表里面 添加代码
}
base->property = n; //第一页连续页有n
//SetPageProperty(base);被修改
nr_free += n; //更改空闲页数目
//list_add(&free_list, &(base->page_link));被修改
}
default_alloc_pages函数:该函数是用于分配空闲块的函数。大题逻辑是顺序查找,直到找到第一个满足的块,进行大小判断并重新分配(基于分页)。
修改方法:根据提示可以得知,在找到可以分配的块后,应当在循环内继续操作,循环外是在遍历一遍后未找到合适大小的内存块的情况。但所给代码只是找到了第一块可以分配的内存块,并未做任何操作,且剩余操作是在循坏外的操作,故为错误代码。应该将操作移至循环内。
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) { //需求太大
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list; //从头指针开始
while ((le = list_next(le)) != &free_list) { //还没被遍历一圈
struct Page *p = le2page(le, page_link); //换成对应page指针的页指针,记为p
if (p->property >= n) { //说明为第一块,且空间足够
page = p;
//break; 删除
//添加代码
int i;
for(i=0;i<n;i++){//把选中的空闲块链表中的每一个页结构初始化
len = list_next(le);
struct Page *pp = le2page(le, page_link);
SetPageReserved(pp);
ClearPageProperty(pp);
list_del(le);//从空闲页链表中删除这个双向链表指针
le = len;
}
if(p->property>n){
(le2page(le,page_link))->property = p->property - n;//如果选中的第一个连续的块大于n,只取其中的大小为n的块
}
ClearPageProperty(p);
SetPageReserved(p);
nr_free -= n;//当前空闲页的数目减n
return p;
}
}
/* 删除
if (page != NULL) {
list_del(&(page->page_link));
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
list_add(&free_list, &(p->page_link));
}
nr_free -= n;
ClearPageProperty(page);
}*/
return page;
}
default_free_pages函数:用于释放使用完的页,同时将使用完的空间再次融合进原链表。
修改方法:原代码的缺陷主要在三点,一是插入链表是并没有找到 base 相对应的位置,二是没有把页插入到空闲页表中,三是合并不合理。
static void default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
assert(PageReserved(base));
list_entry_t *le = &free_list;
struct Page *p;
//查找base位置
while((le=list_next(le)) != &free_list){
p = le2page(le, page_link);
if(p>base)
break;
}
//将页插入到空闲页
for(p = base; p<base+n; p++){
p->flags = 0;
set_page_ref(p, 0);
ClearPageReserved(p);
ClearPageProperty(p);
list_add_before(le, &(p->page_link));
}
base->property = n;
SetPageProperty(base);
//高位地址合并
p = le2page(le,page_link) ;
if(base+n == p){
base->property += p->property;
p->property = 0;
}
//低位地址合并
le = list_prev(&(base->page_link));
p = le2page(le, page_link);
if(p==base-1){
while (le != &free_list) {
if (p->property) {
p->property += base->property;
base->property = 0;
break;
}
le = list_prev(le);
p = le2page(le, page_link);
}
}
nr_free += n;
return ;
}
练习2:实现寻找虚拟地址对应的页表项(需要编程)
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个线性地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。
2-0 补充知识
下图为二级页表结构,在进行地址转换的过程中,先通过线性地址的前10位找到一级页表的entry,内含二级页表的基址,加上中间十位作为偏移量,即可得到二级页表的表项,内含物理页的基址,再加上offset即可得到具体的虚拟地址。
获取directory,table部分:(kern/mm/mmu.h,204——207行):
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF) //获得一级页表的一个表项,该表项内含二级页表的地址,但要注意,如果加入计算需要考虑偏移的问题,因为单纯十位不可能找到合适的地址。
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF) //获得二级页表的偏移值,和PDX(la)的值加和可以得到二级页表的一个表项,该表项内含物理地址的位置。
其中,***PDXSHIFT的值为22,*****右移22位,再与10个1与,就可以获取directory;
***PTXSHIFT的值为11,*****右移10位,再与11个1与,由于地址对齐的原因,0x3FF的11位之前都是0,这样就能提取table部分。
set_page_ref(page,1): //设置此页被引用一次
page2pa(page): //得到page管理的那一页的物理地址
KADDR(pa): //返回pa对应的虚拟地址(线性地址),注释里面如此说:takes a physical address and returns the //corresponding kernel virtual address.
PTE_P 0x001 // page table/directory entry flags bit : Present
PTE_W 0x002 // page table/directory entry flags bit : Writeable
PTE_U 0x004 // page table/directory entry flags bit : User can access
其中,PDE_ADDR被定义在(kern/mm/mmu.h,219——220行):
#define PTE_ADDR(pte) ((uintptr_t)(pte) & ~0xFFF)
#define PDE_ADDR(pde) PTE_ADDR(pde)
数据结构和宏
PDX(la): 返回虚拟地址la的页目录索引
KADDR(pa): 返回物理地址pa对应的虚拟地址
set_page_ref(page,1): 设置此页被引用一次
page2pa(page): 得到page管理的那一页的物理地址
struct Page * alloc_page() : 分配一页出来
memset(void * s, char c, size_t n) : 设置s指向地址的前面n个字节为‘c’
PTE_P 0x001 表示物理内存页存在
PTE_W 0x002 表示物理内存页内容可写
PTE_U 0x004 表示可以读取对应地址的物理内存页内容
2-2 思路
该问题思路较为简单,即根据二级页表的结构进行翻译即可。首先找到使用基址+offset(或者数组表示法)找到一级页表的具体表项,内含二级页表的基址。如果此时找到,则可以直接返回表达式,即二级页表的虚拟地址。
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
// pgdir: the kernel virtual base address of PDT
// la: the linear address need to map
// create: a logical value to decide if alloc a page for PT
// return vaule: the kernel virtual address of this pte 返回二级页表的内核虚拟地址
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); //得到page的物理地址
memset(KADDR(pa), 0, PGSIZE); //使用memset将新建的这个页表虚拟地址,全部设置为0,因为这个页所代表的虚拟地址都没有被映射。
*pdep = pa | PTE_U | PTE_W | PTE_P;
//设置二级页表为可写、可读、可使用
}
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
//PDE_ADDR(*pdep) 为线性地址
//KADDR(PDE_ADDR(*pdep)) 为虚拟地址
//PTX(la) 为二级页表的偏移值,此处使用数组形式表示等同于 虚拟基地址+offset
//最后转化为pte_t类型,返回pte的虚拟地址。
}
练习3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。
3-1 思路
与上一个实验相同,直接进行翻译即可:首先释放物理页,将页帧进行清空,同时要将二级页表的对应表项清除即可。对应文件与实验二相同,找到page_remove_pte函数。
3-2 函数
//取消映射关系,-1说明只被一个表项引用过
page_ref_dec(struct Page *page) {
page->ref -= 1;
return page->ref;
}
// invalidate a TLB entry, but only if the page tables being
// edited are the ones currently in use by the processor.
void
tlb_invalidate(pde_t *pgdir, uintptr_t la) {
if (rcr3() == PADDR(pgdir)) {
invlpg((void *)la);
}
}
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if (*ptep & PTE_P) { //PTE_P代表页存在
struct Page *page = pte2page(*ptep); //获得物理地址
if (page_ref_dec(page) == 0) { //无引用,可以清除
free_page(page);
}
*ptep = 0; //清除二级页表
tlb_invalidate(pgdir, la)
}
}
请在实验