add more to lab2_report, together with two images in ./images/, not finished yet.

This commit is contained in:
Shine wOng
2019-08-28 19:16:10 +08:00
parent 403ae6b550
commit 5894e99465
4 changed files with 220 additions and 1067 deletions

BIN
thu_os/images/get_pte.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -25,7 +25,7 @@ Lab 2 Report
## 文件架构与执行流程
lab2的文件架构相对lab1产生了一些区别在后面可以看到这些区别的存在必要的都是为了操作系统可以正常地运行。下面简述一下lab2中各个文件执行的流程。
lab2的文件架构相对lab1产生了一些区别在后面可以看到这些区别的存在必要的都是为了操作系统可以正常地运行。下面简述一下lab2中各个文件执行的流程。
+ 首先自然是`bios`的程序,在机器上电时被加载到`0xffff0`地址处,完成相关设备的初始化工作,并且到硬盘的主引导扇区读取`bootloader``0x7c00`地址处。
+ 此后机器的控制权就交给了`bootloader`,首先是`bootasm.S`中的汇编代码开始执行在其中完成CPU从实模式到保护模式的转换为此需要首先使能`A20`地址线初始化段表等这些都是lab1里面就完成了的工作。与lab1不同的是`bootasm.S`中,还进行了对物理内存的探测工作,是通过`bios`系统调用来完成的。完成段表的建立与各个段寄存器的初始化后就建立起了C语言的运行环境程序转入`bootmain.c`中开始执行。
@@ -126,7 +126,7 @@ virt addr - 0xC0000000 = phy addr
需要注意的是,由于`entry.S`也是内核代码的一部分,因此其中涉及的内存地址都是虚拟地址,在访存时需要手动进行虚拟地址向物理地址的转化,才能访问到正确的内存空间,具体的代码如下:
```R
```asm
#define REALLOC(x) (x - KERNBASE)
.text
@@ -175,7 +175,7 @@ __gdtdesc:
在进行对物理内存的分配与回收之前,肯定首先需要知道当前的空闲物理内存有哪些,从而才可以选择某一区段的内存进行分配啊。也就是说,首先我们是需要管理所有的空闲物理内存。通过之前的物理内存探测,我们已经可以知道哪些区域的物理内存是可用的,接下来,我们要建立页式物理内存管理机制,将物理内存组织成一个个固定大小的页帧来进行管理,并且建立起一个链表结构将这些空闲的内存块组织起来,进而实现内存的分配与回收工作。这部分的工作都是在`kern/mm/pmm.c::page_init`函数中实现的。
首先,需要将物理内存组织成一个个固定大小的页帧,这里页帧的大小固定为`4K`。为此,我们就需要知道物理内存的起始点,从而确定可以划分的页帧的数目,对于每一个物理页,都有一个与之对应的`struct Page`结构体与之对应,来表示该物理页的状态,如是否可用或者被操作系统保留。这部分的工作对应了`page_init`函数的前半部分:
首先,需要将物理内存组织成一个个固定大小的页帧,这里页帧的大小固定为`4K`。为此,我们就需要知道物理内存的起始点与终止点,从而确定可以划分的页帧的数目,对于每一个物理页,都有一个与之对应的`struct Page`结构体与之对应,来表示该物理页的状态,如是否可用或者被操作系统保留。这部分的工作对应了`page_init`函数的前半部分:
```c
/* *
@@ -334,3 +334,220 @@ default_init_memmap(struct Page *base, size_t n) {
```
这里,我首先写了一个`insert2free_list`函数,来找到`first fit`算法中合适的插入位置,并且将新的空闲页插入其中。在`default_init_memmap`中,首先进行一些权限的设置,比如将当前页面设置为[可用的],而非继续被操作系统[保留的]等,然后就调用`insert2free_list`将新的页面插入到合适的位置。对于参考答案给出的这个代码,是直接将新的页面插入到页表的末端,可能是老师清楚`INT 15`的中断调用返回的`e820map`地址顺序是有序的,但是我并不清楚这个事实,所以我觉得我的实现也许还更严谨?
### `first fit`算法的实现
在建立好`free_area`来管理所有的空闲内存区块后,就可以着手实现`first fit`连续物理内存分配算法了。主要的工作其实就是实现两个函数,页面分配算法(`alloc_pages`)以及页面回收算法(`free_pages`)。
对于页面分配算法,应该按照空闲内存区块的起始地址从小到大的顺序,遍历这些内存区块,直到发现一个区块大于要分配的内存空间,则将这个区块切割,将要求的内存空间分配出去后,把剩下的空闲内存继续保存到`free_area`中,这其中涉及到一些标志位以及相关状态变量的修改。具体的代码如下:
```c
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL, *p;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list){
p = le2page(le, page_link);
if (p->property >= n) {
page = p;
break;
}
}
if (page != NULL) {
list_del(&(page->page_link));
if (page->property > n) {
p = page + n;
p->property = page->property - n;
insert2free_list(&(p->page_link));
}
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
```
我这里的这个实现其实是还可以改进的。可以看到,我在将剩下的空闲内存再次加入到`free_area`中时,调用了前面提到的`insert2free_list`函数,这个函数会遍历所有的空闲区块,直到发现插入的合适位置,其最坏情况与平均情况的时间复杂度都是`O(n)`。实际上,直接将该剩下的空闲区块插入到`page`后面不就可以了吗?为此只需要首先添加该剩余的空闲区块,之后再删除掉`free_area`中的`page->page_link`就可以了,实际复杂度仅为`O(1)`。实际上,老师给出的参考答案就是这样实现的。
对于页面回收算法,则要相对复杂。因为不仅需要将被回收的页面插入到`free_area`中的合适位置,还需要考虑该新加入的页面是否可以和其前后相邻的页面进行合并,从而组织成一个更大的空闲区块。我的回收算法就是这样实现的,先找到合适的位置插入新的页面,再分别检测是否可以与前后相邻的页面合并。具体的代码如下:
```c
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base, *front;
for (; p != base + n; p ++) {
assert(!PageReserved(p) && !PageProperty(p));
p->flags = 0;
set_page_ref(p, 0);
}
base->property = n;
SetPageProperty(base);
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
p = le2page(le, page_link);
if(base < p){
list_add_before(&(p->page_link), &(base->page_link));
if(base + base->property == p){
base->property += p->property;
ClearPageProperty(p);
list_del(&(p->page_link));
}
if(list_prev(&(base->page_link)) != &free_list){
front = le2page(list_prev(&(base->page_link)), page_link);
if(front + front->property == base){
front->property += base->property;
ClearPageProperty(base);
list_del(&(base->page_link));
}
}
break;
}
}
if(le == &free_list){
list_add_before(&free_list, &(base->page_link));
if(list_prev(&(base->page_link)) != &free_list){
front = le2page(list_prev(&(base->page_link)), page_link);
if(front + front->property == base){
front->property += base->property;
ClearPageProperty(base);
list_del(&(base->page_link));
}
}
}
nr_free += n;
}
```
在主循环里面,我是试图查找新加入的页面的后继页面,将新的页面插入到其后继页面之前,再进行合并的检测。但如果新加入页面是被插入到`free_area`链表的最后,则没有这样的后继页面,同时也只有可能与它的前驱进行合并了,`while`循环之后就是对于这种特殊情况的处理。相对于参考答案的代码,我的版本应该是更好的,因为参考答案用了两次循环,存在大量的冗余操作,当然老师的注释也写了应该对这个代码进行优化。
## 页表的建立
我们知道,页表的本质其实就是虚拟页与物理页帧之间的地址映射关系。因此,页表的建立其实就是将两者联系起来,填写相应的页表项,从而使得操作系统通过页表可以完成从线性地址到物理地址的转化工作。
为了建立两级页表,我们首先要为页目录表分配一个页的存储空间:
```c
// create boot_pgdir, an initial page directory(Page Directory Table, PDT)
boot_pgdir = boot_alloc_page();
```
对于每一个线性地址,通过其高十位来索引页目录表,可以得到唯一的页目录表项,从而得到其对应的页表的起始地址;通过其中间十位索引页表,可以得到唯一的页表项,并且进而得到该页表项所对应的物理页帧。可以看出,建立页表的关键,就是要将物理页帧的地址,填入某个线性地址所对应的页表项中,并且设置相应的权限标志位。为此,我们需要首先找到任何一个线性地址所对应的页表项,这个工作被抽象为下面的`get_pte`函数。
### 练习2实现寻找虚拟地址对应的页表项需要编程
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的`get_pte`函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全`get_pte`函数in`kern/mm/pmm.c`,实现其功能。请仔细查看和理解`get_pte`函数中的注释。`get_pte`函数的调用关系图如下所示:
[get_pte call graph](images/get_pte.png)
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
+ 请描述页目录项(`Page Director Entry`)和页表项(`Page Table Entry`中每个组成部分的含义和以及对ucore而言的潜在用处。
+ 如果ucore执行过程中访问内存出现了页访问异常请问硬件要做哪些事情
> 页目录项与页表项的结构
由于一页的大小是`4K`因此虚拟地址的页偏移量为12位其余的20位就是逻辑页号。这里的20位逻辑页号又划分为页目录表的索引和页表的索引分别是10位。这样可以保证一个页目录表或者页表中恰好有`2^10 = 1024`个表项,每个表项占四个字节,一个页目录表或者页表也就是`4K`大小,恰好可以容纳在一页之中。
页目录表与页表的功能是一致的,都是给出某个物理地址。页目录表是给出相应的页表的物理地址,页表则是给出某个物理页帧的起始物理地址。由于一个页表的大小也恰好是一个物理页帧的大小,因此这两个地址都是相对于`4K`对齐的只需要20位就可以描述。这样页表项或者页目录表项的其余12位就可以用来表示一些控制信息比如访问的权限是否可读或是可执行以及该页表项是否存在。实际上页目录项与页表项的结构如下所示
```
----------------------------------
| 20bits | ... | | | |
----------------------------------
phy page number U W P (PTE_U PTE_W PTE_P)
```
这里的三个标志位`PTE_U, PTE_W, PTE_P`分别代表用户态是否可读该物理内存也是否可写以及该页表项是否存在。此外还有一些其他标识符在lab2中没有出现所以就不一一说明了。这里需要注意一下`PTE_P`标识位若该标志位置一则说明当前的表项是存在的这意味着当前表项的各个字段都是有意义的其前20位确实对应了某个物理页帧。这就是说对应的物理页帧已经建立了与某个线性地址之间的映射关系而该表项不存在则说明了索引到该表项的线性地址还没有对应到任何的物理页帧可能等待后续的操作添加这种对应关系特殊地如果该表项还是一个页目录表项的话就意味着该页目录表项还没有对应任何的页表后续需要分配一页的存储空间来保存新建立的页表。
> `get_pte`函数的实现
`get_pte`的功能是找到与一个线性地址相对应的页表项,因此`get_pte`的流程应该是模拟操作系统查询页目录表与页表的过程即首先用线性地址的高10位索引页目录表再用中间10位索引页表如果顺利的话就可以找到与之对应的页表项返回其虚拟地址就可以了。但是这里我们还需要考虑特殊的情况倘若索引页目录表得到的页目录项并非是有效的(`PTE_P`未设置),就需要调用前面的物理内存分配的函数,为建立一个新的页表分配一页的空间,然后将页表的物理地址填入页目录项中,后续的操作再查这个新的页表就可以了。`get_pte`的具体实现如下:
```c
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
pde_t *pde = pgdir + PDX(la);
struct Page* p;
if(!(*pde & PTE_P)){
if(!create) return NULL;
//else allocate a new page for page table
*pde = PADDR(boot_alloc_page());
*pde = *pde | PTE_P | PTE_U | PTE_W;
p = pa2page(*pde);
set_page_ref(p, 1);
memset(page2kva(p), 0, PGSIZE);
}
return ((pte_t*)KADDR(PDE_ADDR(*pde))) + PTX(la);
}
```
需要注意这里的地址转换,还是有点复杂的,比如说查页目录表得到的是页表的物理地址,为了找到该页表,需要将物理地址转换成虚拟地址。此外这里的`create`参数,是表示页表不存在的时候,是否需要新建一个页表,只有在`create = True`时才新建页表。我这个代码的问题是安全性不足,因为即使`create = 1`,也仍然会出现内存不足,分配不出空间的情况,此时贸然调用`boot_alloc_page()`,可能会出现内核停止工作的情况。参考答案就比我优雅多了:
```c
...
if (!create || (page = alloc_page()) == NULL) {
return NULL;
}
...
```
实现了`get_pte`函数之后,就可以方便地设置页表项了,这个工作体现在`page_insert`函数当中。与设置映射关系相反,有时候也需要取消线性地址与物理地址的映射,这时就需要实现`page_remove_pte`函数。
> 如果ucore执行过程中访问内存出现了页访问异常请问硬件要做哪些事情
不会......需要后面更强的我来完成。不知道这里的页访问异常是怎么出现的,难道是访问了未映射到物理地址的虚拟地址?我不清楚这种情况为什么会出现啊。
### 练习3释放某虚地址所在的页并取消对应二级页表项的映射需要编程
当释放一个包含某虚地址的物理内存页时需要让对应此物理内存页的管理数据结构Page做相关的清除处理使得此物理内存页成为空闲另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解`page_remove_pte`函数中的注释。为此,需要补全在`kern/mm/pmm.c`中的`page_remove_pte`函数。`page_remove_pte`函数的调用关系图如下所示:
![page_remove_pte](images/page_remove_pte.png)
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
+ 数据结构Page的全局变量其实是一个数组的每一项与页表中的页目录项和页表项有无对应关系如果有其对应关系是什么
+ 如果希望虚拟地址与物理地址相等则需要如何修改lab2完成此事 鼓励通过编程来具体完成这个问题
> `page_remove_pte`函数的实现
`page_remove_pte`的功能其实恰好与`page_insert`相反啊,因为它要清除这种线性地址到物理地址的映射关系,按照前面的讨论,其实就是将`PTE_P`标志位清零,为此我们首先需要调用`get_pte`函数来获得与当前线性地址对应的页表项。此外,对于物理页帧,也需要修改某些属性,例如`Page.ref`属性是表示有多少个虚拟地址映射到当前的物理页帧,因此取消一个这样的对应关系后,应该使`Page.ref--`,如果`Page.ref == 0`,则表示已经没有任何虚拟页映射到当前物理页,因此就可以将物理页帧释放掉,以供后续的分配使用。
需要注意的是,取消了线性地址到物理地址的映射关系后,还需要`invalidate TLB`,即更新快表。否则如果快表里面仍然存储当前线性地址到物理地址无效的映射,在后续的内存访问中可能会访问到无效错误的区段,这是非常严重的问题。我实际实现的代码如下:
```c
//page_remove_pte - free a Page sturct which is related to linear address la
// - and clean(invalidate) pte which is related to linear address la
//note: PT is changed, so the TLB needs to be invalidated
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
//assert(ptep == get_pte(la));
if(ptep != NULL && (*ptep & PTE_P)){
struct Page* page = pte2page(*ptep);
page_ref_dec(page);
if(page->ref == 0){
free_page(page);
*ptep = *ptep & ~PTE_P;
tlb_invalidate(pgdir, la);
}
}
}
```
很容易可以看出,我这里的实现是错误的,最后两条语句应该放在`if`判断语句之外,即
```c
...
if(page->ref == 0){
free_page(page);
}
*ptep = *ptep & ~PTE_P;
tlb_invalidate(pgdir, la);
...
```
因为只要的确取消了`la``ptep`的映射关系,无论`page->ref`是否减到了零,都应该使当前页表项无效化,并且刷新快表`TLB`。然而最神奇的是,我这个错误的代码还通过了测试......

File diff suppressed because one or more lines are too long