实验三
本次实验是在实验二的基础上, 借助于页表机制和实验一中涉及的中断异常处理机制, 完成Page Fault异常处理和FIFO页替换算法的实现, 结合磁盘提供的缓存空间, 从而能够支持虚存管理, 提供一个比实际物理内存空间“更大”的虚拟内存空间给系统使用。 这个实验与实际操作系统中的实现比较起来要简单, 不过需要了解实验一和实验二的具体实现。 实际操作系统系统中的虚拟内存管理设计与实现是相当复杂的, 涉及到与进程管理系统、 文件系统等的交叉访问。 如果大家有余力, 可以尝试完成扩展练习, 实现extended clock页替换算法。
本实验依赖实验1/2。 请把你做的实验1/2的代码填入本实验中代码中有“LAB1”,“LAB2”的注释相应部分。
下面是实验指导书给出的参考,仔细阅读这些改动。
本练习比较简单,仅仅是将实验1、2的代码进行复制粘贴即可,因此不在此赘述。
完成do_pgfault( mm/vmm.c) 函数, 给未被映射的地址映射上物理页。 设置访问权限 的时候需要参考页面所在 VMA 的权限, 同时需要注意映射物理页时需要操作内存控制 结构所指定的页表, 而不是内核的页表。 注意:在LAB2 EXERCISE 1处填写代码。
下面是练习1所需要补全的代码,依赖注释我们可以轻松得到:
/*LAB3 EXERCISE 1: YOUR CODE
* Maybe you want help comment, BELOW comments can help you finish the code
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* get_pte : get an pte and return the kernel virtual address of this pte for la
* if the PT contians this pte didn't exist, alloc a page for PT (notice the 3th parameter '1')
* pgdir_alloc_page : call alloc_page & page_insert functions to allocate a page size memory & setup
* an addr map pa<--->la with linear address la and the PDT pgdir
* DEFINES:
* VM_WRITE : If vma->vm_flags & VM_WRITE == 1/0, then the vma is writable/non writable
* PTE_W 0x002 // page table/directory entry flags bit : Writeable
* PTE_U 0x004 // page table/directory entry flags bit : User can access
* VARIABLES:
* mm->pgdir : the PDT of these vma
*
*/
#if 0
/*LAB3 EXERCISE 1: YOUR CODE*/
ptep = ??? //(1) try to find a pte, if pte's PT(Page Table) isn't existed, then create a PT.
if (*ptep == 0) {
//(2) if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
}
#end if
//下面是补全的代码
if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {//尝试找到pte,若对应的页表项不存在,则转去创建一个页表项
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;
}
}
执行make qemu
后得到实验结果基本没有问题,具体的流程参看中文注释:
页目录项是指向储存页表的页面的, 所以本质上与页表项相同, 结构也应该相同. 每个页表项的高20位, 就是该页表项指向的物理页面的首地址的高20位(当然物理页面首地址的低12位全为零), 而每个页表项的低12为, 则是一些功能位, 可以通过在mmu.h
中的一组宏定义发现.
#define PTE_P 0x001 // Present 对应物理页面是否存在
#define PTE_W 0x002 // Writeable 对应物理页面是否可写
#define PTE_U 0x004 // User 对应物理页面用户态是否可以访问
#define PTE_PWT 0x008 // Write-Through 对应物理页面在写入时是否写透(即向更低级储存设备写入)
#define PTE_PCD 0x010 // Cache-Disable 对应物理页面是否能被放入高速缓存
#define PTE_A 0x020 // Accessed 对应物理页面是否被访问
#define PTE_D 0x040 // Dirty 对应物理页面是否被写入
#define PTE_PS 0x080 // Page Size 对应物理页面的页面大小
#define PTE_MBZ 0x180 // Bits must be zero 必须为零的部分
#define PTE_AVAIL 0xE00 // Available for software use 用户可自定义的部分
对于实现页替换算法来说,页目录项(pgdir)作为一个双向链表存储了目前所有的页的物理地址和逻辑地址的对应,即在实内存中的所有页,替换算法中被换出的页从pgdir中选出。页表(pte)则存储了替换算法中被换入的页的信息,替换后会将其映射到一物理地址。
产生页访问异常后,CPU把引起页访问异常的线性地址装到寄存器CR2中,并给出了出错码errorCode,说明了页访问异常的类型。ucore OS会把这个值保存在struct trapframe 中tf_err成员变量中。而中断服务例程会调用页访问异常处理函数do_pgfault进行具体处理。
完成vmm.c中的do_pgfault函数, 并且在实现FIFO算法的swap_fifo.c中完成map_swappable和swap_out_vistim函数。 通过对swap的测试。 注意:在LAB2 EXERCISE 2处填写代码。
下面是vmm.c
文件中需要补全的do_pgfault
部分,补完结果如下:
#if 0
else {
/*LAB3 EXERCISE 2: YOUR CODE
* Now we think this pte is a swap entry, we should load data from disk to a page with phy addr,
* and map the phy addr with logical addr, trigger swap manager to record the access situation of this page.
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* swap_in(mm, addr, &page) : alloc a memory page, then according to the swap entry in PTE for addr,
* find the addr of disk page, read the content of disk page into this memroy page
* page_insert : build the map of phy addr of an Page with the linear addr la
* swap_map_swappable : set the page swappable
*/
if(swap_init_ok) {
struct Page *page=NULL;
//(1)According to the mm AND addr, try to load the content of right disk page
// into the memory which page managed.
//(2) According to the mm, addr AND page, setup the map of phy addr <---> logical addr
//(3) make the page swappable.
}
else {
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
#endif
else { // if this pte is a swap entry, then load data from disk to a page with phy addr
// and call page_insert to map the phy addr with logical addr
if(swap_init_ok) {//pte是需要交换的表项
struct Page *page=NULL;//创建一个新页
if ((ret = swap_in(mm, addr, &page)) != 0) {//利用mm结构和addr地址,尝试将硬盘中的内容换入到新的page中
cprintf("swap_in in do_pgfault failed\n");//若失败,则退出
goto failed;
}
page_insert(mm->pgdir, page, addr, perm);//将该页面插入到队列之中,并且建立虚拟地址与物理地址间的对应关系
swap_map_swappable(mm, addr, page, 1);//设置该页面为可交换的
}
else {
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
ret = 0;
failed:
return ret;
}
下面是swap_fifo.c
中需要补完的map_swappable
和swap_out_vistim
函数,补完结果如下:
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);
//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.
list_add(head, entry);//将最近分配的页插入到pra_list_head队列的尾部
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 set the addr of addr of this page to ptr_page.
*/
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);
/* Select the victim */
/*LAB3 EXERCISE 2: YOUR CODE*/
//(1) unlink the earliest arrival page in front of pra_list_head qeueue
//(2) set the addr of addr of this page to ptr_page
/* Select the tail */
list_entry_t *le = head->prev;//选择队列中被最早调入的页表
assert(head!=le);
struct Page *p = le2page(le, pra_page_link);
list_del(le);//将换出的页表从队列中删除
assert(p !=NULL);
*ptr_page = p;//将该页的地址存储在ptr_page中
return 0;
}
执行make qemu
后得到实验结果无问题
本实验的主要设计思路为:
do_pgfault
即为进行页面换入。我认为,目前的swap_manager框架足以支持在ucore中实现extended clock算法,在kern/mm/mmu.h文件中有如下定义:
/* page table/directory entry flags */
#define PTE_P 0x001 // Present
#define PTE_W 0x002 // Writeable
#define PTE_U 0x004 // User
#define PTE_PWT 0x008 // Write-Through
#define PTE_PCD 0x010 // Cache-Disable
#define PTE_A 0x020 // Accessed
#define PTE_D 0x040 // Dirty
#define PTE_PS 0x080 // Page Size
#define PTE_MBZ 0x180 // Bits must be zero
#define PTE_AVAIL 0xE00 // Available for software use
// The PTE_AVAIL bits aren't used by the kernel or interpreted by the
// hardware, so user processes are allowed to set them arbitrarily.
#define PTE_USER (PTE_U | PTE_W | PTE_P)
其中PTE_A
中的内容的即标志着该页是否被访问过,由此我们可以实现extended clock
算法。于是我们可以对kern/mm/swap_fifo.c
做相应的修改,判断是否被访问过即可:
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->next;
assert(head != le);
while(le != head)
{
struct Page *p = le2page(le, pra_page_link);
pte_t *ptep = get_pte(mm->pgdir, p->pra_vaddr, 0);
if(!(*ptep & PTE_A))
{ //未被访问
list_del(le);
assert(p != NULL);
*ptr_page = p;
return 0;
}
*ptep ^= PTE_A;
le = le->next;
}
le = le->next;
while(le != head)
{
struct Page *p = le2page(le, pra_page_link);
pte_t *ptep = get_pte(mm->pgdir, p->pra_vaddr, 0);
list_del(le);
assert(p != NULL);
*ptr_page = p;
return 0;
}
}
需要被换出的页的特征是什么?
最早被换入,且最近没有被访问过的页。
在ucore中如何判断具有这样特征的页?
首先判断其最近有没有被访问过(利用条件*ptep & PTE_A
进行判断),若无,则按照FIFO原则进行置换。
何时进行换入和换出操作?
当需要调用的页不在页表中时,并且在页表已满的情况下,需要进行换入和换出操作。
问题分析:算法根据页面近期是否被修改从而决定该页面是否应当被换出。所以在查询空闲页时,需要加上对dirty bit的判断。
大体思路:当操作系统需要淘汰页时,对当前指针指向的页所对应的页表项进行查询,如果dirty bit为0,则把此页换出到硬盘上;如果dirty bit为1,则将dirty bit置为0,继续访问下一个页。
相比较FIFO的操作,dirty bit的替换算法只需要识别出哪些页被访问过,以及哪些页被修改过即可。在kern/mm/mmu.h文件下有如下的定义:
#define PTE_A 0x020 // Accessed
#define PTE_D 0x040 // Dirty
其中PTE_A和PTE_D分别是表示访问和修改的标识位,因此与*ptep求与即可判断页是否被访问或修改过。首先根据基础的extended clock算法,未被访问的页应优先考虑换出;在此基础上,由于被修改的也需要被写回硬盘,因此未被修改的页应该有限换出。因此采用多轮循环。只需要修改kern/mm/vmm.h
中的_fifo_swap_out_victim()
函数即可实现:
_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);
//将head指针指向最先进入的页面
list_entry_t *le = head->next;
assert(head != le);
//查找最先进入并且未被修改的页面
while(le != head) {
struct Page *p = le2page(le, pra_page_link);
//获取页表项
pte_t *ptep = get_pte(mm->pgdir, p->pra_vaddr, 0);
//判断获得的页表项是否正确
if(!(*ptep & PTE_A) && !(*ptep & PTE_D)) { //未被访问,未被修改
//如果dirty bit为0,换出
//将页面从队列中删除
list_del(le);
assert(p != NULL);
//将这一页的地址存储在prt_page中
*ptr_page = p;
return 0;
}
le = le->next;
}
le = le->next;
while(le != head) {
struct Page *p = le2page(le, pra_page_link);
pte_t *ptep = get_pte(mm->pgdir, p->pra_vaddr, 0);
if(!(*ptep & PTE_A) && (*ptep & PTE_D)) { //未被访问,已被修改
list_del(le);
assert(p != NULL);
*ptr_page = p;
return 0;
}
*ptep ^= PTE_A; //页被访问过则将PTE_A位置0
le = le->next;
}
le = le->next;
while(le != head) {
struct Page *p = le2page(le, pra_page_link);
pte_t *ptep = get_pte(mm->pgdir, p->pra_vaddr, 0);
if(!(*ptep & PTE_D)) { //未被修改,此时所有页均被访问过,即PTE_A位为0
list_del(le);
assert(p != NULL);
*ptr_page = p;
return 0;
}
le = le->next;
}
//如果这行到这里证明找完一圈,所有页面都不符合换出条件
//那么强行换出最先进入的页面
le = le->next;
while(le != head) {
struct Page *p = le2page(le, pra_page_link);
pte_t *ptep = get_pte(mm->pgdir, p->pra_vaddr, 0);
if(*ptep & PTE_D) { //已被修改
list_del(le);
assert(p != NULL);
//将这一页的地址存储在ptr_page中
*ptr_page = p;
return 0;
}
le = le->next;
}
}