本次实验是在实验二的基础上,借助于页表机制和实验一中涉及的中断异常处理机制,完成Page Fault异常处理和FIFO页替换算法的实现,结合磁盘提供的缓存空间,从而能够支持虚存管理,提供一个比实际物理内存空间“更大”的虚拟内存空间给系统使用。这个实验与实际操作系统中的实现比较起来要简单,不过需要了解实验一和实验二的具体实现。实际操作系统系统中的虚拟内存管理设计与实现是相当复杂的,涉及到与进程管理系统、文件系统等的交叉访问。
ucore在lab3中新增了vma_struct结构(kern/mm/vmm.h)来描述合法的连续虚拟内存空间块,一个进程合法的虚拟地址空间段将以vma集合的方式表示。
在ucore中,以vma_struct虚地址空间的大小顺序可以组成一个双向循环链表,与lab2类似,vma_struct反向包裹list_link链表节点属性,利用le2vma宏可以使用page_link节点找到所关联的vma_struct。
// the control struct for a set of vma using the same PDT
struct mm_struct {
// 连续虚拟内存块链表 (内部节点虚拟内存块的起始、截止地址必须全局有序,且不能出现重叠)
list_entry_t mmap_list; // linear list link which sorted by start addr of vma
// 当前访问的mmap_list链表中的vma块(由于局部性原理,之前访问过的vma有更大可能会在后续继续访问,该缓存可以减少从mmap_list中进行遍历查找的次数,提高效率)
struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
// 当前mm_struct关联的一级页表的指针
pde_t *pgdir; // the PDT of these vma
// 当前mm_struct->mmap_list中vma块的数量
int map_count; // the count of these vma
// 用于虚拟内存置换算法的属性,使用void*指针做到通用 (lab中默认的swap_fifo替换算法中,将其做为了一个先进先出链表队列)
void *sm_priv; // the private data for swap manager
};
ucore提供了mm_struct结构(kern/mm/vmm.h)作为一个总的内存管理器,统一的管理一个进程的虚拟内存以及物理内存。
其中,mm_struct的mmap_list用来存储上面提到的用于表示进程合法虚拟地址空间集合的vma双向循环链表。
本实验依赖实验1/2。请把你做的实验1/2的代码填入本实验中代码中有“LAB1”,“LAB2”的注释相应部分。
前面两个实验不考虑challenge则只修改了pmm.c、default_pmm.c、trap.c
三个文件,复制即可,不再赘述。
完成do_pgfault(mm/vmm.c)
函数,给未被映射的地址映射上物理页。设置访问权限 的时候需要参考页面所在 VMA 的权限,同时需要注意映射物理页时需要操作内存控制 结构所指定的页表,而不是内核的页表。注意:在LAB2 EXERCISE 1处填写代码。
问题分析
练习1要求完成do_pgfault函数,作用是给未被映射的虚拟地址映射上物理页;当启动分页机制后如果一条指令或数据的虚拟地址所对应的物理页不在内存中,或者访问权限不够,那么就会产生页错误异常,其具体原因有三点:
①目标页帧不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已 经撤销);
②相应的物理页帧不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),这在本次实验中会出现,我们将在下面介绍换页机制实现时进一步讲解如何处理;
③不满足访问权限(此时页表项P标志=1,但低权限的程序试图访问高权限的地址空间,或者有程序试图写只读页面).
产生页访问异常后,CPU把引起页访问异常的线性地址装到寄存器CR2中,并给出了出错码errorCode,说明了页访问异常的类型。ucore OS会把这个值保存在struct trapframe 中tf_err成员变量中。而中断服务例程会调用页访问异常处理函数do_pgfault进行具体处理。
//函数原型:
int do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr);
• mm_struct: 进程内存合法性记录表的头结点
• error_code: 错误码,说明了异常的类型
• addr: cr2 寄存器中记录的当前要访问的目标线性地址
解题思路
do_pgfault函数根据从CPU的控制寄存器CR2中获取的页访问异常的物理地址以及根据errorCode的错误类型来查找此地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果满足要求是一次合法访问,需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB;如果该虚地址不满足要求,则认为是一次非法访问,不予处理。
具体实现
①首先检查是否在合法范围内,不是则属于非法访问;
find_vma根据输入参数addr和mm变量,查找在mm变量中的mmap_list双向链表中某个vma包含此addr,即vma->vm_start<=addr end。
②检查页面异常发生时的错误码的最低两位,即存在位和读/写位,如果发现错误则打印相关提示信息并返回。导致错误的原因有:读没有读权限的内存、写没有写权限的内存、所读内容在内存中却读失败等。(原代码中已实现,即下图中的switch语句)错误码:
• 00
• fault 原因: 对一个 "不存在"的地址进行了 读 操作
• 01:
• fault 原因: 对一个"不存在"的地址进行了 写 操作
• 10:
• fault 原因: 对一个存在的地址进行了 读操作,但触发了页保护(权限) 错误–实打实的权限问题,这是不应该出现的
• 11
• fault 原因: 对一个存在的地址进行了 写操作,但触发了页保护(权限)错误
源代码中对四种状态与 vm 中的 flag 位进行了校验,排除了不应该存在的情况,这里省略不表,通过校验的条件如下:
IF (write an existed addr ) OR
(write an non_existed addr && addr is writable) OR
(read an non_existed addr && addr is readable)
THEN
continue process
③用虚拟地址addr索引页目录表和页表,得到对应的页表项。这时要分两种情况讨论。
如果页表项为0,说明系统尚未为虚拟地址addr分配物理页,因此首先需要申请分配一个物理页;然后设置页目录表和页表,以建立虚拟地址addr到物理页的映射;最后,设置该物理页为swappable,并且把它插入到可置换物理页链表的末尾。
测试
通过make qemu来测试do_pgfault函数;
check_pgfault()succeeded,测试成功;
问题简答
(1)请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中组成部分对ucore实现页替换算法的潜在用处。
页目录项(pgdir)作为一个双向链表存储了目前所有的页的物理地址和逻辑地址的对应,即在实内存中的所有页,替换算法中被换出的页从pgdir中选出;
页表(pte)则存储着替换算法中被换入的页的信息,替换后将被映射到一个物理页。
(2) 如果ucore的缺页服务例程在执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
首先CPU在当前内核栈保存当前被打断的程序现场:
即依次压入当前被打断程序使用的EFLAGS,CS,EIP,errorCode;由于页访问异常的中断号是0xE,CPU把异常中断号0xE对应的中断服务例程的地址(vectors.S中的标号vector14处)加载到CS和EIP寄存器中,开始执行中断服务例程。
这时ucore开始处理异常中断,首先需要保存硬件没有保存的寄存器。在vectors.S中的标号vector14处先把中断号压入内核栈,然后再在trapentry.S中的标号__alltraps处把DS、ES和其他通用寄存器都压栈。自此,被打断的程序执行现场(context)被保存在内核栈中。接下来,在trap.c的trap函数开始了中断服务例程的处理流程。
完成vmm.c中的do_pgfault函数,并且在实现FIFO算法的swap_fifo.c中完成map_swappable和swap_out_vistim函数。通过对swap的测试。注意:在LAB2 EXERCISE 2处填写代码。
问题分析
根据练习1,当页错误异常发生时,有可能是因为页面保存在swap区或者磁盘上,练习2需要利用页面替换算法解决这个问题。页面替换分为页面换入(do_pgfault()函数实现)和页面换出(swap_fifo.c()中实现),当申请空闲页面时,alloc_pages()函数不能获得空闲页,需要调用swap_out()函数换出不常用的页面;
解题思路
FIFO替换算法会维护一个队列,队列按照页面调用的次序排列,越早被加载到内存的页面会越早被换出。
为支持换入换出,在lab 2的基础上主要修改了两个地方:一是当虚拟页被换出到磁盘时,用对应页表项的高24位记录磁盘地址;二是在Page结构体中增加了pra_page_link和pra_vaddr两个字段,前者用于将可换出的物理页保存在一个链表中,后者用于记录当前物理页对应的虚拟页地址(由于可以换入换出,同一个物理页在不同时刻可能被映射到不同的虚拟页,因此有必要增加一个字段记录当前映射到的虚拟页地址)。
map_swappable函数根据FIFO置换算法,将一个物理页添加到可换出物理页链表的末尾,同时更新物理页对应的虚拟页地址。
swap_out_victim函数根据FIFO置换算法,选择可换出物理页链表的首元素,作为将被换出的物理页。
具体实现
(1) do_pgdefault()函数补充
如果页表项不为0,而又出现缺页异常,说明系统已建立虚拟地址addr到物理页的映射,但对应物理页已经被换出到磁盘中。这时同样需要申请分配一个物理页,把换出到磁盘中的那个页面的内容写到该物理页中;接下来和上面的步骤类似,同样需要建立虚拟地址addr到物理页的映射,同样需要把该物理页插入到可置换页链表的末尾。
问题简述
如果要在ucore上实现"extended clock页替换算法"请给你的设计方案,现有的swap_manager框架是否足以支持在ucore中实现此算法?如果是,请给你的设计方案。如果不是,请给出你的新的扩展和基此扩展的设计方案。
目前的swao_manager框架足以支持在ucore中实现extended clock算法。
在mmu.h中有如下定义:
dirty cit对应的位是PTE_D,引用位标志为PTE_A,所以用(*ptep&PTE_A)可以表明该页是否被方问过,在这一步的基础上(*ptep&PTE_D)可以判断当前位是否为脏位;
设计方案:
这两位标志位就存在四种可能的组合情况:(0,0)表示最近未被引用也未被修改,首先选择此页淘汰;(0,1)最近未被使用,但被修改,其次选择;(1,0)最近使用而未修改,再次选择;(1,1)最近使用且修改,最后选择。
当操作系统需要淘汰页时,对当前指针指向的页所对应的页表项进行查询,第一次遍历如果访问位为“0”且脏位为“0”,则淘汰该页;
第二次遍历如果引用位为1,则置0,继续访问下一个页,如果访问位为0,则是(0,1)的情况换出;
第三次遍历找(0,0),找到换出,访问位在第二次遍历时全置0,所以这次遍历相当于找第三种情况(1,0);
三圈之后还是没有找到换出的情况说明所有页都是脏页,那么随便选择第一个就可以换出了。
问题简述:
• ①需要被换出的页的特征是什么?
被淘汰的页是在主存驻留期间其页面内容未被修改过的,如果不满足则可以根据条件放宽;
• ②在ucore中如何判断具有这样特征的页?
最多遍历三次链表即可,第一次遍历可以确定有没有(0,0)这样的页,第二次遍历也已确定(0,1)这样的情况(第二次遍历时将访问位1置0),第三次遍历说明初始时访问位为0的情况不存在且已经把原来访问位为1的置0了,那么此时如果有(0,0)则说明是原来的(1,0),直接换出,如果第三次遍历依然没有换出,则说明全是最后一种情况(1,1),那么换出链表首的页即可;
• ③何时进行换入和换出操作?
当需要调用的页不在页表时需要换入,而页表已满的情况下需要换出。
问题分析
算法根据页面近期是否被访问与修改来决定换出的页,大致思路如下:
设计思路
与上述问题的设计方案相同;
这两位标志位就存在四种可能的组合情况:(0,0)表示最近未被引用也未被修改,首先选择此页淘汰;(0,1)最近未被使用,但被修改,其次选择;(1,0)最近使用而未修改,再次选择;(1,1)最近使用且修改,最后选择。
当操作系统需要淘汰页时,对当前指针指向的页所对应的页表项进行查询,第一次遍历如果访问位为“0”且脏位为“0”,则淘汰该页;
第二次遍历如果引用位为1,则置0,继续访问下一个页,如果访问位为0,则是(0,1)的情况换出;
第三次遍历找(0,0),找到换出,访问位在第二次遍历时全置0,所以这次遍历相当于找第三种情况(1,0);
三圈之后还是没有找到换出的情况说明所有页都是脏页,那么随便选择第一个就可以换出了。
具体实现:
使用循环与计数的方式进行设计方案的实现:
(1) 修改默认替换算法:
(2) _extended_clock_swap_victim实现:
用i表示第几次循环;总共最多需要遍历3次即可。
当操作系统需要淘汰页时,对当前指针指向的页所对应的页表项进行查询,第一次遍历如果访问位为“0”且脏位为“0”,则淘汰该页;
第二次遍历如果引用位为1,则置0,继续访问下一个页,如果访问位为0,则是(0,1)的情况换出;
第三次遍历找(0,0),找到换出,访问位在第二次遍历时全置0,所以这次遍历相当于找第三种情况(1,0);
三圈之后还是没有找到换出的情况说明所有页都是脏页,那么随便选择第一个就可以换出了。
本次实验主要完成了ucore内核对虚拟内存的管理工作。ucore通过lab2、lab3两个连续的实验,完成了对计算机内存的抽象与管理,为后续的用户级的多进程/线程的实现打下了基础。lab3对于已经理解了lab1、lab2中各种硬件交互以及C中晦涩巧妙的宏实现的人来说难度并不算大,整体的学习曲线变得平缓了。
由于前几个实验都是与操作系统内核紧密相关的,还没有涉及到与用户程序的交互,显得有些单调、枯燥,就连所实现的虚拟内存管理的相关功能都是通过一段精心设计的模拟内存访问过程的代码来校验其正确性的。但很快ucore就会在后续的实验中引入多进程/线程、用户态进程以及进程/线程同步等更加贴近平常应用开发时接触到的系统功能,对ucore的学习也会变得更加有趣。
而这一实验的challenge部分比起前面两次都要简单一些,只要能够掌握clock算法以及PTE_A、PTE_D位和它的换出策略就可以顺利完成代码实现,收获很大。
【challenge中的代码】
static int
_extended_clock_swap_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);
list_entry_t *le =head->next;//指向head的下一个节点
assert(head!=le);
struct Page *p = NULL;
pte_t *ptep = NULL;
int i = 0;
while(i<3){
i++;
le = head->prev;//指向head的上一个节点
while(le!=head){//回到头结点则是遍历了一圈
p = le2page(le, pra_page_link);
//获取页表项进而通过页表项中的标志位来换页
ptep =get_pte(mm->pgdir,p->pra_vaddr,0);
if(i == 1 && !(*ptep&PTE_A)&& !(*ptep&PTE_D)){
list_del(le);
assert(p!=NULL);
*ptr_page = p;
return 0;
}
else if(i == 2 && !(*ptep&PTE_A)){
list_del(le);
assert(p!=NULL);
*ptr_page = p;
return 0;
}
else if( i == 2 && (*ptep&PTE_A)){
*ptep &= ~PTE_A;
}
else if(i == 3 && !(*ptep&PTE_A)&& !(*ptep&PTE_D)){
list_del(le);
assert(p!=NULL);
*ptr_page = p;
return 0;
}
le = le ->prev;
}
}
//从大循环出来则初始时全是(1,1),换第一个就行
le = head ->prev;
assert(head!=le);
p = le2page(le, pra_page_link);//le2page宏可以根据链表元素获得对应的Page指针p
list_del(le); //将进来最早的页面从队列中删除
assert(p !=NULL);
*ptr_page = p; //将这一页的地址存储在ptr_page中
return 0;
}