此实验借助页表机制和中断异常处理机制,完成page fault异常处理和fifo页替换算法的实现。ucore建立了mm_struct和vma_struct数据结构,描述了ucore模拟应用程序运行所需的合法内存空间。vma_struct描述的地址范围就是程序的合法虚拟地址范围。
struct vma_struct {
struct mm_struct *vm_mm;//表示该vma隶属于的mm,每个mm中的所有vma均属于同一页目录表的虚拟内存空间
uintptr_t vm_start; // 虚拟内存空间的起始位置
uintptr_t vm_end; // 虚拟内存空间的结束位置
uint32_t vm_flags; // 这块连续的虚拟内存空间的属性(只读、可读写、可执行)
list_entry_t list_link;//该结构用于链表建立
};
每个进程都会有一个mm_struct用来管理虚拟内存和物理内存
struct mm_struct {
list_entry_t mmap_list;// 双向链表头,链接了所有属于该mm的vma
struct vma_struct *mmap_cache;// 指向当前正在使用的虚拟内存空间。由于“局部性“原理,程序会经常使用正在用的虚拟内存空间,这属性将使得无需查询链表,查询将加速30%以上。
pde_t *pgdir; // 指向vma所属于的页目录表
int map_count; // 包含了vma的数量
void *sm_priv; // 用于虚拟内存置换算法的属性,使用void*指针做到通用
};
涉及vma_struct的操作函数包括三个vma_create、insert_vma_struct、find_vma,分别是创建、插入链表、查找。
涉及mm_struct的操作函数仅有两个mm_create和mm_destory,功能同字面。
Page Fault异常处理
在程序的执行过程中,CPU无法访问到相应的物理内存,CPU将会产生一次该异常。
产生异常的原因主要有以下几点:
1、目标页帧不存在
2、对应的物理页帧并不在内存中
3、不满足访问权限
出现上述情况之一,产生page fault,CPU会把产生异常的线性地址存储在CR2中,并把表示页访问异常类型的值errorCode保存在中断栈中,具体流程可见lab1笔记。
ucore中的do_pgfault函数是完成页访问异常的主要函数,它根据CPU的CR2种获得页访问异常的地址以及根据errorCode的错误类型来查找此地址会否在某个VMA的地址范围以内以及是否满足正确的读写权限。
如果范围合法并且权限也正确,那么就分配一个空闲的内存页进行映射或者是将交换出去的内存页交换回来,刷新TLB,并调用iret中断,返回。
如果地址不在VMA的范围以内,或者是权限不足,那么就认定是一次非法访问。
页面置换机制
在操作系统的设计中,一个基本的原则是:并非所有的物理页可以交换出去,只有映射到用户空间并且呗用户程序直接访问的页面才能被交换,而被内核直接使用的内核空间的页面不能被换出。操作系统是执行的关键代码,需要保证运行的高效性和实时性,并且如果处理缺页过程所用到的内核代码或者数据被换出,那么整个内核都会面临崩溃的危险。
ucore在设计上,充分利用了PTE,正常情况下PTE由页基址、保留的可供软件使用的位Avail、全局标志位G、页属性表索引标志PAT、页尺寸标志、脏位、访问位、页级高速缓存禁用标志、页级直写标记、用户/管理标志、读/写标志、存在标志组成
我们需要注意到一点就是当一个页表项或者页目录表项的“存在”标志被清零时,操作系统可以使用该表项的其余位来存储一些其他信息,比如该页在磁盘存储系统上的位置。
而ucore中,一个页被置换到硬盘的某8个扇区(0.5KB/扇区),该PTE的最低位--present 位应该为0 (即 PTE_P 标记为空,表示虚实地址映射关系不存在),接下来的7位暂时保 留,可以用作各种扩展;而原来用来表示页帧号的高24位地址,恰好可以用来表示此页在硬 盘上的起始扇区的位置(其从第几个扇区开始)。
执行换入的时机,check_mm_struct变量这个数据结构表示了目前所有合法的虚拟内存空间集合,do_pgfault函数被调用时,会判断产生异常的地址是否属于check_mm_struct某个vma表示的合法虚拟地址空间,且保存在硬盘的swap文件中,若是则将调用swap_in函数完成页面的换入。
执行换出的时机在ucore中有两种。第一种是积极的策略,系统周期性地主动把某些不常用的页swap_out到硬盘上,即使内存并没有满。第二种是消极的策略,只有在没有空闲的物理页分配时,这时才开始查找不常用的页面swap_out。
在lab3中,换出的策略采取上述第二种。
页面替换算法的数据结构
为了表示物理页可被换出的情况,对Page数据结构进行了扩展:
struct Page {
......
list_entry_t pra_page_link;
uintptr_t pra_vaddr;
};
pra_page_link用来构造按页的第一次访问时间进行排序的一个链表,表头表示第一次访问时间最近的页,表尾表示第一次访问时间最远的页。
pra_vaddr可以用来记录此物理页对应的虚拟页起始地址。
由于ucore在lab3中仅有一个内核页表,在页被交换到磁盘上时,对应的PTE的高24位偏移*8=对应的磁盘交换的起始扇区号。
同lab2构建一个管理虚拟内存页面置换的框架swap_manager,提供的默认实现的fifo_swap_manager。
代码部分
alloc_pages函数,在lab3中进行了改动
//alloc_pages - call pmm->alloc_pages to allocate a continuous n*PAGESIZE memory
struct Page *
alloc_pages(size_t n) {
struct Page *page=NULL;
bool intr_flag;
while (1)
{
// 关闭中断,避免分配内存时,物理内存管理器内部的数据结构变动时被中断打断,导致数据错误
local_intr_save(intr_flag);
{
// 分配n个物理页
page = pmm_manager->alloc_pages(n);
}
// 恢复中断控制位
local_intr_restore(intr_flag);
// 满足下面之中的一个条件,就跳出while循环
// page != null 表示分配成功
// 如果n > 1 说明不是发生缺页异常来申请的(否则n=1)
// 如果swap_init_ok == 0 说明没有开启分页模式
if (page != NULL || n > 1 || swap_init_ok == 0) break;
extern struct mm_struct *check_mm_struct;
//cprintf("page %x, call swap_out in alloc_pages %d\n",page, n);
// 尝试着将某一物理页置换到swap磁盘交换扇区中,以腾出一个新的物理页来
// 如果交换成功,则理论上下一次循环,pmm_manager->alloc_pages(1)将有机会分配空闲物理页成功
swap_out(check_mm_struct, n, 0);
}
//cprintf("n %d,get page %x, No %d in alloc_pages\n",n,page,(page-pages));
return page;
}
do_pgfault函数
int 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
// 试图从mm关联的vma链表块中查询,是否存在当前addr线性地址匹配的vma块
struct vma_struct *vma = find_vma(mm, addr);
// 全局页异常处理数自增1
pgfault_num++;
//If the addr is in the range of a mm's vma?
if (vma == NULL || vma->vm_start > addr) {
// 如果没有匹配到vma
cprintf("not valid addr %x, and can not find it in vma\n", addr);
goto failed;
}
// 页访问异常错误码有32位。位0为1 表示对应物理页不存在;位1为1 表示写异常(比如写了只读页);位2为1 表示访问权限异常(比如用户态程序访问内核空间的数据)
// 对3求模,主要判断bit0、bit1的值
switch (error_code & 3) {
default:
// bit0,bit1都为1,访问的映射页表项存在,且发生的是写异常
// 说明发生了缺页异常
case 2:
// bit0为0,bit1为1,访问的映射页表项不存在、且发生的是写异常
if (!(vma->vm_flags & VM_WRITE)) {
// 对应的vma块映射的虚拟内存空间是不可写的,权限校验失败
cprintf("do_pgfault failed: error code flag = write AND not present, but the addr's vma cannot write\n");
// 跳转failed直接返回
goto failed;
}
// 校验通过,则说明发生了缺页异常
break;
case 1:
// bit0为1,bit1为0,访问的映射页表项存在,且发生的是读异常(可能是访问权限异常)
cprintf("do_pgfault failed: error code flag = read AND present\n");
// 跳转failed直接返回
goto failed;
case 0:
// bit0为0,bit1为0,访问的映射页表项不存在,且发生的是读异常
if (!(vma->vm_flags & (VM_READ | VM_EXEC))) {
// 对应的vma映射的虚拟内存空间是不可读且不可执行的
cprintf("do_pgfault failed: error code flag = read AND not present, but the addr's vma cannot read or exec\n");
goto failed;
}
// 校验通过,则说明发生了缺页异常
}
// 构造需要设置的缺页页表项的perm权限
uint32_t perm = PTE_U;
if (vma->vm_flags & VM_WRITE) {
perm |= PTE_W;
}
// 构造需要设置的缺页页表项的线性地址(按照PGSIZE向下取整,进行页面对齐)
addr = ROUNDDOWN(addr, PGSIZE);
ret = -E_NO_MEM;
// 用于映射的页表项指针(page table entry, pte)
pte_t *ptep=NULL;
// 获取addr线性地址在mm所关联页表中的页表项
// 第三个参数=1 表示如果对应页表项不存在,则需要新创建这个页表项
if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {
cprintf("get_pte in do_pgfault failed\n");
goto failed;
}
// 如果对应页表项的内容每一位都全为0,说明之前并不存在,需要设置对应的数据,进行线性地址与物理地址的映射
if (*ptep == 0) {
// 令pgdir指向的页表中,la线性地址对应的二级页表项与一个新分配的物理页Page进行虚实地址的映射
if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
cprintf("pgdir_alloc_page in do_pgfault failed\n");
goto failed;
}
}
else {
// 如果不是全为0,说明可能是之前被交换到了swap磁盘中
if(swap_init_ok) {
// 如果开启了swap磁盘虚拟内存交换机制
struct Page *page=NULL;
// 将addr线性地址对应的物理页数据从磁盘交换到物理内存中(令Page指针指向交换成功后的物理页)
if ((ret = swap_in(mm, addr, &page)) != 0) {
// swap_in返回值不为0,表示换入失败
cprintf("swap_in in do_pgfault failed\n");
goto failed;
}
// 将交换进来的page页与mm->padir页表中对应addr的二级页表项建立映射关系(perm标识这个二级页表的各个权限位)
page_insert(mm->pgdir, page, addr, perm);
// 当前page是为可交换的,将其加入全局虚拟内存交换管理器的管理
swap_map_swappable(mm, addr, page, 1);
page->pra_vaddr = addr;
}
else {
// 如果没有开启swap磁盘虚拟内存交换机制,但是却执行至此,则出现了问题
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
// 返回0代表缺页异常处理成功
ret = 0;
failed:
return ret;
}
pgdir_alloc_page 建立映射虚实关系:
// 令pgdir指向的页表中,la线性地址对应的二级页表项与一个新分配的物理页Page进行虚实地址的映射
struct Page *
pgdir_alloc_page(pde_t *pgdir, uintptr_t la, uint32_t perm) {
// 分配一个新的物理页用于映射la
struct Page *page = alloc_page();
if (page != NULL) { // !=null 分配成功
// 建立la对应二级页表项(位于pgdir页表中)与page物理页基址的映射关系
if (page_insert(pgdir, page, la, perm) != 0) {
// 映射失败,释放刚才分配的物理页
free_page(page);
return NULL;
}
// 如果启用了swap交换分区功能
if (swap_init_ok){
// 将新映射的这一个page物理页设置为可交换的,并纳入全局swap交换管理器中管理
swap_map_swappable(check_mm_struct, la, page, 0);
// 设置这一物理页关联的虚拟内存
page->pra_vaddr=la;
// 校验这个新分配出来的物理页page是否引用次数正好为1
assert(page_ref(page) == 1);
}
}
return page;
}
swap_fifo.c
static int
_fifo_init_mm(struct mm_struct *mm)
{
// 初始化先进先出链表队列
list_init(&pra_list_head);
mm->sm_priv = &pra_list_head;
return 0;
}
static int
_fifo_map_swappable(struct mm_struct *mm, uintptr_t addr, struct Page *page, int swap_in)
{
// 获取到mm_struct关联的先进先出链表队列
list_entry_t *head=(list_entry_t*) mm->sm_priv;
// 获取参数page结构对应的swap链表节点
list_entry_t *entry=&(page->pra_page_link);
assert(entry != NULL && head != NULL);
//record the page access situlation
/*LAB3 EXERCISE 2: YOUR CODE*/
//(1)link the most recent arrival page at the back of the pra_list_head qeueue.
// 将其加入队列的头部(先进先出,最新的page页被挂载在最头上)
list_add(head, entry);
return 0;
}
/*
* (4)_fifo_swap_out_victim: According FIFO PRA, we should unlink the earliest arrival page in front of pra_list_head qeueue,
* then assign the value of *ptr_page to the addr of this page.
*/
static int
_fifo_swap_out_victim(struct mm_struct *mm, struct Page ** ptr_page, int in_tick)
{
// 获取到mm_struct关联的先进先出链表队列
list_entry_t *head=(list_entry_t*) mm->sm_priv;
assert(head != NULL);
assert(in_tick==0);
/* Select the victim */
/*LAB3 EXERCISE 2: YOUR CODE*/
//(1) unlink the earliest arrival page in front of pra_list_head qeueue
//(2) assign the value of *ptr_page to the addr of this page
/* Select the tail */
// 找到头节点的前一个(双向循环链表 head的前一个节点=队列的最尾部节点)
list_entry_t *le = head->prev;
assert(head!=le);
// 获得尾节点对应的page结构
struct Page *p = le2page(le, pra_page_link);
// 将le节点从先进先出链表队列中删除
list_del(le);
assert(p !=NULL);
// 令ptr_page指向被挑选出来的page
*ptr_page = p;
return 0;
}