ucore lab3(虚拟内存管理)
一、实验目的
1.1了解虚拟内存的Page Fault异常处理实现
1.2了解页替算法在操作系统中的实现
二、实验内容
本次实验是在实验二的基础上,借助于页表机制和实验一中涉及的中断异常处理机制,完成Page Fault异常处理和FIFO页替换算法的实现,结合磁盘提供的缓存空间,从而能够支持虚存管理,提供一个比实际物理内存空间“更大”的虚拟内存空间给系统使用。这个实验与实际操作系统中的实现比较起来要简单,不过需要了解实验一和实验二的具体实现。实际操作系统系统中的虚拟内存管理设计与实现是相当复杂的,涉及到与进程管理系统、文件系统等的交叉访问。如果大家有余力,可以尝试完成扩展练习,实现extended clock页替换算法。
三、实验步骤及流程
3.0 练习0:填写已有实验
本实验依赖实验1/2。请把你做的实验1/2的代码填入本实验中代码中有“LAB1”,“LAB2”的注释相应部分。
3.1 练习1:给未被映射的地址映射上物理页(需要编程)
3.1.1实验要求
完成do_pgfault(mm/vmm.c)函数,给未被映射的地址映射上物理页。设置访问权限 的时候需要参考页面所在 VMA 的权限,同时需要注意映射物理页时需要操作内存控制 结构所指定的页表,而不是内核的页表。注意:在LAB3 EXERCISE 1处填写代码。执行“make qemu”后,如果通过check_pgfault函数的测试后,会“check_pgfault() succeeded!”的输出,表示练习1基本正确。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
·请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中组成部分对ucore实现页替换算法的潜在用处。
·如果ucore的缺页服务例程在执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
3.1.2关键数据结构及知识点
(1)有关虚拟内存需要注意:
①虚拟内存单元不一定有实际的物理内存单元对应,即实际的物理内存单元可能不存在;
②如果虚拟内存单元对应有实际的物理内存单元,那二者的地址一般是不相等的;
③通过操作系统实现的某种内存映射可建立虚拟内存与物理内存的对应关系,使得程序员或CPU访问的虚拟内存地址会自动转换为一个物理内存地址。
(2)当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生页错误异常。产生页面异常的原因有:
①目标(虚拟)页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销),此时应该直接报错;
②相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),此时应该建立映射虚拟页和物理页的映射关系;
③目标访问页的权限不符合(此时页表项P标志=1,比如企图写只读页面),此时应该直接报错。
当出现上面情况之一,那么就会产生页面page fault(#PF)异常。产生异常的线性地址(虚拟地址)存储在 CR2中,并且将是page fault的产生类型保存在error code中。
(3)虚拟地址空间和物理地址空间
一块虚拟地址(vma)可能映射到一个或多个多个物理页。由于一开始,分配虚拟空间超过了物理空间的大小,比如在上图中只有5个物理页帧,但其实我们给它分了7个虚拟页,那很明显一定会有两个虚拟页没有对应的物理页帧。那么如果访问到这两个虚拟页,在二级页表里面它会没有对应映射关系,一旦没有对应映射关系,那就会产生缺页异常,因此就是do_pgfault函数的实现功能,它要建立这个映射关系。
(4)do_pgfault()的调用关系
(5)虚拟内存块空间(vma,kern/mm/vmm.h)
struct vma_struct {
struct mm_struct *vm_mm; // 指向一个比vma_struct更高的抽象层次的数据结构mm_struct
uintptr_t vm_start; // vma的开始地址
uintptr_t vm_end; // vma的结束地址
uint32_t vm_flags; // 虚拟内存空间的属性
list_entry_t list_link; // 双向链表,按照从小到大的顺序把虚拟内存空间链接起来
};
VMA数据结构,用于管理应用程序的虚拟内存,更准确来说,VMA是描述应用程序对虚拟内存“需求”的结构,它包含五个成员:
①mm:是一个更高级更抽象的数据结构,代表整个应用程序所用到的所有VMA(该VMA的占有程序),接下来马上讨论。
②vm_start、vm_end:描述一个合理的地址空间范围(确保 vm_start < vm_end的关系);
③vm_flags:该虚拟内存空间的属性,目前的属性包括可读、可写、可执行;
④list_link:一个双向链表,按照从小到大的顺序把一系列用vma_struct表示的虚拟内存空间链接起来,并且还要求这些链起来的vma_struct应该是不相交的,即vma之间的地址空间无交集。
(6)应用程序映射到的虚拟空间块综述结构(mm,kern/mm/vmm.h)
struct mm_struct {
list_entry_t mmap_list; // 双向链表头,链接了所有属于同一页目录表的虚拟内存空间
struct vma_struct *mmap_cache; // 指向当前正在使用的虚拟内存空间
pde_t *pgdir; //指向的就是 mm_struct数据结构所维护的页表
int map_count; //记录mmap_list里面链接的vma_struct的个数
void *sm_priv; //指向用来链接记录页访问情况的链表头
如果说vma描述和管理的是一系列虚拟内存块结构,那么mm则用来描述,究竟是哪个应用程序使用了这些vma,它们是通过vma中的成员mm联系在一起的。mm也有五个成员,分别解释如下:
①list_entry_t mmap_list:双向链表,链接了所有属于同一页目录表的虚拟内存空间;
②struct vma_struct *mmap_cache:指向当前正在使用的虚拟内存空间;
③pde_t *pgdir:指向的mm_struct数据结构所维护的一级页表,每个应用程序都有页表,那么这里指向应用程序正在操作的那个;
④int map_count:记录mmap_list里链接的vma_struct的个数(该程序用了多少个虚拟页);
⑤void *sm_priv:指向用来链接记录页访问情况的链表头。用于FIFO替换策略的访问。
3.1.3代码实现(do_pgfault函数的实现(kern/vmm.c))
do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {
int ret = -E_INVAL;
//try to find a vma which include addr尝试寻找包括addr的vma
struct vma_struct *vma = find_vma(mm, addr);
pgfault_num++;
//If the addr is in the range of a mm's vma?
if (vma == NULL || vma->vm_start > addr) {
cprintf("not valid addr %x, and can not find it in vma\n", addr);
goto failed;
}
//check the error_code
switch (error_code & 3) {//错误处理
default:
/* error code flag : default is 3 ( W/R=1, P=1): write, present */
case 2: /* error code flag : (W/R=1, P=0): write, not present */
if (!(vma->vm_flags & VM_WRITE)) {
cprintf("do_pgfault failed: error code flag = write AND not present, but the addr's vma cannot write\n");
goto failed;
}
break;
case 1: /* error code flag : (W/R=0, P=1): read, present */
cprintf("do_pgfault failed: error code flag = read AND present\n");
goto failed;
//如果无法读取直接报错
case 0: /* error code flag : (W/R=0,read, not present */
if (!(vma->vm_flags & (VM_READ | VM_EXEC))) {
cprintf("do_pgfault failed: error code flag = read AND not present, but the addr's vma cannot read or exec\n");
goto failed;
}
}
uint32_t perm = PTE_U;//prem:给物理页赋予权限的中间变量
if (vma->vm_flags & VM_WRITE) {
perm |= PTE_W;
}
addr = ROUNDDOWN(addr, PGSIZE);
ret = -E_NO_MEM;
pte_t *ptep=NULL;
if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {
cprintf("get_pte in do_pgfault failed\n");
goto failed;
}
//权限不够,失败
if (*ptep == 0) { // 二级页表入口,尝试找到 pte,如果 pte 的 pt (页表)不存在,然后创建 pt。
if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
cprintf("pgdir_alloc_page in do_pgfault failed\n");
goto failed;
}
}
//页表项非空,尝试换入页面
else { // 如果这个 pte 是一个交换条目,那么用 phy addr 将数据从磁盘加载到一个页面,并调用 page _ insert 将 phy addr 映射到逻辑 addr
if(swap_init_ok) {
struct Page *page=NULL;
//根据mm结构和addr地址,尝试将硬盘中的内容换入至page中
if ((ret = swap_in(mm, addr, &page)) != 0) {
cprintf("swap_in in do_pgfault failed\n");
goto failed;
}
page_insert(mm->pgdir, page, addr, perm);
//建立虚拟地址和物理地址之间的对应关系,perm设置物理页权限,为了保证和它对应的虚拟页权限一致
swap_map_swappable(mm, addr, page, 1);//将此页面设置为可交换的 ,也添加到算法所维护的次序队列
page->pra_vaddr = addr; //设置页对应的虚拟地址
}
else {
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
ret = 0;
failed:
return ret;
}
该函数主要完成的是具体的页访问异常处理工作:
(1)首先是三个传入参数,它们分别是:应用程序虚拟存储总管mm、错误码error_code和具体出错的虚拟地址addr。接下来,我们讨论对于具体可能出现页访问错误的情况进行一一处理和报错。
(3)如果是虚拟地址的问题,如虚拟地址的范围超过了限制,或者是虚拟地址无法被查找到,即可以说该地址是不合法的,进行了一次非法访问,那么可以直接报错。
(3)如果是目标访问页的权限不符合,比如对一个只读页进行写操作,或者读了一个不可读的页,那么此时可以直接报错。这里涉及到了一个对于传入的错误码error_code的理解,在该函数的注释中,我们了解到,错误码的低2位分别是:
P标志(位0)最低位:表示当前的错误是由于不存在页面(0)引起,还是由于违反访问权限(1)引起。
W / R标志(位1):表示当前错误是由于读操作(0)引起还是还是写操作(1)引起。
因此,和权限相关的判断,只需要对P和W/R,即error_code的最低两位判断即可。
(3)如果能够顺利通过上述的合法性判断,那么此次虚拟内存访问就被认为是合法的,此时,页访问异常的原因,是由于该合法虚拟页,没有对应物理页的映射导致,因此下一步要建立起这个映射。建立起这个映射首先要改变一下待换入页面的权限值:首先,无论如何这个页面需要能够被用户访问,其次,如果对应映射的vma有写的权限,该物理页也需要可写。
(4)然后,我们通过当前应用程序mm所指向的一级页表,以及虚拟地址,去查询有没有对应的二级页表,如果查询结果为NULL,那么报错,因为没有对应的二级页表项,它根本不存在,也不知道用什么物理页去映射(当然,这里不可能不存在,如果查找到不存在的情况,由于get_pte的create标记位为1,那么会创建一个新的二级页表);之后,如果是上述新创建的二级页表,那么*ptep就会是0,代表页表为空,此时调用pgdir_alloc_page,对它进行初始化;接下来的else语句就是对它进行一个映射的替换:
swap_init_ok是一个标记位,代表交换初始化成功,可以开始替换的过程了。首先声明了一个页,之后将结构mm、虚拟地址和这个空页,调用了swap_in函数。该函数首先为传入的空页page分配初始化,之后获取了mm一级页表对应的二级页表,通过swapfs_read尝试将硬盘中的内容换入到新的page中,即可返回,最后的return 0是一个正常返回值。
(5)执行完上述函数,最后,建立起该页的虚拟地址和物理地址之间的对应关系,然后设置为可交换,该页的虚拟地址设置为传入的地址。至此,do_pgfault结束,建立起了新的映射关系,下次访问不会有异常。
3.2 练习2:补充完成基于FIFO的页面替换算法(需要编程)
3.2.1实验要求
完成vmm.c中的do_pgfault函数,并且在实现FIFO算法的swap_fifo.c中完成map_swappable和swap_out_victim函数。通过对swap的测试。注意:在LAB3 EXERCISE 2处填写代码。执行“make qemu”后,如果通过check_swap函数的测试后,会有“check_swap() succeeded!”的输出,表示练习2基本正确。
请在实验报告中简要说明你的设计实现过程并回答如下问题:如果要在ucore上实现"extended clock页替换算法"请给你的设计方案,现有的swap_manager框架是否足以支持在ucore中实现此算法?如果是,请给你的设计方案。如果不是,请给出你的新的扩展和基此扩展的设计方案。并需要回答如下问题:
·需要被换出的页的特征是什么?
·在ucore中如何判断具有这样特征的页?
·何时进行换入和换出操作?
3.2.2关键数据结构及知识点
(1)练习2主要实现功能
在练习1中,当页错误异常发生时,有可能是因为页面保存在swap区或者磁盘文件上造成的,所以我们需要利用页面替换算法解决这个问题。
而练习1和这里的关联就在于:页面替换主要分为两个方面,页面换出和页面换入。练习1实现的是页面换入,主要在上述的do_pgfault()函数实现;而练习2这里主要实现,页面换出,主要是在swap_out_vistim()函数。另外,在练习1还有一个函数叫做swappable,代表将该页面设置为可交换的。于是,练习2主要是对于这两个函数的实现。
(2)实现页面的替换需要换入和换出,这里使用的是FIFO策略:
先进先出(First In First Out, FIFO)页替换算法:该算法总是淘汰最先进入内存的页,即选择在内存中驻留时间最久的页予以淘汰。只需把一个应用程序在执行过程中已调入内存的页按先后次序链接成一个队列,队列头指向内存中驻留时间最久的页,队列尾指向最近被调入内存的页。这样需要淘汰页时,从队列头很容易查找到需要淘汰的页。
(3)为实现各种页替换算法设计了一个页替换算法的类框架swap_manager:(kern/mm/swap.h)
struct swap_manager
{
const char *name;
/* Global initialization for the swap manager */
int (*init) (void);
/* Initialize the priv data inside mm_struct */
int (*init_mm) (struct mm_struct *mm);
/* Called when tick interrupt occured */
int (*tick_event) (struct mm_struct *mm);
/* Called when map a swappable page into the mm_struct */
int (*map_swappable) (struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in);
/* When a page is marked as shared, this routine is called to
* delete the addr entry from the swap manager */
int (*set_unswappable) (struct mm_struct *mm, uintptr_t addr);
/* Try to swap out a page, return then victim */
int (*swap_out_victim) (struct mm_struct *mm, struct Page **ptr_page, int in_tick);
/* check the page relpacement algorithm */
int (*check_swap)(void);
};
这里关键的两个函数指针是map_swappable和swap_out_vistim,前一个函数用于记录页访问情况相关属性,后一个函数用于挑选需要换出的页。显然第二个函数依赖于第一个函数记录的页访问情况。tick_event函数指针也很重要,结合定时产生的中断,可以实现一种积极的换页策略。
3.2.3代码实现
(1)map_swappable函数(kern/swap_fifo.c)
将最近被用到的页面添加到算法所维护的次序队列,执行的是一个页加入队列的操作,由page fault触发,每次都在表头加入新的page。
_fifo_map_swappable(struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in)
{
list_entry_t *head=(list_entry_t*) mm->sm_priv;
list_entry_t *entry=&(page->pra_page_link);
assert(entry != NULL && head != NULL);
//record the page access situlation
//link the most recent arrival page at the back of the pra_list_head qeueue.
list_add(head, entry);
return 0;
}
(2)_fifo_swap_out_victim()函数(kern/swap_fifo.c)
换出的策略,FIFO换出最早进入内存的页面,也就是在队尾在替换出页面的时候,使用链表操作,删除掉最早进入的那个页,并按照注释将这个页面给到传入参数ptr_page。
_fifo_swap_out_victim(struct mm_struct *mm, struct Page ** ptr_page, int in_tick)
{
list_entry_t *head=(list_entry_t*) mm->sm_priv;
assert(head != NULL);
assert(in_tick==0);
/* Select the tail */
list_entry_t *le = head->prev;//用le指示需要被换出的页
assert(head!=le);
struct Page *p = le2page(le, pra_page_link);//le2page宏可以根据链表元素获得对应的Page指针p
list_del(le);//将进来最早的页面从队列中删除
assert(p !=NULL);
*ptr_page = p;
return 0;
}
四、思考题
4.1 练习1
Q1:请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中组成部分对ucore实现页替换算法的潜在用处。
页替换涉及到换入换出,换入时需要将某个虚拟地址对应于磁盘的一页内容读入到内存中,换出时需要将某个虚拟页的内容写到磁盘中的某个位置。而页表项可以记录该虚拟页在磁盘中的位置,为换入换出提供磁盘位置信息。页目录项则是用来索引对应的页表。
分页机制的实现,确保了虚拟地址和物理地址之间的对应关系,一方面,通过查找虚拟地址是否存在于一二级页表中,可以容易发现该地址是否是合法的,另一方面,通过修改映射关系即可实现页替换操作。另外,基于页表实现了地址的分段操作,在这里,一个物理地址不同的位数上,会存储一系列不同的信息,例如物理页是否存在,是否可读,对应的物理页用户态是否可以访问。
Q2:如果ucore的缺页服务例程在执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
CPU会把产生异常的线性地址存储在CR2寄存器中,并且把表示页访问异常类型的值(简称页访问异常错误码,errorCode)保存在中断栈中。之后通过上述分析的trap–> trap_dispatch–>pgfault_handler–>do_pgfault调用关系,一步步做出处理。
4.2 练习2
如果要在ucore上实现"extended clock页替换算法"请给你的设计方案,现有的swap_manager框架是否足以支持在ucore中实现此算法?如果是,请给你的设计方案。如果不是,请给出你的新的扩展和基此扩展的设计方案。并需要回答如下问题:
·需要被换出的页的特征是什么?
·在ucore中如何判断具有这样特征的页?
·何时进行换入和换出操作?
时钟(Clock)页替换算法:是LRU算法的一种近似实现。时钟页替换算法把各个页面组织成环形链表的形式,类似于一个钟的表面。然后把一个指针(简称当前指针)指向最老的那个页面,即最先进来的那个页面。另外,时钟算法需要在页表项(PTE)中设置了一位访问位来表示此页表项对应的页当前是否被访问过。当该页被访问时,CPU中的MMU硬件将把访问位置“1”。当操作系统需要淘汰页时,对当前指针指向的页所对应的页表项进行查询,如果访问位为“0”,则淘汰该页,如果该页被写过,则还要把它换出到硬盘上;如果访问位为“1”,则将该页表项的此位置“0”,继续访问下一个页。该算法近似地体现了LRU的思想,且易于实现,开销少,需要硬件支持来设置访问位。时钟页替换算法在本质上与FIFO算法是类似的,不同之处是在时钟页替换算法中跳过了访问位为1的页。
目前的swap_manager框架足以支持在ucore中实现extended clock算法,之所以能够支持该算法,是因为在kern/mm/mmu.h文件中定义PTE_A中的内容的即标志着该页是否被访问过,由此我们可以对kern/mm/swap_fifo.c做相应的修改,判断是否被访问过即可。
**·需要被换出的页的特征是:**最早被换入,且最近没有被访问过的页。
**·在ucore中如何判断具有这样特征的页:**首先判断其最近有没有被访问过(利用位运算,让条件*ptep & PTE_A是否为1),若无,则按照FIFO原则进行置换。
**·何时进行换入和换出操作:**类似于FIFO算法,当需要调用的页不在页表中时,并且在页表已满的情况下,需要进行换入和换出操作。
五、运行结果
运行结果如下图,通过和参考运行结果进行对比,二者一致,“check_pgfault() succeeded!”的输出说明练习1正确,“check_swap() succeeded!”的输出说明练习2正确。
六、实验心得
通过本次实验对中断异常处理机制,Page Fault异常处理以及FIFO页替换算法有了更深入的学习与理解,在完成实验过程中也收获了很多细节方面的知识点。在验收过程中通过助教老师的提问也发现了自己一些不足,对自己在实验过程中遗漏的部分知识点也进行了查缺补漏。相信本次实验的内容与收获会对今后的学习起到很好的帮助。