练习0:填写已有实验
本实验依赖实验1/2。请把你做的实验1/2的代码填入本实验中代码中有“LAB1”,“LAB2”的注释相应部分。
练习1:给未被映射的地址映射上物理页
完成do_pgfault(mm/vmm.c)函数,给未被映射的地址映射上物理页。设置访问权限 的时候需要参考页面所在 VMA 的权限,同时需要注意映射物理页时需要操作内存控制 结构所指定的页表,而不是内核的页表。注意:在LAB2 EXERCISE 1处填写代码。执行make qemu后,如果通过check_pgfault函数的测试后,会有“check_pgfault() succeeded!”的输出,表示练习1基本正确。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中组成部分对ucore实现页替换算法的潜在用处。
如果ucore的缺页服务例程在执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
页面异常
当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内 存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生 页错误异常。产生页面异常的原因主要有:
- 目标页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
- 相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上)
- 访问权限不符合(此时页表项P标志=1,比如企图写只读页面)
当出现上面情况之一,那么就会产生页面page fault(#PF)
异常。产生异常的线性地址存储在 CR2中,并且将是page fault
的产生类型保存在 error code 中
关键数据结构
page_fault函数不知道哪些是“合法”的虚拟页,原因是ucore还缺少一定的数据结构来描述这种不在物理内存中的“合法”虚拟页。为此ucore通过建立mm_struct和vma_struct数据结构,描述了ucore模拟应用程序运行所需的合法内存空间。当访问内存产生page fault异常时,可获得访问的内存的方式(读或写)以及具体的虚拟内存地址,这样ucore就可以查询此地址,看是否属于vma_struct数据结构中描述的合法地址范围中,如果在,则可根据具体情况进行请求调页/页换入换出处理;如果不在,则报错。
虚拟地址空间和物理地址空间的示意图
vma_struct
vma_struct述应用程序对虚拟内存“需求”,以及针对vma_struct的函数操作。里把一个vma_struct结构的变量简称为vma变量。
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是描述应用程序对虚拟内存“需求”的变量。vm_start
和vm_end
描述的是一个合理的地址空间范围(即严格确保 vm_start < vm_end的关系),list_link
是一个双向链表,按照从小到大的顺序把一系列用vma_struct
表示的虚拟内存空间链接起来,并且还要求这些链起来的vma_struct
应该是不相交的,即vma之间的地址空间无交集。
vm_flags
表示了这个虚拟内存空间的属性,目前的属性包括
#define VM_READ 0x00000001 //只读
#define VM_WRITE 0x00000002 //可读写
#define VM_EXEC 0x00000004 //可执行
vm_mm
是一个指针,指向一个比vma_struct
更高的抽象层次的数据结构mm_struct
。
mm_struct
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; //指向用来链接记录页访问情况的链表头
};
mmap_list是双向链表头,链接了所有属于同一页目录表的虚拟内存空间,mmap_cache是指向当前正在使用的虚拟内存空间,由于操作系统执行的“局部性”原理,当前正在用到的虚拟内存空间在接下来的操作中可能还会用到,这时就不需要查链表,而是直接使用此指针就可找到下一次要用到的虚拟内存空间。pgdir 所指向的就是 mm_struct数据结构所维护的页表。通过访问pgdir可以查找某虚拟地址对应的页表项是否存在以及页表项的属性等。map_count记录mmap_list 里面链接的 vma_struct的个数。sm_priv指向用来链接记录页访问情况的链表头,这建立了mm_struct和后续要讲到的swap_manager之间的联系。
错码errorCode
产生页访问异常后,CPU把引起页访问异常的线性地址装到寄存器CR2中,并给出了出错码errorCode,说明了页访问异常的类型。ucore OS会把这个值保存在struct trapframe 中tf_err成员变量中。而中断服务例程会调用页访问异常处理函数do_pgfault进行具体处理。这里的页访问异常处理是实现按需分页、页换入换出机制的关键之处。
do_pgfault()函数
int
do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {
int ret = -E_INVAL;
struct vma_struct *vma = find_vma(mm, addr); //查询vma
pgfault_num++;
if (vma == NULL || vma->vm_start > addr) { //vma->vm_start<=addr end
cprintf("not valid addr %x, and can not find it in vma\n", addr);
goto failed;
}
switch (error_code & 3) { //错误处理
default:
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:
cprintf("do_pgfault failed: error code flag = read AND present\n");
goto failed;
case 0:
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;
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) { //权限不够,失败!
if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
cprintf("pgdir_alloc_page in do_pgfault failed\n");
goto failed;
}
}
else { //页表项非空,尝试换入页面
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); //建立虚拟地址和物理地址之间的对应关系
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;
}
ucore中do_pgfault函数是完成页访问异常处理的主要函数,它根据从CPU的控制寄存器CR2中获取的页访问异常的物理地址以及根据errorCode的错误类型来查找此地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系。所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB,然后调用iret中断,返回到产生页访问异常的指令处重新执行此指令。如果该虚地址不在某VMA范围内,则认为是一次非法访问。
do_pgfault的调用关系图
练习2:补充完成基于FIFO的页面替换算法
完成vmm.c中的do_pgfault函数,并且在实现FIFO算法的swap_fifo.c中完成map_swappable和swap_out_vistim函数。通过对swap的测试。注意:在LAB2 EXERCISE 2处填写代码。执行make qemu后,如果通过check_swap函数的测试后,会有“check_swap() succeeded!”的输出,表示练习2基本正确。请在实验报告中简要说明你的设计实现过程。
请在实验报告中回答如下问题:
如果要在ucore上实现”extended clock页替换算法”请给你的设计方案,现有的swap_manager框架是否足以支持在ucore
中实现此算法?如果是,请给你的设计方案。如果不是,请给出你的新的扩展和基此扩展的设计方案。并需要回答如下
问题
需要被换出的页的特征是什么?
在ucore中如何判断具有这样特征的页?
何时进行换入和换出操作?
页错误异常
页错误异常发生时,有可能是因为页面保存在swap区或者磁盘文件上造成的,所以我们需要通过页面分配解决这个问题。 页面替换主要分为两个方面,页面换出和页面换入。
- 页面换入主要在上述的
do_pgfault()
函数实现; - 页面换出主要在
swap_out_vistim()
函数实现。
页面换入部分
do_pgfault()函数中的部分代码
else { //页表项非空,尝试换入页面
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); //建立虚拟地址和物理地址之间的对应关系
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;
}
在换入时,需要先检查产生访问异常的地址是否属于某个vma表示的合法虚拟地址,并且保存在硬盘的swap文件中(对应的PTE的高24位不为0)。如果满足以上亮点,则执行swap_in() 函数换入页面。
页面换出部分
换出机制
换出页面的时机相对复杂一些,针对不同的策略有不同的时机。ucore目前大致有两种策略,即积极换出策略和消极换出策略。积极换出策略是指操作系统周期性地(或在系统不忙的时候)主动把某些认为“不常用”的页换出到硬盘上,从而确保系统中总有一定数量的空闲页存在,这样当需要空闲页时,基本上能够及时满足需求;消极换出策略是指,只是当试图得到空闲页时,发现当前没有空闲的物理页可供分配,这时才开始查找“不常用”页面,并把一个或多个这样的页换出到硬盘上。在实验三中的基本练习中,支持上述的第二种情况。对于第一种积极换出策略,即每隔1秒执行一次的实现积极的换出策略,可考虑在扩展练习中实现。对于第二种消极的换出策略,则是在ucore调用alloc_pages函数获取空闲页时,此函数如果发现无法从物理内存页分配器获得空闲页,就会进一步调用swap_out函数换出某页,实现一种消极的换出策略。
FIFO
替换算法会维护一个队列,队列按照页面调用的次序排列,越早被加载到内存的页面会越早被换出。
类框架swap_manager
为了实现各种页替换算法,我们设计了一个页替换算法的类框架swap_manager:
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函数指针也很重要,结合定时产生的中断,可以实现一种积极的换页策略。
_fifo_map_swappable函数
_fifo_map_swappable()函数的主要作用是将最近被用到的页面添加到算法所维护的次序队列。
static int
_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);
list_add(head, entry);
return 0;
}
_fifo_swap_out_victim()函数
_fifo_swap_out_victim()函数是用来查询哪个页面需要被换出,它的主要作用是用来查询哪个页面需要被换出。
static int
_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);
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;
}
换出则相对简单,当申请空闲页面时,alloc_pages()函数不能获得空闲页,则需要调用swap_out()函数换出不常用的页面。