Files
912-notes/thu_os/lab3_report.md
2019-09-02 20:31:49 +08:00

375 lines
22 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Lab 3 Report
============
## 实验目的
+ 了解虚拟内存的Page Fault异常处理实现
+ 了解页替换算法在操作系统中的实现
## 实验内容
本次实验是在实验二的基础上借助于页表机制和实验一中涉及的中断异常处理机制完成Page Fault异常处理和FIFO页替换算法的实现结合磁盘提供的缓存空间从而能够支持虚存管理提供一个比实际物理内存空间“更大”的虚拟内存空间给系统使用。这个实验与实际操作系统中的实现比较起来要简单不过需要了解实验一和实验二的具体实现。实际操作系统系统中的虚拟内存管理设计与实现是相当复杂的涉及到与进程管理系统、文件系统等的交叉访问。如果大家有余力可以尝试完成扩展练习实现extended clock页替换算法。
## 练习
对实验报告的要求:
+ 基于markdown格式来完成以文本方式为主
+ 填写各个基本练习中要求完成的报告内容
+ 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
+ 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
+ 列出你认为OS原理中很重要但在实验中没有对应上的知识点
## 新增加的文件概述
lab3相对于前面的lab2增加了虚拟内存的管理为此lab3中增加了一些关键文件。首先对这些文件的功能做一个简单的说明后面还会有更详细的分析。
为了进行对虚拟内存的管理,需要有相关数据结构的支持,其中的两个关键数据结构`struct mm_struct` `struct vma_struct`以及相关的函数定义在`mm/vmm.c(h)`当中。通过这两个结构体,`ucore`模拟了一个用户进程的虚拟地址空间,并对其进行相关的测试工作。
`mm/swap.c(h)`中,定义了页面置换算法的框架`struct smap_manager`,以及为了完成页面换入换出所需要的一些必要函数,还有相关的测试代码。
`mm/swap_fifo.c(h)`中,利用`struct swap_manager`框架实现了先进先出置换算法`FIFO`(其实没有实现,需要自己编程实现)。
此外,为了将页面换入换出到外存上,需要支持对外存的读写操作,这些函数定义在了`driver/ide.c`当中。
## 虚拟内存管理
lab3中需要完成对虚拟内存的管理工作。上面已经说到虚拟内存的管理是通过两个结构体`mm_struct`以及`vmm_struct`来实现的,下面主要阐述这两个结构体如何实现对虚拟内存的管理。
### `vma_struct`
`vma_struct`是用于描述一个进程的连续虚拟内存空间的,如果一个进程有若干个相互之间不连续的虚拟内存空间,则每个虚拟内存空间都需要维护一个`vma_struct`结构体。`vma_struct`的结构如下:
```c
// the virtual continuous memory area(vma), [vm_start, vm_end),
// addr belong to a vma means vma.vm_start<= addr <vma.vm_end
struct vma_struct {
struct mm_struct *vm_mm; // the set of vma using the same PDT
uintptr_t vm_start; // start addr of vma
uintptr_t vm_end; // end addr of vma, not include the vm_end itself
uint32_t vm_flags; // flags of vma
list_entry_t list_link; // linear list link which sorted by start addr of vma
};
```
可以看到,其中的`vm_start`以及`vm_end`分别描述该连续虚拟内存空间的起始虚拟地址和结束虚拟地址,一个进程的各个`vma_struct`之间通过`vma_struct::list_link`连接起来,构成了整个进程的虚拟内存空间。
### `mm_struct`
`mm_struct`可以被认为是进程控制块(PCB, Process Control Block)之类的结构,一个`mm_struct`代表了一个进程。`mm_struct`的结构如下:
```c
// 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
struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
pde_t *pgdir; // the PDT of these vma
int map_count; // the count of these vma
void *sm_priv; // the private data for swap manager
};
```
可以看到,`mm_struct`维护了一个页目录表`mm_struct::pgdir`,它是当前进程的页目录表,将当前进程的虚拟地址映射到物理地址上。此外,`mm_struct::mmap_list`其实是`vma_struct`的链表的头节点,通过这个`mmap_list`可以将当前进程的各个虚拟地址空间连接起来,并且用`map_count`来指示这些虚拟地址空间的数量。通过这两个数据结构,就可以实现进程的虚拟地址空间向物理地址空间的映射,如图所示:
![virt_memory2phy_memory](images/vm2pm.png)
此外,需要注意的是,`mm_struct`还维护了一个`mmap_cache`变量,这其实保存的是当前进程上一次访问的连续虚拟地址空间。根据局部性原理,该进程接下来极有可能再次访问该虚拟地址空间,从而可以在下次访问它时,可以直接获得对应的`vma_struct`,而不需要遍历`vma_struct`链表来找到它。
通过对虚拟地址空间的管理,操作系统可以完成一系列的工作,例如检查当前进程所要访问的虚拟地址是否合法。检查的方法是在该进程的虚拟地址空间查找要访问的虚拟地址,如果的确在其中,则说明该地址是一个合法的访问地址,否则就会触发页访问异常。相关的代码如下:
```c
// find_vma - find a vma (vma->vm_start <= addr <= vma_vm_end)
struct vma_struct *
find_vma(struct mm_struct *mm, uintptr_t addr) {
struct vma_struct *vma = NULL;
if (mm != NULL) {
vma = mm->mmap_cache;
if (!(vma != NULL && vma->vm_start <= addr && vma->vm_end > addr)) {
bool found = 0;
list_entry_t *list = &(mm->mmap_list), *le = list;
while ((le = list_next(le)) != list) {
vma = le2vma(le, list_link);
if (vma->vm_start<=addr && addr < vma->vm_end) {
found = 1;
break;
}
}
if (!found) {
vma = NULL;
}
}
if (vma != NULL) {
mm->mmap_cache = vma;
}
}
return vma;
}
```
查找的方法就是从`mm_struct::mmap_cache`开始遍历整个`vma_struct`链表,直到找到一个对应的虚拟地址空间,或者始终没有找到。
## 练习一:给未被映射的地址映射上物理页(需要编程)
完成`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的缺页服务例程在执行过程中访问内存出现了页访问异常请问硬件要做哪些事情
### 页访问异常的处理
产生页访问异常的原因有很多,比如
+ 目标页帧不存在,即页表项全为零,表示没有给虚拟页分配物理页帧。
+ 相应的物理页帧不在内存当中,而是在磁盘的`swap`分区。此时页表项的`PTE_P`标志位为零。
+ 访问权限出错,比如试图写只读的页,或者用户进程试图访问内核的地址空间。
所有这些情况都会产生页访问异常(Page Fault, PF),因此页访问异常的处理函数首先需要解决的问题,就是如何来分辨当前页访问异常产生的原因,从而根据不同的情况采取不同的解决方法。
> 如何辨别不同原因产生的页访问异常?
在发生页访问异常时CPU会将产生异常的线性地址存储在`CR2`寄存器中并且把页访问异常类型的值简称页访问异常错误码errorCode保存在中断栈中。因此可以通过这两个信息来追溯页访问异常的原因。
lab3中采用的方法是将这两个信息接合起来考虑首先根据错误码来定位某些原因引起的页访问异常再利用`CR2`寄存器中的线性地址,通过手动查当前进程的页目录表,来进一步对页访问异常进行定位。页访问异常错误的结构如下所示:
![page_fault_errorcode](images/page_fault_error_code.png)
我们这里只用到了第零位和第一位,毕竟目前还没有用户态,第二位的内核访问/用户访问也无从说起。根据错误码来对页访问异常进行辨别的代码如下所示:
```c
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
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, P=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;
}
}
// other operations
//......
```
可以看到,根据异常错误码,主要是对一些权限信息进行检查。例如当前进程是否在试图写一个不可写的页面,或者被访问的页面是否是不可读也是不可执行的,如果的确出现了权限的越界,操作系统应该终止这些操作的进行,并且直接返回错误信息。而如果所有权限信息都是正确的,发生页访问异常的原因只是当前页面不存在(`P = 0`),则需要进行后续的操作,以判断是进行内存分配,还是页面置换。此时,就需要结合存储在`CR2`寄存器中的线性地址来进行进一步的判断了。
前面已经提到过,具体的方法就是查询当前进程的页目录表(`mm_struct::pgdir`),通过得到的页表项来进行判断--如果页表项全为零,则表示不存在当前虚拟地址的映射关系,此时应该给当前的虚拟页分配一个新的页面;而如果页表项不为零,只是`PTE_P`为零,则表示要访问的页不在内存中,需要进行页面置换,这部分将在练习二中讨论。
### 给未被映射的地址映射上物理页
如果访问的地址尚未被映射到物理页,则在页访问异常的处理函数`do_pgfault`中,需要给该页也分配一个新的物理页帧,并且将分配的物理页的地址以及相关的权限信息填入到页表项中,这和我们前面的内容是一致的。
但是这里需要考虑特殊的情况假如当前内存中已经没有空闲的页面了又应该如何操作呢在lab2中我们是直接返回错误码然后就退出了但是这在引入了虚拟内存地址的现在显然是不合适的。因此此时是需要调用页面置换算法从当前进程的页面中选择一个置换到外存中随后再进行存储空间的分配。然后这一系列的操作老师都已经封装成的一个函数见下面的代码
```c
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;
/*LAB3 EXERCISE 1: YOUR CODE*/
ptep = get_pte(mm->pgdir, addr, 1); //(1) try to find a pte, if pte's PT(Page Table) isn't existed, then create a PT.
if (*ptep == 0) {
pgdir_alloc_page(mm->pgdir, addr, perm | PTE_P); //(2) if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
}
```
这里的`pgdir_alloc_page`函数,已经实现了上面我所描述的功能。在它的内部,是调用了`alloc_pages`函数,其实现如下:
```c
//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);
{
page = pmm_manager->alloc_pages(n);
}
local_intr_restore(intr_flag);
if (page != NULL || n > 1 || swap_init_ok == 0) break;
extern struct mm_struct *check_mm_struct;
swap_out(check_mm_struct, n, 0);
}
return page;
}
```
可以看到,在`alloc_pages`函数内部,已经包含了页面置换的功能。
我的exercise1的实现和老师的大体相同吧但是我的肯定还是有问题的老师的代码主要是多了很多异常的检查工作比我的代码健壮多了见下
```c
// try to find a pte, if pte's PT(Page Table) isn't existed, then create a PT.
// (notice the 3th parameter '1')
if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {
cprintf("get_pte in do_pgfault failed\n");
goto failed;
}
if (*ptep == 0) { // if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
cprintf("pgdir_alloc_page in do_pgfault failed\n");
goto failed;
}
}
```
## 练习2补充完成基于FIFO的页面替换算法需要编程
完成`vmm.c`中的`do_pgfault`函数,并且在实现`FIFO`算法的`swap_fifo.c`中完成`map_swappable``swap_out_victim`函数。通过对`swap`的测试。注意:在`LAB2 EXERCISE 2`处填写代码。执行
```
make qemu
```
后,如果通过`check_swap`函数的测试后,会有`check_swap() succeeded!`的输出表示练习2基本正确。
请在实验报告中简要说明你的设计实现过程。
请在实验报告中回答如下问题:
+ 如果要在ucore上实现"extended clock页替换算法"请给你的设计方案,现有的`swap_manager`框架是否足以支持在ucore中实现此算法如果是请给你的设计方案。如果不是请给出你的新的扩展和基此扩展的设计方案。并需要回答如下问题
- 需要被换出的页的特征是什么?
- 在ucore中如何判断具有这样特征的页
- 何时进行换入和换出操作?
### `FIFO`算法的实现
`FIFO`算法其实就维护一个按页面在内存中的驻留时间排序的队列。在未出现缺页异常的页面访问时,`FIFO`算法不需要做任何操作,因为此时访问的页面必然早就进入了内存,并且早就被添加到页面队列了。因此,只有在产生页面异常时,`FIFO`算法才具有实质性的操作,包括首先将队列头取出,它代表了驻留内存时间最长的页面,对应于`swap_out_victim`函数,将其换出到外存当中;继而将新进入的页面添加到队列尾,对应于`map_swappable`函数。
为了实现`FIFO`算法,对于每一个进程,我们都需要维护这样的一个驻留页面队列,事实上,`mm_struct::sm_priv`就是为了页面置换算法而预留的,由于它是一个`void*`类型的指针,即它可以指向任何类型,因此这个变量不只是可以用来维护`FIFO`的驻留页面队列,对于其他的页面置换算法,该变量也可以用来指向它们所需要维护的数据结构。
除此以外,还需要扩展`struct Page`结构体,需要给它增加一个新的字段,将这些驻留在内存中的页面链接起来,并且使`mm_struct::sm_priv`作为这个链表的头节点。这样,通过对进程进行描述的`mm_struct`结构体,就还可以获得该进程的页面访问信息了。
但是目前为止还存在一个问题--`struct Page`是对物理页帧进行描述的结构体,通过物理页帧并不能直接找到与之映射的虚拟页面,从而找不到对应于该物理页帧的页表项,而将页面换出是需要修改页表项的。为了解决这个问题,还需要给`struct Page`增加一个字段,指出当前映射到它的虚拟地址。这样,`struct Page`的结构如下所示:
```c
/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property; // the num of free block, used in first fit pm manager
list_entry_t page_link; // free list link
list_entry_t pra_page_link; // used for pra (page replace algorithm)
uintptr_t pra_vaddr; // used for pra (page replace algorithm)
};
```
到目前为止,终于可以实现`FIFO`算法了,对于`map_swappable`函数,主要功能就是将刚进入内存的页面,添加到页面队列的队尾,即页面链表的最后。而`swap_out_victim`函数,主要功能就是取出页面队列的队首,并且将该被置换的页面返回。具体的实现如下:
```c
/*
* (3)_fifo_map_swappable: According FIFO PRA, we should link the most recent arrival page at the back of pra_list_head qeueue
*/
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_before(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 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
list_entry_t *victim = list_next(head);
list_del(victim);
struct Page* p = le2page(victim, pra_page_link);
*ptr_page = p;
return 0;
}
```
相关的实现都非常简单啊,我就不多说了。
### 缺页异常的处理
接下来是应该补全`do_pgfault`函数,前面已经处理了页面不存在的情况,还剩下页面不在内存中,而是驻留在外存的情况没有处理。
这里首先需要解决一个问题,如果页面在外存中,那么又应该如何在外存中找到该页面呢?我们这里的方法是利用了“交换表项”(swap_entry),它的结构如图所示:
![swap_entry](images/swap_entry.png)
它的高24位是当前页面所驻留的磁盘扇区号这样只要读对应的磁盘扇区就可以将该页面从外存中换入内存了。需要说明的是这里我们并没有利用额外的空间来存储这个“交换表项”而是继续沿用页表项来存储这样的磁盘扇区号的。可以这样做是因为可以通过`PTE_P`位的值来将页表项与“交换表项”区别开,当`PTE_P`位的值是1时当前页表项就是存储内存中某个页面的起始地址以及相关的权限`PTE_P`位的值是0时当前的页表项就成了“交换表项”用来保存被交换到磁盘上的该页的磁盘扇区号。
这样,为了处理缺页异常,只需要调用`swap_out_victim`,将某一页置换出内存,再将被访问的页换入到该物理页帧的空间中,然后将其添加到页面队列的队尾,并且修改对应的页表项就可以了。对应的代码如下:
```c
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) {
struct Page *page=NULL;
swap_in(mm, addr, &page);
page->pra_vaddr = addr;
page_insert(mm->pgdir, page, addr, perm | PTE_P);
swap_map_swappable(mm, addr, page, 0);
}
else {
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
...
```
这里并没有显式地调用`swap_out`函数来将某一页面换出,这是因为在`swap_in`函数的实现中,是首先调用了前面提到的`alloc_pages`函数,在物理内存不够时,通过这个函数来调用`swap_out`以将某个页面换出。由此也可以看出,我们采用的是消极的页面换出机制,即只有在内存空间不足时,才执行页面的换出;在还有剩余的内存空间时,直接给要换入的页面分配新的内存空间,并不执行换出操作。