《系统软件开发实践》 实验报告 第19页
二、实验说明
Lab0和lab1为必做实验内容,由于时间和精力有限,lab2到lab8的实验就暂时只能做并分析一下lab2 的实验内容了。
实验二主要涉及操作系统的物理内存管理。 操作系统为了使用内存,还需高效地管理内存资源。在实验二中我们会了解并且自己动手完成一个简单的物理内存管理系统。
三、实验原理与分析
本次实验主要完成ucore内核对物理内存的管理工作。参ucore总控函数kern_init的代码,可以清楚地看到在调用完成物理内存初始化的pmm_init函数之前和之后,是已有lab1实验的工作,好像没啥修改。其实不是的,ucore有两个方面的扩展。首先, bootloader的工作有增加,在bootloader中,完成了对物理内存资源的探测工作(可进一步参阅附录A和附录B),让ucore kernel在后续执行中能够基于bootloader探测出的物理内存情况进行物理内存管理初始化工作。其次,bootloader不像lab1那样,直接调用kern_init函数, 而是先调用位于lab2/kern/init/entry.S中的kern_entry函数。 kern_entry函数的主要任务是为执行kern_init建立一个良好的C语言运行环境(设置堆栈),而且临时建立了一个段映射关系, 为之后建立分页机制的过程做一个准备。完成这些工作后, 才调用kern_init函数。
kern_init函数在完成一些输出并对lab1实验结果的检查后,将进入物理内存管理初始化的工作,即调用pmm_init函数完成物理内存的管理,这也是我们lab2的内容。接着是执行中断和异常相关的初始化工作,即调用pic_init函数和idt_init函数等,这些工作与lab1的中断异常初始化工作的内容是相同的。
为了完成物理内存管理,这里首先需要探测可用的物理内存资源;了解到物理内存位于什么地方,有多大之后,就以固定页面大小来划分整个物理内存空间 ,并准备以此为最小内存分配单位来管理整个物理内存,管理在内核运行过程中每页内存,设定其可用状态(free的,used的,还是reserved的),这其实就对应了我们在课本上讲到的连续内存分配概念和原理的具体实现;接ucore kernel就要建立页表,启动分页机制,让CPU的MMU把预先建立好的页表中的页表项读入到TLB中,根据页表项描述的虚拟页(Page) 与物理页帧(Page Frame) 的对应关系完成CPU对内存的读、写和执行操作。这一部分其实就对应了我们在课本上讲到内存映射、页表、多级页表等概念和原理的具体实现。
在代码分析上,我们可以根据执行流程来直接看源代码,并可采用GDB源码调试的手段来动态地分析ucore的执行过程。内存管理相关的总体控制函数是pmm_init函数,它完成的主要工作包括:
1. 初始化物理内存页管理器框架pmm_manager;
2. 建立空闲 的page链表,这样就可以分配以页(4KB)为单位的空闲 内存了; 3. 检查物理内存页分配算法;
4. 为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表; 5. 建立一一映射关系的二级页表; 6. 使能分页机制;
7. 从新设置全局段描述符表; 8. 取消临时二级页表; 9. 检查页表建立是否正确;
10.通过自映射机制完成页表的打印输。
四、实验练习过程与详细分析
-
《系统软件开发实践》 实验报告 第20页
1、实现firstfit连续物理内存分配算法
在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。
物理内存页管理器顺着双向链表进行搜索空闲内存区域,直到找到一个足够大的空闲区域,这是一种速度很快的算法,因为它尽可能少地搜索链表。如果空闲区域的大小和申请分配的大小正好一样,则把这个空闲区域分配出去,成功返回;否则将该空闲区分为两部分,一部分区域与申请分配的大小相等,把它分配出去,剩下的一部分区域形成新的空闲区。其释放内存的设计思路很简单,只需把这块区域重新放回双向链表中即可。 (1)default_init_memmap()函数
这个函数是用来初始化空闲页链表的。主要有两个步骤:初始化每一个空闲页,计算空闲页的总数。
static void
default_init_memmap(struct Page *base, size_t n) { assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
//检查此页是否是保留页
assert(PageReserved(p));//设置标志位 p->flags = 0;
SetPageProperty(p);//将引用计数清零 p->property = 0; set_page_ref(p, 0);
//使用头插法将空闲页插入到链表(使用头插法是因为地址是从低地址向高地址增长)
list_add_before(&free_list, &(p->page_link)); }
nr_free += n;
//本行用于计算空闲页的总数 base->property = n; }
(2)default_alloc_pages ()函数
这个函数是用来分配空闲页链表的。大致可以分为七个步骤: 第一步:寻找足够大的空闲块
第二步:如果找到了,重新设置标志位,从空闲链表中删除此页 第三步:判断空闲块大小是否合适 第四步:如果不合适,分割页块 第五步:如果合适则不进行操作 第六步:计算剩余空闲页个数 第七步:返回分配的页块地址
static struct Page *
default_alloc_pages(size_t n) { assert(n > 0); if (n > nr_free) { return NULL; }
list_entry_t *le, *len;
-
《系统软件开发实践》 实验报告 第21页
le = &free_list;
//在空闲页链表中寻找合适大小的页块 while((le=list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);//找到合适页块 if(p->property >= n){ int i;
for(i=0;i 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; } //如果大小合适则不需要操作了 ClearPageProperty(p); SetPageReserved(p); nr_free -= n; return p; } } return NULL; } (3)default_free_pages()函数 这个函数的作用是释放已经使用完的页,把他们合并到freelist中。这个函数的基本完成思想和思路为: 1、首先在freelist中查找合适的位置以供插入; 2、然后改变被释放页的标志位,以及头部的计数器; 3、尝试在freelist中向高地址或低地址合并; 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;//查找插入位置 while((le=list_next(le)) != &free_list) { p = le2page(le, page_link); if(p>base){ break; } } //向插入位置插入多个页并设置标志位 for(p=base;p - 《系统软件开发实践》 实验报告 第22页 list_add_before(le, &(p->page_link)); } base->flags = 0; set_page_ref(base, 0); ClearPageProperty(base); SetPageProperty(base); base->property = n;//记录页块信息到头部 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(le!=&free_list && 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函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。尝试获取页表的地址,如果获取不到就新建一个页表: pde_t *pdep = &pgdir[PDX(la)]; //本行用于尝试获取页表 //用if语句判断是否获取页表成功 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); //设置权限 -
相关推荐: