You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

242 lines
8.9 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 第五章实验4缺页异常的处理
### 5.1 实验内容
实验要求在APP里写递归程序其执行过程导致栈的不断增长。在代理内核中实现缺页中断的处理例程trap使其能够支撑递归程序的正确执行。
**练习一:缺页中断实例的完善(需要编程)**
**在**"pk/mmap.c"内有__handle_page_fault()函数,完善该函数,实现缺页中的处理。
```
202 static int __handle_page_fault(uintptr_t vaddr, int prot)
203 {
204 printk("page fault vaddr:0x%lx\n", vaddr);
205 //your code here
206 //start------------>
207 pte_t* pte =0;
208
209 //<-----------end
210 if (pte == 0 || *pte == 0 || !__valid_user_range(vaddr, 1))
211 return -1;
212 else if (!(*pte & PTE_V))
213 {
214
215 //your code here
216 //start--------->
217
218 uintptr_t ppn =0;
219 vmr_t* v = NULL;
220
221 //<----------end
```
当你完成__handle_page_fault()函数后,可进行如下测试:
编译app目录下实验四相关文件
`$ riscv64-unknown-elf-gcc app/app4_1.c -o app/elf/app4_1`
`$ riscv64-unknown-elf-gcc app/app4_2.c -o app/elf/app4_2`
使用spike运行预期输出如下
```
spike obj/pke app/elf/app4_1
PKE IS RUNNING
page fault vaddr:0x0000000000011000
page fault vaddr:0x000000007f7ecc00
page fault vaddr:0x00000000000100c0
page fault vaddr:0x000000007f000000
page fault vaddr:0x000000007f001000
```
`$ spike obj/pke app/elf/app4_1`
//递归程序可正常运行
如果你的两个测试app都可以正确输出的话那么运行检查的python脚本
`$ ./pke-lab4`
若得到如下输出,那么恭喜你,你已经成功完成了实验四!!!
```
build pk : OK
running app4_1 : OK
test4_1 : OK
running app4_2 : OK
test4_2 : OK
```
### 5.2 基础知识
**5.2.1 虚拟地址空间**
物理地址唯一,且有限的,但现在的操作系统上往往有不止一个的程序在运行。如果只有物理地址,那对于程序员来说无疑是相当繁琐的。程序不知道那些内存可用,那些内存已经被其他程序占有,这就意味着操作系统必须管理所有的物理地址,并且所有所有代码都是共用的。
为了解决上述问题,操作系统引入了虚拟地址的概念。每个进程拥有着独立的虚拟地址空间,这个空间是连续的,一个虚拟页可以映射到不同或者相同的物理页。这就是我们所说的虚拟地址。在程序中,我们所使用的变量的地址均为虚拟地址。
**5.2.2 虚拟地址同物理地址之间的转换**
虚拟地址只是一个逻辑上的概念在计算机中最后正真被访问的地址仍是物理地址。所以我们需要在一个虚拟地址访问内存之前将它翻译成物理地址这个过程称为地址翻译。CPU上的内存管理单元MMU会利用存放在主存的页表完成这一过程。
RISCV的S模式下提供了基于页面的虚拟内存管理机制内存被划分为固定大小的页。我们使用物理地址对这些页进行描述我们在此回顾上一章所讲到的RISCV物理地址的定义
<img src="pictures/fig5_1.png" alt="fig5_1" style="zoom:80%;" />
图5.1 RISCV64 物理地址
可以看到物理地址由PPN物理页号与Offset偏移量组成。这里的PPN就对应着上述的物理页。
现在我们来看RISCV虚拟地址的定义
<img src="pictures/fig5_2.png" alt="fig5_2" style="zoom:80%;" />
图5.2 RISCV64 虚拟地址
可以看到虚拟地址同样由页号和偏移量组成。而这二者之间是如何转换的呢RV64支持多种分页方案如Sv32、Sv39、Sv48它们的原理相似这里我们对pke中所使用的Sv39进行讲述。Sv39中维护着一个三级的页表其页表项定义如下
<img src="pictures/fig1_7.png" alt="fig1_7" style="zoom:80%;" />
图5.3 Sv39页表项
当启动分页后MMU会对每个虚拟地址进行页表查询页表的地址由satp寄存器保存。在"pk/mmap.c"中的pk_vm_init函数中有如下一行代码其中sptbr即为satp的曾用名在这行代码中我们将页表地址写入satp寄存器。
```
458 write_csr(sptbr, ((uintptr_t)root_page_table >> RISCV_PGSHIFT) | SATP_MODE_CHOICE);
```
<img src="pictures/fig5_4.png" alt="fig5_4" style="zoom:80%;" />
图5.4 地址转换
于是当需要进行页表转换时我们变从satp所存储的页表地址开始逐级的转换。
在pke中位于"pk/mmap.c"中的转换代码如下:
```
112 static size_t pt_idx(uintptr_t addr, int level)
113 {
114 size_t idx = addr >> (RISCV_PGLEVEL_BITS*level + RISCV_PGSHIFT);
115 return idx & ((1 << RISCV_PGLEVEL_BITS) - 1);
116 }
```
首先我们来看pt_idx函数函数中将虚拟地址addr右移RISCV_PGLEVEL_BITS*level + RISCV_PGSHIFT位其中RISCV_PGSHIFT对应着VPN中的Offset而level则对应着各级VPNpt_idx通过level取出指定的VPN。当level = 2, 得到vpn[2]即页目录项在一级页表的序号当level = 1, 得到vpn[1]即页目录项在二级页表的序号同理当level = 0, 则得到vpn[0],即页表项在三级页表的序号。
```
125 static pte_t* __walk_internal(uintptr_t addr, int create)
126 {
127 pte_t* t = root_page_table;
128 for (int i = (VA_BITS - RISCV_PGSHIFT) / RISCV_PGLEVEL_BITS - 1; i > 0; i--) {
129 size_t idx = pt_idx(addr, i);
130 if (unlikely(!(t[idx] & PTE_V)))
131 return create ? __continue_walk_create(addr, &t[idx]) : 0;
132 t = (pte_t*)(pte_ppn(t[idx]) << RISCV_PGSHIFT);
133 }
134 return &t[pt_idx(addr, 0)];
135 }
```
接着我们进一步分析__walk_internal函数首先VA_BITS即虚拟地址的位数为39RISCV_PGSHIFT即代表虚拟地址中Offset的位数二者相减剩下的就是VPN0、VPN1……VPNX的位数在除以VPN的位数得到就是VPN的数量。由于pke中式Sv39故而VPN的数量为3即VPN0、VPN1、VPN2。
接着我们使用pt_idx函数得到各级VPN的值依据图5.2所示逐级查询,一直找到该虚拟地址对应的页表项,而该页表项中存着该虚拟地址所对应的物理页号,再加上虚拟地址中的偏离量,我们就可以找到最终的物理地址了!!
**5.2.3** **缺页异常处理**
```
1 #include<stdio.h>
2
3 int main()
4 {
5
6 uintptr_t addr = 0x7f000000;
7 *(int *)(addr)=1;
8
9 uintptr_t addr1_same_page = 0x7f000010;
10 uintptr_t addr2_same_page = 0x7f000fff;
11 *(int *)(addr1_same_page)=2;
12 *(int *)(addr2_same_page)=3;
13
14 uintptr_t addr1_another_page = 0x7f001000;
15 uintptr_t addr2_another_page = 0x7f001ff0;
16 *(int *)(addr1_another_page)=4;
17 *(int *)(addr2_another_page)=5;
18
19
20 return 0;
21 }
```
以上程序中我们人为的访问虚拟地址0x7f000000与虚拟地址0x7f001000所对应的物理页由于操作系统并没有事先加载这些页面于是会出发缺页中断异常。进入pk/mmap.c文件下的__handle_page_fault函数中其代码如下
```
203 static int __handle_page_fault(uintptr_t vaddr, int prot)
204 {
205 printk("page fault vaddr:0x%lx\n", vaddr);
206 //your code here
207 //start------------>
208 pte_t* pte =0;
209
210 //<-----------end
211 if (pte == 0 || *pte == 0 || !__valid_user_range(vaddr, 1))
212 return -1;
213 else if (!(*pte & PTE_V))
214 {
215
216 //your code here
217 //start--------->
218
219 uintptr_t ppn =0;
220 vmr_t* v = NULL;
221
222 //<----------end
223
224 if (v->file)
225 {
226 size_t flen = MIN(RISCV_PGSIZE, v->length - (vaddr - v->addr));
227 // ssize_t ret = file_pread(v->file, (void*)vaddr, flen, vaddr - v->addr + v->offset);
228 ssize_t ret = file_pread_pnn(v->file, (void*)vaddr, flen, ppn, vaddr - v->addr + v->offset);
229 kassert(ret > 0);
230 if (ret < RISCV_PGSIZE)
231 memset((void*)vaddr + ret, 0, RISCV_PGSIZE - ret);
232 }
233 else
234 memset((void*)vaddr, 0, RISCV_PGSIZE);
235 __vmr_decref(v, 1);
236 *pte = pte_create(ppn, prot_to_type(v->prot, 1));
237 }
238
239 pte_t perms = pte_create(0, prot_to_type(prot, 1));
240 if ((*pte & perms) != perms)
241 return -1;
242
243 flush_tlb();
244 return 0;
245 }
```
对于一个没有对应物理地址的虚拟地址我们需要进行如下的处理。首先找到该物理地址所对应的pte这里你可能会使用到__walk函数__walk中调用了上文中我们讨论过的__walk_internal函数对于一个给定的虚拟地址返回其对于的pte其定义如下
```
138 pte_t* __walk(uintptr_t addr)
139 {
140 return __walk_internal(addr, 0);
141 }
```
其次使用操作系统为该虚拟地址分配一个相对应的物理页还记得物理内存管理中的内存分配嘛现在它有用武之地了最后将该物理地址写入第一步的得到的pte中这里你会用到page2ppn和pte_create函数。
以上,就是本次实验需要大家完成的部分了!