adjust the formats of the labs

pull/1/head
Zhiyuan Shao 4 years ago
parent 1ed8cc61b8
commit 7908a91c8b

@ -1,399 +1,430 @@
## 第三章实验2系统调用的实现 # 第三章实验2系统调用的实现
### 3.1 实验环境搭建 ## 3.1 实验环境搭建
实验2需要在实验1的基础之上完成所以首先你需要切换到lab2_small的branch然后commit你的代码。 实验2需要在实验1的基础之上完成所以首先你需要切换到lab2_small的branch然后commit你的代码。
首先查看本地拥有的branch输入如下命令 首先查看本地拥有的branch输入如下命令
`$ git branch` `$ git branch`
如果有如下输出: 如果有如下输出:
``` ```
lab1_small lab1_small
lab2_small lab2_small
lab3_small lab3_small
lab4_small lab4_small
lab5_small lab5_small
``` ```
则你本地就有实验二的branch那么可以直接切换分支到lab2输入如下命令 则你本地就有实验二的branch那么可以直接切换分支到lab2输入如下命令
`$ git checkout lab2_small` `$ git checkout lab2_small`
当然如果本地没有lab2的相关代码也可以直接从远端拉取 当然如果本地没有lab2的相关代码也可以直接从远端拉取
`$ git checkout -b lab2_small origin/ lab2_small` `$ git checkout -b lab2_small origin/ lab2_small`
然后merge实验一中你的代码 然后merge实验一中你的代码
`$ git merge -m "merge lab1" lab1_small` `$ git merge -m "merge lab1" lab1_small`
完成一切后,我们就可以正式进入实验二了! 完成一切后,我们就可以正式进入实验二了!
### 3.2 实验内容 ## 3.2 实验内容
实验要求:了解系统调用的执行过程,并实现一个自定义的系统调用。
#### 应用: ####
**3.2.1 练习一在app中使用系统调用**
app2_1.c源文件如下
系统调用的英文名字是System Call用户通过系统调用来执行一些需要特权的任务那么我们具体在app中是如何使用内核所提供的系统调用的呢
1 #define ecall() ({\
RISC-V中提供了ecall指令用于向运行时环境发出请求我们可以使用内联汇编使用该指令进行系统调用代码如下 2 asm volatile(\
3 "li x17,81\n"\
``` 4 "ecall");\
1 #define ecall() ({\ 5 })
2 asm volatile(\ 6
3 "li x17,81\n"\ 7 int main(void){
4 "ecall");\ 8 //调用自定义的81号系统调用
5 }) 9 ecall();
6 10 return 0;
7 int main(void){ 11 }
8 //调用自定义的81号系统调用
9 ecall();
10 return 0;
11 }
``` 以上代码中我们将系统调用号81通过x17寄存器传给内核再通过ecall指令进行系统调用当然目前代理内核中并没有81号系统调用的实现这需要我们在后面的实验中完成。
例3.1 ecall
以上代码中我们将系统调用号81通过x17寄存器传给内核再通过ecall指令进行系统调用当然目前代理内核中并没有81号系统调用的实现这需要我们在后面的实验中完成。
**3.2.2 练习二:系统调用过程跟踪** #### 任务一 : 在app中使用系统调用理解 ####
在我们执行了ecall指令后代理内核中又是如何执行的呢 任务描述:
在第一章的表1.7中我们可以看到Environment call from U-mode是exception(trap)的一种其对应的code是8。我们在实验二中已经讨论过中断入口函数位置的设置现在继续跟踪中断入口函数找出系统调用的执行过程。 系统调用的英文名字是System Call用户通过系统调用来执行一些需要特权的任务那么我们具体在app中是如何使用内核所提供的系统调用的呢
**3.2.3 练习三:自定义系统调用(需要编程)** RISC-V中提供了ecall指令用于向运行时环境发出请求我们可以使用内联汇编使用该指令进行系统调用。
阅读pk目录syscall.c文件增加一个系统调用sys_get_memsize()系统调用返回spike设置的内存空间大小, 系统调用号为81。
预期输出:
提示在pk目录下的mmap.c文件中函数pk_vm_init中定义了代理内核的内存空间大小。
理解app2_1的调用过程。
spike 通过-m来指定分配的物理内存单位是MiB默认为2048。如
`$ spike obj/pke app/elf/app2_1`
#### 任务二 : 系统调用过程跟踪(理解) ####
得到的输出如下:
任务描述:
```
PKE IS RUNNING
physical mem_size = 0x80000000 在我们执行了ecall指令后代理内核中又是如何执行的呢
```
在第一章的表1.7中我们可以看到Environment call from U-mode是exception(trap)的一种其对应的code是8。
可以看到默认情况下spike的物理内存是2GB
你可以修改-m选项运行app3观察输出。 预期输出:
`$ spike -m1024 obj/pke app/elf/app2_1` 我们在实验二中已经讨论过中断入口函数位置的设置,现在继续跟踪中断入口函数,找出系统调用的执行过程。
预计得到输出格式:
#### 任务三 : 自定义系统调用(编程) ####
```
PKE IS RUNNING 任务描述:
physical mem_size = 0x40000000
```
阅读pk目录syscall.c文件增加一个系统调用sys_get_memsize()系统调用返回spike设置的内存空间大小, 系统调用号为81。
如果你的app可以正确输出的话那么运行检查的python脚本
`$ ./pke-lab2` 预期输出:
若得到如下输出,那么恭喜你,你已经成功完成了实验二!!!
在pk目录下的mmap.c文件中函数pk_vm_init中定义了代理内核的内存空间大小。
```
build pk : OK spike 通过-m来指定分配的物理内存单位是MiB默认为2048。如
running app3 m2048 : OK
test3_m2048 : OK `$ spike obj/pke app/elf/app2_1`
running app3 m1024 : OK
test3_m1024 : OK 得到的输出如下:
Score: 20/20
``` ```
PKE IS RUNNING
### 3.3 基础知识 physical mem_size = 0x80000000
```
**3.3.1 系统调用**
可以看到默认情况下spike的物理内存是2GB
​首先,我们要知道什么是系统调用。
你可以修改-m选项运行app3观察输出。
例如读文件(read)、写文件(write)等,其实我们已经接触过形形色色的系统调用。系统调用和函数调用的外形相似,但他们有着本质的不同。
`$ spike -m1024 obj/pke app/elf/app2_1`
系统调用的英文名字是System Call。由于用户进程只能在操作系统给它圈定好的“用户环境”中执行但“用户环境”限制了用户进程能够执行的指令即用户进程只能执行一般的指令无法执行特权指令。如果用户进程想执行一些需要特权指令的任务比如通过网卡发网络包等只能让操作系统来代劳了。系统调用就是用户模式下请求操作系统执行某些特权指令的任务的机制。
预计得到输出格式:
相较于函数调用在普通的用户模式下运行,系统调用则运行在内核模式中。
```
见下图: PKE IS RUNNING
physical mem_size = 0x40000000
<img src="pictures/fig3_1.png" alt="fig3_1" style="zoom:80%;" /> ```
图3.1系统调用过程 如果你的app可以正确输出的话那么运行检查的python脚本
系统调用属于一种中断,当用户申请系统调用时,系统会从用户态陷入到内核态,完成相应的服务后,再回到原来的用户态上下文中。 `$ ./pke-lab2`
**3.3.2 代理内核与应用程序的加载** 若得到如下输出,那么恭喜你,你已经成功完成了实验二!!!
阅读pke.lds文件可以看到整个PK程序的入口为reset_vector函数 ```
build pk : OK
``` running app3 m2048 : OK
3 OUTPUT_ARCH( "riscv" ) test3_m2048 : OK
4 running app3 m1024 : OK
5 ENTRY( reset_vector ) test3_m1024 : OK
``` Score: 20/20
```
我们在machine/mentry.S中找的这个符号。
## 3.3 实验指导
```
36 reset_vector: **3.3.1 系统调用**
37 j do_reset
``` ​首先,我们要知道什么是系统调用。
首先初始化x0~x31共32个寄存器其中x10a0寄存器与x11a1寄存器存储着从之前boot loader中传来的参数而不复位。 例如读文件(read)、写文件(write)等,其实我们已经接触过形形色色的系统调用。系统调用和函数调用的外形相似,但他们有着本质的不同。
``` 系统调用的英文名字是System Call。由于用户进程只能在操作系统给它圈定好的“用户环境”中执行但“用户环境”限制了用户进程能够执行的指令即用户进程只能执行一般的指令无法执行特权指令。如果用户进程想执行一些需要特权指令的任务比如通过网卡发网络包等只能让操作系统来代劳了。系统调用就是用户模式下请求操作系统执行某些特权指令的任务的机制。
223 do_reset:
224 li x1, 0 相较于函数调用在普通的用户模式下运行,系统调用则运行在内核模式中。
.....
255 li x31, 0 见下图:
```
<img src="pictures/fig3_1.png" alt="fig3_1" style="zoom:80%;" />
将mscratch寄存器置0
图3.1系统调用过程
```
256 csrw mscratch, x0 系统调用属于一种中断,当用户申请系统调用时,系统会从用户态陷入到内核态,完成相应的服务后,再回到原来的用户态上下文中。
```
**3.3.2 代理内核与应用程序的加载**
将trap_vector的地址写入t0寄存器trap_vector是mechine模式下异常处理的入口地址。再将t0的值写入mtvec寄存器中。然后读取mtvec寄存器中的地址到t1寄存器。比较t0于t1。
阅读pke.lds文件可以看到整个PK程序的入口为reset_vector函数
```
259 la t0, trap_vector ```
260 mtvec, t0 3 OUTPUT_ARCH( "riscv" )
261 rr t1, mtvec 4
262 1:bne t0, t1, 1b 5 ENTRY( reset_vector )
``` ```
正常情况下t1自然是的等于t0的于是程序顺序执行将栈地址写入sp寄存器中 我们在machine/mentry.S中找的这个符号。
``` ```
264 la sp, stacks + RISCV_PGSIZE - MENTRY_FRAME_SIZE 36 reset_vector:
``` 37 j do_reset
```
读取mhartid到a3寄存器调整sp
首先初始化x0~x31共32个寄存器其中x10a0寄存器与x11a1寄存器存储着从之前boot loader中传来的参数而不复位。
```
266 csrr a3, mhartid ```
267 slli a2, a3, RISCV_PGSHIFT 223 do_reset:
268 add sp, sp, a2 224 li x1, 0
``` .....
255 li x31, 0
当a3不等于0时跳转到 init_first_hart ```
``` 将mscratch寄存器置0
270 # Boot on the first hart
271 beqz a3, init_first_hart ```
``` 256 csrw mscratch, x0
```
此时进入"machine/minit.c"文件在init_first_hart中对外设进行初始化
将trap_vector的地址写入t0寄存器trap_vector是mechine模式下异常处理的入口地址。再将t0的值写入mtvec寄存器中。然后读取mtvec寄存器中的地址到t1寄存器。比较t0于t1。
```
154 void init_first_hart(uintptr_t hartid, uintptr_t dtb) ```
155 { 259 la t0, trap_vector
…… //初始化外设 260 mtvec, t0
180 boot_loader(dtb); 261 rr t1, mtvec
181 } 262 1:bne t0, t1, 1b
``` ```
在init_first_hart的最后一行调用boot_loader函数 正常情况下t1自然是的等于t0的于是程序顺序执行将栈地址写入sp寄存器中
``` ```
160 void boot_loader(uintptr_t dtb) 264 la sp, stacks + RISCV_PGSIZE - MENTRY_FRAME_SIZE
161 { ```
……. //CSR寄存器设置
169 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0); 读取mhartid到a3寄存器调整sp
170 }
``` ```
266 csrr a3, mhartid
在boot_loader中经历设置中断入口地址清零sscratch寄存器关中断等一系列操作后。最后会调用enter_supervisor_mode函数正式切换至Supervisor模式。 267 slli a2, a3, RISCV_PGSHIFT
268 add sp, sp, a2
``` ```
204 void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1)
205 { 当a3不等于0时跳转到 init_first_hart
206 uintptr_t mstatus = read_csr(mstatus);
207 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, PRV_S); ```
208 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0); 270 # Boot on the first hart
209 write_csr(mstatus, mstatus); 271 beqz a3, init_first_hart
210 write_csr(mscratch, MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE); ```
211 #ifndef __riscv_flen
212 uintptr_t *p_fcsr = MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE; // the x0's save slot 此时进入"machine/minit.c"文件在init_first_hart中对外设进行初始化
213 *p_fcsr = 0;
214 #endif ```
215 write_csr(mepc, fn); 154 void init_first_hart(uintptr_t hartid, uintptr_t dtb)
216 155 {
217 register uintptr_t a0 asm ("a0") = arg0; …… //初始化外设
218 register uintptr_t a1 asm ("a1") = arg1; 180 boot_loader(dtb);
219 asm volatile ("mret" : : "r" (a0), "r" (a1)); 181 }
220 __builtin_unreachable(); ```
221 }
``` 在init_first_hart的最后一行调用boot_loader函数
在enter_supervisor_mode函数中将 mstatus的MPP域设置为1表示中断发生之前的模式是Supervisor将mstatus的MPIE域设置为0表示中断发生前MIE的值为0。随即将机器模式的内核栈顶写入mscratch寄存器中设置mepc为rest_of_boot_loader的地址并将kernel_stack_top与0作为参数存入a0和a1。 ```
160 void boot_loader(uintptr_t dtb)
最后执行mret指令该指令执行时程序从机器模式的异常返回将程序计数器pc设置为mepc即rest_of_boot_loader的地址将特权级设置为mstatus寄存器的MPP域即方才所设置的代表Supervisor的1MPP设置为0将mstatus寄存器的MIE域设置为MPIE即方才所设置的表示中断关闭的0MPIE设置为1。 161 {
……. //CSR寄存器设置
于是当mret指令执行完毕程序将从rest_of_boot_loader继续执行。 169 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0);
170 }
``` ```
144 static void rest_of_boot_loader(uintptr_t kstack_top)
145 { 在boot_loader中经历设置中断入口地址清零sscratch寄存器关中断等一系列操作后。最后会调用enter_supervisor_mode函数正式切换至Supervisor模式。
146 arg_buf args;
147 size_t argc = parse_args(&args); ```
148 if (!argc) 204 void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1)
149 panic("tell me what ELF to load!"); 205 {
150 206 uintptr_t mstatus = read_csr(mstatus);
151 // load program named by argv[0] 207 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, PRV_S);
152 long phdrs[128]; 208 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0);
153 current.phdr = (uintptr_t)phdrs; 209 write_csr(mstatus, mstatus);
154 current.phdr_size = sizeof(phdrs); 210 write_csr(mscratch, MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE);
155 load_elf(args.argv[0], &current); 211 #ifndef __riscv_flen
156 212 uintptr_t *p_fcsr = MACHINE_STACK_TOP() - MENTRY_FRAME_SIZE; // the x0's save slot
157 run_loaded_program(argc, args.argv, kstack_top); 213 *p_fcsr = 0;
158 } 214 #endif
``` 215 write_csr(mepc, fn);
216
这个函数中我们对应用程序的ELF文件进行解析并且最终运行应用程序。 217 register uintptr_t a0 asm ("a0") = arg0;
218 register uintptr_t a1 asm ("a1") = arg1;
**3.3.3 中断的处理过程** 219 asm volatile ("mret" : : "r" (a0), "r" (a1));
220 __builtin_unreachable();
考虑一下当程序执行到中断之前程序是有自己的运行状态的例如寄存器里保持的上下文数据。当中断发生硬件在自动设置完中断原因和中断地址后就会调转到中断处理程序而中断处理程序同样会使用寄存器于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器我们称之为callee-saved寄存器。 221 }
```
在PK的machine/minit.c中间中便通过delegate_traps()将部分中断及同步异常委托给S模式。同学们可以查看具体是哪些中断及同步异常
在enter_supervisor_mode函数中将 mstatus的MPP域设置为1表示中断发生之前的模式是Superior将mstatus的MPIE域设置为0表示中段发生前MIE的值为0。随机将机器模式的内核栈顶写入mscratch寄存器中设置mepc为rest_of_boot_loader的地址并将kernel_stack_top与0作为参数存入a0和a1。
```
43 // send S-mode interrupts and most exceptions straight to S-mode 最后执行mret指令该指令执行时程序从机器模式的异常返回将程序计数器pc设置为mepc即rest_of_boot_loader的地址将特权级设置为mstatus寄存器的MPP域即方才所设置的代表Superior的1MPP设置为0将mstatus寄存器的MIE域设置为MPIE即方才所设置的表示中断关闭的0MPIE设置为1。
44 static void delegate_traps()
45 { 于是当mret指令执行完毕程序将从rest_of_boot_loader继续执行。
46 if (!supports_extension('S'))
47 return; ```
48 144 static void rest_of_boot_loader(uintptr_t kstack_top)
49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; 145 {
50 uintptr_t exceptions = 146 arg_buf args;
51 (1U << CAUSE_MISALIGNED_FETCH) | 147 size_t argc = parse_args(&args);
52 (1U << CAUSE_FETCH_PAGE_FAULT) | 148 if (!argc)
53 (1U << CAUSE_BREAKPOINT) | 149 panic("tell me what ELF to load!");
54 (1U << CAUSE_LOAD_PAGE_FAULT) | 150
55 (1U << CAUSE_STORE_PAGE_FAULT) | 151 // load program named by argv[0]
56 (1U << CAUSE_USER_ECALL); 152 long phdrs[128];
57 153 current.phdr = (uintptr_t)phdrs;
58 write_csr(mideleg, interrupts); 154 current.phdr_size = sizeof(phdrs);
59 write_csr(medeleg, exceptions); 155 load_elf(args.argv[0], &current);
60 assert(read_csr(mideleg) == interrupts); 156
61 assert(read_csr(medeleg) == exceptions); 157 run_loaded_program(argc, args.argv, kstack_top);
62 } 158 }
``` ```
这里介绍一下RISCV的中断委托机制在默认的情况下所有的异常都会被交由机器模式处理。但正如我们知道的那样大部分的系统调用都是在S模式下处理的因此RISCV提供了这一委托机制可以选择性的将中断交由S模式处理从而完全绕过M模式。 这个函数中我们对应用程序的ELF文件进行解析并且最终运行应用程序。
接下我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中stvec保存着发生异常时处理器需要跳转到的地址也就是说当中断发生我们将跳转至trap_entry现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中其代码如下 **3.3.3 中断的处理过程**
``` 考虑一下当程序执行到中断之前程序是有自己的运行状态的例如寄存器里保持的上下文数据。当中断发生硬件在自动设置完中断原因和中断地址后就会调转到中断处理程序而中断处理程序同样会使用寄存器于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器我们称之为callee-saved寄存器。
60 trap_entry:
61 csrrw sp, sscratch, sp 在PK的machine/minit.c中间中便通过delegate_traps()将部分中断及同步异常委托给S模式。同学们可以查看具体是哪些中断及同步异常
62 bnez sp, 1f
63 csrr sp, sscratch ```
64 1:addi sp,sp,-320 43 // send S-mode interrupts and most exceptions straight to S-mode
65 save_tf 44 static void delegate_traps()
66 move a0,sp 45 {
67 jal handle_trap 46 if (!supports_extension('S'))
``` 47 return;
48
在61行交换了sp与sscratch的值这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。 49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
50 uintptr_t exceptions =
如果sp也就是传入的sscratch值不为零则跳转至64行若sscratch的值为零则恢复原sp中的值。这是因为当中断来源于S模式时sscratch的值为0sp中存储的就是内核的堆栈地址。而当中断来源于U模式时sp中存储的是用户的堆栈地址sscratch中存储的则是内核的堆栈地址需要交换二者是sp指向内核的堆栈地址。 51 (1U << CAUSE_MISALIGNED_FETCH) |
52 (1U << CAUSE_FETCH_PAGE_FAULT) |
接着在64,65行保存上下文最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中代码如下 53 (1U << CAUSE_BREAKPOINT) |
54 (1U << CAUSE_LOAD_PAGE_FAULT) |
``` 55 (1U << CAUSE_STORE_PAGE_FAULT) |
112 void handle_trap(trapframe_t* tf) 56 (1U << CAUSE_USER_ECALL);
113 { 57
114 if ((intptr_t)tf->cause < 0) 58 write_csr(mideleg, interrupts);
115 return handle_interrupt(tf); 59 write_csr(medeleg, exceptions);
116 60 assert(read_csr(mideleg) == interrupts);
117 typedef void (*trap_handler)(trapframe_t*); 61 assert(read_csr(medeleg) == exceptions);
118 62 }
119 const static trap_handler trap_handlers[] = { ```
120 [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch,
121 [CAUSE_FETCH_ACCESS] = handle_instruction_access_fault, 这里介绍一下RISCV的中断委托机制在默认的情况下所有的异常都会被交由机器模式处理。但正如我们知道的那样大部分的系统调用都是在S模式下处理的因此RISCV提供了这一委托机制可以选择性的将中断交由S模式处理从而完全绕过M模式。
122 [CAUSE_LOAD_ACCESS] = handle_load_access_fault,
123 [CAUSE_STORE_ACCESS] = handle_store_access_fault, 接下我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中stvec保存着发生异常时处理器需要跳转到的地址也就是说当中断发生我们将跳转至trap_entry现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中其代码如下
124 [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch,
125 [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction, ```
126 [CAUSE_USER_ECALL] = handle_syscall, 60 trap_entry:
127 [CAUSE_BREAKPOINT] = handle_breakpoint, 61 csrrw sp, sscratch, sp
128 [CAUSE_MISALIGNED_LOAD] = handle_misaligned_load, 62 bnez sp, 1f
129 [CAUSE_MISALIGNED_STORE] = handle_misaligned_store, 63 csrr sp, sscratch
130 [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load, 64 1:addi sp,sp,-320
131 [CAUSE_STORE_PAGE_FAULT] = handle_fault_store, 65 save_tf
132 }; 66 move a0,sp
``` 67 jal handle_trap
```
handle_trap函数中实现了S模式下各类中断的处理。可以看到代码的126行就对应着系统调用的处理handle_syscall的实现如下
在61行交换了sp与sscratch的值这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。
```
100 static void handle_syscall(trapframe_t* tf) 如果sp也就是传入的sscratch值不为零则跳转至64行若sscratch的值为零则恢复原sp中的值。这是因为当中断来源于S模式是sscratch的值为0sp中存储的就是内核的堆栈地址。而当中断来源于U模式时sp中存储的是用户的堆栈地址sscratch中存储的则是内核的堆栈地址需要交换二者是sp指向内核的堆栈地址。
101 {
102 tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13], 接着在64,65行保存上下文最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中代码如下
103 tf->gpr[14], tf->gpr[15], tf->gpr[17]);
104 tf->epc += 4; ```
105 } 112 void handle_trap(trapframe_t* tf)
``` 113 {
114 if ((intptr_t)tf->cause < 0)
还记得我们在例3.1中是将中断号写入x17寄存器嘛其对应的就是这里do_syscall的最后一个参数我们跟踪进入do_syscall函数其代码如下 115 return handle_interrupt(tf);
116
``` 117 typedef void (*trap_handler)(trapframe_t*);
313 long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n) 118
314 { 119 const static trap_handler trap_handlers[] = {
315 const static void* syscall_table[] = { 120 [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch,
316 // your code here: 121 [CAUSE_FETCH_ACCESS] = handle_instruction_access_fault,
317 // add get_init_memsize syscall 122 [CAUSE_LOAD_ACCESS] = handle_load_access_fault,
318 [SYS_init_memsize ] = sys_get_init_memsize, 123 [CAUSE_STORE_ACCESS] = handle_store_access_fault,
319 [SYS_exit] = sys_exit, 124 [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch,
320 [SYS_exit_group] = sys_exit, 125 [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction,
321 [SYS_read] = sys_read, 126 [CAUSE_USER_ECALL] = handle_syscall,
322 [SYS_pread] = sys_pread, 127 [CAUSE_BREAKPOINT] = handle_breakpoint,
323 [SYS_write] = sys_write, 128 [CAUSE_MISALIGNED_LOAD] = handle_misaligned_load,
324 [SYS_openat] = sys_openat, 129 [CAUSE_MISALIGNED_STORE] = handle_misaligned_store,
325 [SYS_close] = sys_close, 130 [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load,
326 [SYS_fstat] = sys_fstat, 131 [CAUSE_STORE_PAGE_FAULT] = handle_fault_store,
327 [SYS_lseek] = sys_lseek, 132 };
328 [SYS_renameat] = sys_renameat, ```
329 [SYS_mkdirat] = sys_mkdirat,
330 [SYS_getcwd] = sys_getcwd, handle_trap函数中实现了S模式下各类中断的处理。可以看到代码的126行就对应着系统调用的处理handle_syscall的实现如下
331 [SYS_brk] = sys_brk,
332 [SYS_uname] = sys_uname, ```
333 [SYS_prlimit64] = sys_stub_nosys, 100 static void handle_syscall(trapframe_t* tf)
334 [SYS_rt_sigaction] = sys_rt_sigaction, 101 {
335 [SYS_times] = sys_times, 102 tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13],
336 [SYS_writev] = sys_writev, 103 tf->gpr[14], tf->gpr[15], tf->gpr[17]);
337 [SYS_readlinkat] = sys_stub_nosys, 104 tf->epc += 4;
338 [SYS_rt_sigprocmask] = sys_stub_success, 105 }
339 [SYS_ioctl] = sys_stub_nosys, ```
340 [SYS_getrusage] = sys_stub_nosys,
341 [SYS_getrlimit] = sys_stub_nosys, 还记得我们在例3.1中是将中断号写入x17寄存器嘛其对应的就是这里do_syscall的最后一个参数我们跟踪进入do_syscall函数其代码如下
342 [SYS_setrlimit] = sys_stub_nosys,
343 [SYS_set_tid_address] = sys_stub_nosys, ```
344 [SYS_set_robust_list] = sys_stub_nosys, 313 long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n)
345 }; 314 {
346 315 const static void* syscall_table[] = {
347 syscall_t f = 0; 316 // your code here:
348 317 // add get_init_memsize syscall
349 if (n < ARRAY_SIZE(syscall_table)) 318 [SYS_init_memsize ] = sys_get_init_memsize,
350 f = syscall_table[n]; 319 [SYS_exit] = sys_exit,
351 if (!f) 320 [SYS_exit_group] = sys_exit,
352 panic("bad syscall #%ld!",n); 321 [SYS_read] = sys_read,
353 322 [SYS_pread] = sys_pread,
354 return f(a0, a1, a2, a3, a4, a5, n); 323 [SYS_write] = sys_write,
355 } 324 [SYS_openat] = sys_openat,
``` 325 [SYS_close] = sys_close,
326 [SYS_fstat] = sys_fstat,
do_syscall中通过传入的系统调用号n查询syscall_table得到对应的函数并最终执行系统调用。 327 [SYS_lseek] = sys_lseek,
328 [SYS_renameat] = sys_renameat,
329 [SYS_mkdirat] = sys_mkdirat,
330 [SYS_getcwd] = sys_getcwd,
331 [SYS_brk] = sys_brk,
332 [SYS_uname] = sys_uname,
333 [SYS_prlimit64] = sys_stub_nosys,
334 [SYS_rt_sigaction] = sys_rt_sigaction,
335 [SYS_times] = sys_times,
336 [SYS_writev] = sys_writev,
337 [SYS_readlinkat] = sys_stub_nosys,
338 [SYS_rt_sigprocmask] = sys_stub_success,
339 [SYS_ioctl] = sys_stub_nosys,
340 [SYS_getrusage] = sys_stub_nosys,
341 [SYS_getrlimit] = sys_stub_nosys,
342 [SYS_setrlimit] = sys_stub_nosys,
343 [SYS_set_tid_address] = sys_stub_nosys,
344 [SYS_set_robust_list] = sys_stub_nosys,
345 };
346
347 syscall_t f = 0;
348
349 if (n < ARRAY_SIZE(syscall_table))
350 f = syscall_table[n];
351 if (!f)
352 panic("bad syscall #%ld!",n);
353
354 return f(a0, a1, a2, a3, a4, a5, n);
355 }
```
do_syscall中通过传入的系统调用号n查询syscall_table得到对应的函数并最终执行系统调用。

@ -1,323 +1,405 @@
## 第四章实验3物理内存管理 # 第四章实验3物理内存管理
### 4.1 实验内容 ## 4.1 实验内容
实验要求:了解物理内存,管理物理内存。
**4.1.1 练习一OS内存的初始化过程** #### 应用: ####
在"pk/mmap.c"内有 pk_vm_init()函数阅读该函数了解OS内存初始化的过程。
app3_1.c源文件如下
```
364 uintptr_t pk_vm_init() 1 #define ecall() ({\
365 { 2 asm volatile(\
366 // HTIF address signedness and va2pa macro both cap memory size to 2 GiB 3 "li x17,81\n"\
//设置物理内存大小 4 "ecall");\
367 mem_size = MIN(mem_size, 1U << 31); 5 })
//计算物理页的数量 6
368 size_t mem_pages = mem_size >> RISCV_PGSHIFT; 7 int main(void){
369 free_pages = MAX(8, mem_pages >> (RISCV_PGLEVEL_BITS-1)); 8 //调用自定义的81号系统调用
370 9 ecall();
//_end为内核结束地址 10 return 0;
371 extern char _end; 11 }
372 first_free_page = ROUNDUP((uintptr_t)&_end, RISCV_PGSIZE);
373 first_free_paddr = first_free_page + free_pages * RISCV_PGSIZE; 对于操作系统来说内存分配的过程需要对应用层透明故而实验三的app同实验二相同并在内核中对于的内存分配单元做如下校验
374
//映射内核的物理空间
375 root_page_table = (void*)__page_alloc(); static void
376 __map_kernel_range(DRAM_BASE, DRAM_BASE, first_free_paddr - DRAM_BASE, PROT_READ|PROT_WRITE|PROT_EXEC); basic_check(void) {
377 struct Page *p0, *p1, *p2;
//crrent.mmap_max: 0x000000007f7ea000 p0 = p1 = p2 = NULL;
378 current.mmap_max = current.brk_max = assert((p0 = alloc_page()) != NULL);
379 MIN(DRAM_BASE, mem_size - (first_free_paddr - DRAM_BASE)); assert((p1 = alloc_page()) != NULL);
380 assert((p2 = alloc_page()) != NULL);
//映射用户栈
381 size_t stack_size = MIN(mem_pages >> 5, 2048) * RISCV_PGSIZE; assert(p0 != p1 && p0 != p2 && p1 != p2);
382 size_t stack_bottom = __do_mmap(current.mmap_max - stack_size, stack_size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED , 0, 0); assert(p0->ref == 0 && p1->ref == 0 && p2->ref == 0);
383 kassert(stack_bottom != (uintptr_t)-1);
384 current.stack_top = stack_bottom + stack_size;
385 list_entry_t free_list_store = free_list;
//开启分页 list_init(&free_list);
386 flush_tlb(); assert(list_empty(&free_list));
387 write_csr(sptbr, ((uintptr_t)root_page_table >> RISCV_PGSHIFT) | SATP_MODE_CHOICE);
388 unsigned int nr_free_store = nr_free;
//分配内核栈空间, nr_free = 0;
389 uintptr_t kernel_stack_top = __page_alloc() + RISCV_PGSIZE; free_page(p0);
390 return kernel_stack_top; free_page(p1);
391 } free_page(p2);
``` assert(nr_free == 3);
以上代码中我们给出了大体的注释请根据以上代码读者可以尝试画一下PK的逻辑地址空间结构图以及逻辑地址空间到物理地址空间的映射关系。 assert((p0 = alloc_page()) != NULL);
assert((p1 = alloc_page()) != NULL);
**4.1.2 练习二first_fit内存页分配算法需要编程** assert((p2 = alloc_page()) != NULL);
在"pk/pmm.c" 中,我们实现了对物理内存的管理。 assert(alloc_page() == NULL);
构建了物理内存页管理器框架struct pmm_manager结构如下 free_page(p0);
assert(!list_empty(&free_list));
```
135 const struct pmm_manager default_pmm_manager = { struct Page *p;
136 .name = "default_pmm_manager", assert((p = alloc_page()) == p0);
137 .init = default_init, assert(alloc_page() == NULL);
138 .init_memmap = default_init_memmap,
139 .alloc_pages = default_alloc_pages, assert(nr_free == 0);
140 .free_pages = default_free_pages, free_list = free_list_store;
141 .nr_free_pages = default_nr_free_pages, nr_free = nr_free_store;
142 .pmm_check = basic_check,
143 }; free_page(p);
``` free_page(p1);
free_page(p2);
默认的内存管理器有如下属性: }
l name:内存管理器的名字
l init:对内存管理算法所使用的数据结构进行初始化
#### 任务一 : OS内存的初始化过程理解 ####
l init_ memmap:根据物理内存设置内存管理算法的数据结构
任务描述:
l alloc_pages分配物理页
在"pk/mmap.c"内有 pk_vm_init()函数阅读该函数了解OS内存初始化的过程。
l free_pages释放物理页
l nr_free_pages空闲物理页的数量 预期输出:
l pmm_check :检查校验函数
```
参考已经实现的函数完成default_alloc_pages()和default_free_pages(),实现first_fit内存页分配算法。 364 uintptr_t pk_vm_init()
365 {
first_fit分配算法需要维护一个查找有序地址按从小到大排列空闲块以页为最小单位的连续地址空间的数据结构而双向链表是一个很好的选择。pk/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作所以需要了解如何使用这个文件提供的各种函数从而可以完成对双向链表的初始化/插入/删除等。 366 // HTIF address signedness and va2pa macro both cap memory size to 2 GiB
//设置物理内存大小
367 mem_size = MIN(mem_size, 1U << 31);
//计算物理页的数量
你可以使用python脚本检查你的输出 368 size_t mem_pages = mem_size >> RISCV_PGSHIFT;
369 free_pages = MAX(8, mem_pages >> (RISCV_PGLEVEL_BITS-1));
`./pke-lab3` 370
//_end为内核结束地址
若得到如下输出,你就已经成功完成了实验三!!! 371 extern char _end;
372 first_free_page = ROUNDUP((uintptr_t)&_end, RISCV_PGSIZE);
``` 373 first_free_paddr = first_free_page + free_pages * RISCV_PGSIZE;
build pk : OK 374
running app3 m2048 : OK //映射内核的物理空间
test3_m2048 : OK 375 root_page_table = (void*)__page_alloc();
running app3 m1024 : OK 376 __map_kernel_range(DRAM_BASE, DRAM_BASE, first_free_paddr - DRAM_BASE, PROT_READ|PROT_WRITE|PROT_EXEC);
test3_m1024 : OK 377
Score: 20/20 //crrent.mmap_max: 0x000000007f7ea000
``` 378 current.mmap_max = current.brk_max =
379 MIN(DRAM_BASE, mem_size - (first_free_paddr - DRAM_BASE));
### 4.2 基础知识 380
//映射用户栈
**4.2.1 物理内存空间与编址** 381 size_t stack_size = MIN(mem_pages >> 5, 2048) * RISCV_PGSIZE;
382 size_t stack_bottom = __do_mmap(current.mmap_max - stack_size, stack_size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED , 0, 0);
计算机的存储结构可以抽象的看做由N个连续的字节组成的数组。想一想在数组中我们如何找到一个元素对了是下标那么我们如何在内存中如何找打一个元素呢自然也是下标。这个下标的起始位置和位数由机器本身决定我们称之为“物理地址”。 383 kassert(stack_bottom != (uintptr_t)-1);
384 current.stack_top = stack_bottom + stack_size;
至于物理内存的大小由于我们的RISC-V目标机也就是我们的pke以及app运行的环境这里我们假设目标机为64位机即用到了56位的物理内存编址虚拟地址采用Sv39方案参见[第一章RISC-V体系结构的内容](chapter1.md/#paging)是由spike模拟器构造的构造过程中可以通过命令行的-m选项来指定物理内存的大小。而且spike会将目标机的物理内存地址从0x8000-0000开始编制。例如如果物理内存空间的大小为2GBspike的默认值则目标机的物理地址范围为[0x8000-0000, 0x10000-0000]其中0x10000-0000已经超过32位能够表达的范围了但是我们目标机是64位机再例如如果目标机物理内存空间大小为1GB启动spike时带入-m1024m参数则目标机的物理地址范围为[0x8000-0000, 0xC000-0000]。在以下的讨论中我们用符号PHYMEM_TOP代表物理内存空间的高地址部分在以上的两个例子中PHYMEM_TOP分别为0x10000-0000和0xC000-0000。在定义了PHYMEM_TOP符号后物理内存的范围就可以表示为[0x8000-0000, PHYMEM_TOP]。 385
//开启分页
我们的PK内核的逻辑编址可以通过查看pke.lds得知pke.lds有以下规则 386 flush_tlb();
387 write_csr(sptbr, ((uintptr_t)root_page_table >> RISCV_PGSHIFT) | SATP_MODE_CHOICE);
``` 388
14 /* Begining of code and text segment */ //分配内核栈空间,
15 . = 0x80000000; 389 uintptr_t kernel_stack_top = __page_alloc() + RISCV_PGSIZE;
``` 390 return kernel_stack_top;
391 }
可见PK内核的逻辑地址的起始也是0x8000-0000这也就意味着PK内核实际上采用的是直接地址映射的办法保证在未打开分页情况下逻辑地址到物理地址的映射的。代理内核的本质也是一段程序他本身是需要内存空间的而这一段空间在PK的设计中是静态分配给内核使用的不能被再分配给任何应用。那么静态分配给代理内核的内存空间具体是哪一段内存区域呢 ```
通过阅读PK的代码我们可知PK内核占据了以下这一段 以上代码中我们给出了大体的注释请根据以上代码读者可以尝试画一下PK的逻辑地址空间结构图以及逻辑地址空间到物理地址空间的映射关系。
```
KERNTOP------->+---------------------------------+ 0x80816000 #### 任务二 : first_fit内存页分配算法编程 ####
(first_free_paddr)| |
| Kern Physical Memory | 任务描述:
| | 8M 2048pages
(first_free_page)| |
DRAM_BASE----> +---------------------------------+ 0x80016000 在"pk/pmm.c" 中,我们实现了对物理内存的管理。
| Kern Text/Data/BBS |
KERN------>+---------------------------------+ 0x80000000 构建了物理内存页管理器框架struct pmm_manager结构如下
```
```
也就是说,[0x8000-0000, 0x8081-6000]这段物理内存空间是被PK内核所“保留”的余下的物理内存空间为[0x8081-6000PHYMEM_TOP]也就是下图中的Empty Memory*)部分,这部分内存将会是我们的操作系统需要真正用于动态分配(给应用程序)的空间,**而本实验就是要管理这部分物理内存空间**。 135 const struct pmm_manager default_pmm_manager = {
136 .name = "default_pmm_manager",
``` 137 .init = default_init,
PHYMEM_TOP ----> +-------------------------------------------------+ 138 .init_memmap = default_init_memmap,
| | 139 .alloc_pages = default_alloc_pages,
| Empty Memory (*) | 140 .free_pages = default_free_pages,
| | 141 .nr_free_pages = default_nr_free_pages,
KERNTOP ---> +-------------------------------------------------+ 0x80816000 142 .pmm_check = basic_check,
(first_free_paddr)| | 143 };
| PK kernel resevered | ```
| |
| | 默认的内存管理器有如下属性:
KERN ----> +-------------------------------------------------+ 0x80000000
``` l name:内存管理器的名字
最后我们来看物理内存分配的单位操作系统中物理页是物理内存分配的基本单位。一个物理页的大小是4KB我们使用结构体Page来表示其结构如图 l init:对内存管理算法所使用的数据结构进行初始化
``` l init_ memmap:根据物理内存设置内存管理算法的数据结构
struct Page {
sint_t ref; l alloc_pages分配物理页
uint_t flags;
uint_t property; l free_pages释放物理页
list_entry_t page_link;
}; l nr_free_pages空闲物理页的数量
```
l pmm_check :检查校验函数
l ref表示这样页被页表的引用记数
参考已经实现的函数完成default_alloc_pages()和default_free_pages(),实现first_fit内存页分配算法。
l flags表示此物理页的状态标记
first_fit分配算法需要维护一个查找有序地址按从小到大排列空闲块以页为最小单位的连续地址空间的数据结构而双向链表是一个很好的选择。pk/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作所以需要了解如何使用这个文件提供的各种函数从而可以完成对双向链表的初始化/插入/删除等。
l property用来记录某连续内存空闲块的大小即地址连续的空闲页的个数
l page_link是维持空闲物理页链表的重要结构。 预期输出:
Page结构体对应着物理页我们来看Page结构体同物理地址之间是如何转换的。首先我们需要先了解一下物理地址。 我们在实验二中已经讨论过中断入口函数位置的设置,现在继续跟踪中断入口函数,找出系统调用的执行过程。
<img src="pictures/fig4_1.png" alt="fig4_1" style="zoom:80%;" />
你可以使用python脚本检查你的输出
图4.1 RISCV64 物理地址
`./pke-lab3`
总的来说物理地址分为两部分页号PPN和offset
若得到如下输出,你就已经成功完成了实验三!!!
页号可以理解为物理页的编码而offset则为页内偏移量。现在考虑一下12位的offset对应的内存大小是多少呢
```
2<<12=40964KBPA4KB12offset便 build pk : OK
running app3 m2048 : OK
有了物理地址PA这一概念那PA和Pages结构体又是如何转换 test3_m2048 : OK
running app3 m1024 : OK
实际上在初始化空闲页链表之前系统会定义一个Page结构体的数组而链表的节点也正是来自于这些数组这个数组的每一项代表着一个物理页而且它们的数组下标就代表着每一项具体代表的是哪一个物理页就如下图所示 test3_m1024 : OK
Score: 20/20
<img src="pictures/fig4_2.png" alt="fig4_2" style="zoom:80%;" /> ```
## 4.2 实验指导
**3.2.2** **中断的处理过程**
**4.2.1 物理内存空间与编址**
当程序执行到中断之前程序是有自己的运行状态的例如寄存器里保持的上下文数据。当中断发生硬件在自动设置完中断原因和中断地址后就会调转到中断处理程序而中断处理程序同样会使用寄存器于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器我们称之为callee-saved寄存器。
计算机的存储结构可以抽象的看做由N个连续的字节组成的数组。想一想在数组中我们如何找到一个元素对了是下标那么我们如何在内存中如何找打一个元素呢自然也是下标。这个下标的起始位置和位数由机器本身决定我们称之为“物理地址”。
在PK的machine/minit.c中间中便通过delegate_traps()将部分中断及同步异常委托给S模式。同学们可以查看具体是哪些中断及同步异常
至于物理内存的大小由于我们的RISC-V目标机也就是我们的pke以及app运行的环境这里我们假设目标机为64位机即用到了56位的物理内存编址虚拟地址采用Sv39方案参见[第一章RISC-V体系结构的内容](chapter1.md/#paging)是由spike模拟器构造的构造过程中可以通过命令行的-m选项来指定物理内存的大小。而且spike会将目标机的物理内存地址从0x8000-0000开始编制。例如如果物理内存空间的大小为2GBspike的默认值则目标机的物理地址范围为[0x8000-0000, 0x10000-0000]其中0x10000-0000已经超过32位能够表达的范围了但是我们目标机是64位机再例如如果目标机物理内存空间大小为1GB启动spike时带入-m1024m参数则目标机的物理地址范围为[0x8000-0000, 0xC000-0000]。在以下的讨论中我们用符号PHYMEM_TOP代表物理内存空间的高地址部分在以上的两个例子中PHYMEM_TOP分别为0x10000-0000和0xC000-0000。在定义了PHYMEM_TOP符号后物理内存的范围就可以表示为[0x8000-0000, PHYMEM_TOP]。
```
43 // send S-mode interrupts and most exceptions straight to S-mode 我们的PK内核的逻辑编址可以通过查看pke.lds得知pke.lds有以下规则
44 static void delegate_traps()
45 { ```
46 if (!supports_extension('S')) 14 /* Begining of code and text segment */
47 return; 15 . = 0x80000000;
48 ```
49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
50 uintptr_t exceptions = 可见PK内核的逻辑地址的起始也是0x8000-0000这也就意味着PK内核实际上采用的是直接地址映射的办法保证在未打开分页情况下逻辑地址到物理地址的映射的。代理内核的本质也是一段程序他本身是需要内存空间的而这一段空间在PK的设计中是静态分配给内核使用的不能被再分配给任何应用。那么静态分配给代理内核的内存空间具体是哪一段内存区域呢
51 (1U << CAUSE_MISALIGNED_FETCH) |
52 (1U << CAUSE_FETCH_PAGE_FAULT) | 通过阅读PK的代码我们可知PK内核占据了以下这一段
53 (1U << CAUSE_BREAKPOINT) |
54 (1U << CAUSE_LOAD_PAGE_FAULT) | ```
55 (1U << CAUSE_STORE_PAGE_FAULT) | KERNTOP------->+---------------------------------+ 0x80816000
56 (1U << CAUSE_USER_ECALL); (first_free_paddr)| |
57 | Kern Physical Memory |
58 write_csr(mideleg, interrupts); | | 8M 2048pages
59 write_csr(medeleg, exceptions); (first_free_page)| |
60 assert(read_csr(mideleg) == interrupts); DRAM_BASE----> +---------------------------------+ 0x80016000
61 assert(read_csr(medeleg) == exceptions); | Kern Text/Data/BBS |
62 } KERN------>+---------------------------------+ 0x80000000
``` ```
这里介绍一下RISCV的中断委托机制在默认的情况下所有的异常都会被交由机器模式处理。但正如我们知道的那样大部分的系统调用都是在S模式下处理的因此RISCV提供了这一委托机制可以选择性的将中断交由S模式处理从而完全绕过M模式。 也就是说,[0x8000-0000, 0x8081-6000]这段物理内存空间是被PK内核所“保留”的余下的物理内存空间为[0x8081-6000PHYMEM_TOP]也就是下图中的Empty Memory*)部分,这部分内存将会是我们的操作系统需要真正用于动态分配(给应用程序)的空间,**而本实验就是要管理这部分物理内存空间**。
接下我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中stvec保存着发生异常时处理器需要跳转到的地址也就是说当中断发生我们将跳转至trap_entry现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中其代码如下 ```
PHYMEM_TOP ----> +-------------------------------------------------+
``` | |
60 trap_entry: | Empty Memory (*) |
61 csrrw sp, sscratch, sp | |
62 bnez sp, 1f KERNTOP ---> +-------------------------------------------------+ 0x80816000
63 csrr sp, sscratch (first_free_paddr)| |
64 1:addi sp,sp,-320 | PK kernel resevered |
65 save_tf | |
66 move a0,sp | |
67 jal handle_trap KERN ----> +-------------------------------------------------+ 0x80000000
``` ```
在61行交换了sp与sscratch的值这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。 最后我们来看物理内存分配的单位操作系统中物理页是物理内存分配的基本单位。一个物理页的大小是4KB我们使用结构体Page来表示其结构如图
如果sp也就是传入的sscratch值不为零则跳转至64行若sscratch的值为零则恢复原sp中的值。这是因为当中断来源于S模式是sscratch的值为0sp中存储的就是内核的堆栈地址。而当中断来源于U模式时sp中存储的是用户的堆栈地址sscratch中存储的则是内核的堆栈地址需要交换二者是sp指向内核的堆栈地址。 ```
struct Page {
接着在64,65行保存上下文最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中代码如下 sint_t ref;
uint_t flags;
``` uint_t property;
112 void handle_trap(trapframe_t* tf) list_entry_t page_link;
113 { };
114 if ((intptr_t)tf->cause < 0) ```
115 return handle_interrupt(tf);
116 l ref表示这样页被页表的引用记数
117 typedef void (*trap_handler)(trapframe_t*);
118 l flags表示此物理页的状态标记
119 const static trap_handler trap_handlers[] = {
120 [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch, l property用来记录某连续内存空闲块的大小即地址连续的空闲页的个数
121 [CAUSE_FETCH_ACCESS] = handle_instruction_access_fault,
122 [CAUSE_LOAD_ACCESS] = handle_load_access_fault, l page_link是维持空闲物理页链表的重要结构。
123 [CAUSE_STORE_ACCESS] = handle_store_access_fault,
124 [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch, Page结构体对应着物理页我们来看Page结构体同物理地址之间是如何转换的。首先我们需要先了解一下物理地址。
125 [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction,
126 [CAUSE_USER_ECALL] = handle_syscall, <img src="pictures/fig4_1.png" alt="fig4_1" style="zoom:80%;" />
127 [CAUSE_BREAKPOINT] = handle_breakpoint,
128 [CAUSE_MISALIGNED_LOAD] = handle_misaligned_load, 图4.1 RISCV64 物理地址
129 [CAUSE_MISALIGNED_STORE] = handle_misaligned_store,
130 [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load, 总的来说物理地址分为两部分页号PPN和offset
131 [CAUSE_STORE_PAGE_FAULT] = handle_fault_store,
132 }; 页号可以理解为物理页的编码而offset则为页内偏移量。现在考虑一下12位的offset对应的内存大小是多少呢
```
2<<12=40964KBPA4KB12offset便
handle_trap函数中实现了S模式下各类中断的处理。可以看到代码的126行就对应着系统调用的处理handle_syscall的实现如下
有了物理地址PA这一概念那PA和Pages结构体又是如何转换
```
100 static void handle_syscall(trapframe_t* tf) 实际上在初始化空闲页链表之前系统会定义一个Page结构体的数组而链表的节点也正是来自于这些数组这个数组的每一项代表着一个物理页而且它们的数组下标就代表着每一项具体代表的是哪一个物理页就如下图所示
101 {
102 tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13], <img src="pictures/fig4_2.png" alt="fig4_2" style="zoom:80%;" />
103 tf->gpr[14], tf->gpr[15], tf->gpr[17]);
104 tf->epc += 4;
105 } **3.2.2** **中断的处理过程**
```
当程序执行到中断之前程序是有自己的运行状态的例如寄存器里保持的上下文数据。当中断发生硬件在自动设置完中断原因和中断地址后就会调转到中断处理程序而中断处理程序同样会使用寄存器于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器我们称之为callee-saved寄存器。
还记得我们在例3.1中是将中断号写入x17寄存器嘛其对应的就是这里do_syscall的最后一个参数我们跟踪进入do_syscall函数其代码如下
在PK的machine/minit.c中间中便通过delegate_traps()将部分中断及同步异常委托给S模式。同学们可以查看具体是哪些中断及同步异常
```
313 long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n) ```
314 { 43 // send S-mode interrupts and most exceptions straight to S-mode
315 const static void* syscall_table[] = { 44 static void delegate_traps()
316 // your code here: 45 {
317 // add get_init_memsize syscall 46 if (!supports_extension('S'))
318 [SYS_init_memsize ] = sys_get_init_memsize, 47 return;
319 [SYS_exit] = sys_exit, 48
320 [SYS_exit_group] = sys_exit, 49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
321 [SYS_read] = sys_read, 50 uintptr_t exceptions =
322 [SYS_pread] = sys_pread, 51 (1U << CAUSE_MISALIGNED_FETCH) |
323 [SYS_write] = sys_write, 52 (1U << CAUSE_FETCH_PAGE_FAULT) |
324 [SYS_openat] = sys_openat, 53 (1U << CAUSE_BREAKPOINT) |
325 [SYS_close] = sys_close, 54 (1U << CAUSE_LOAD_PAGE_FAULT) |
326 [SYS_fstat] = sys_fstat, 55 (1U << CAUSE_STORE_PAGE_FAULT) |
327 [SYS_lseek] = sys_lseek, 56 (1U << CAUSE_USER_ECALL);
328 [SYS_renameat] = sys_renameat, 57
329 [SYS_mkdirat] = sys_mkdirat, 58 write_csr(mideleg, interrupts);
330 [SYS_getcwd] = sys_getcwd, 59 write_csr(medeleg, exceptions);
331 [SYS_brk] = sys_brk, 60 assert(read_csr(mideleg) == interrupts);
332 [SYS_uname] = sys_uname, 61 assert(read_csr(medeleg) == exceptions);
333 [SYS_prlimit64] = sys_stub_nosys, 62 }
334 [SYS_rt_sigaction] = sys_rt_sigaction, ```
335 [SYS_times] = sys_times,
336 [SYS_writev] = sys_writev, 这里介绍一下RISCV的中断委托机制在默认的情况下所有的异常都会被交由机器模式处理。但正如我们知道的那样大部分的系统调用都是在S模式下处理的因此RISCV提供了这一委托机制可以选择性的将中断交由S模式处理从而完全绕过M模式。
337 [SYS_readlinkat] = sys_stub_nosys,
338 [SYS_rt_sigprocmask] = sys_stub_success, 接下我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中stvec保存着发生异常时处理器需要跳转到的地址也就是说当中断发生我们将跳转至trap_entry现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中其代码如下
339 [SYS_ioctl] = sys_stub_nosys,
340 [SYS_getrusage] = sys_stub_nosys, ```
341 [SYS_getrlimit] = sys_stub_nosys, 60 trap_entry:
342 [SYS_setrlimit] = sys_stub_nosys, 61 csrrw sp, sscratch, sp
343 [SYS_set_tid_address] = sys_stub_nosys, 62 bnez sp, 1f
344 [SYS_set_robust_list] = sys_stub_nosys, 63 csrr sp, sscratch
345 }; 64 1:addi sp,sp,-320
346 65 save_tf
347 syscall_t f = 0; 66 move a0,sp
348 67 jal handle_trap
349 if (n < ARRAY_SIZE(syscall_table)) ```
350 f = syscall_table[n];
351 if (!f) 在61行交换了sp与sscratch的值这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。
352 panic("bad syscall #%ld!",n);
353 如果sp也就是传入的sscratch值不为零则跳转至64行若sscratch的值为零则恢复原sp中的值。这是因为当中断来源于S模式是sscratch的值为0sp中存储的就是内核的堆栈地址。而当中断来源于U模式时sp中存储的是用户的堆栈地址sscratch中存储的则是内核的堆栈地址需要交换二者是sp指向内核的堆栈地址。
354 return f(a0, a1, a2, a3, a4, a5, n);
355 } 接着在64,65行保存上下文最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中代码如下
```
```
do_syscall中通过传入的系统调用号n查询syscall_table得到对应的函数并最终执行系统调用。 112 void handle_trap(trapframe_t* tf)
113 {
114 if ((intptr_t)tf->cause < 0)
115 return handle_interrupt(tf);
116
117 typedef void (*trap_handler)(trapframe_t*);
118
119 const static trap_handler trap_handlers[] = {
120 [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch,
121 [CAUSE_FETCH_ACCESS] = handle_instruction_access_fault,
122 [CAUSE_LOAD_ACCESS] = handle_load_access_fault,
123 [CAUSE_STORE_ACCESS] = handle_store_access_fault,
124 [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch,
125 [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction,
126 [CAUSE_USER_ECALL] = handle_syscall,
127 [CAUSE_BREAKPOINT] = handle_breakpoint,
128 [CAUSE_MISALIGNED_LOAD] = handle_misaligned_load,
129 [CAUSE_MISALIGNED_STORE] = handle_misaligned_store,
130 [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load,
131 [CAUSE_STORE_PAGE_FAULT] = handle_fault_store,
132 };
```
handle_trap函数中实现了S模式下各类中断的处理。可以看到代码的126行就对应着系统调用的处理handle_syscall的实现如下
```
100 static void handle_syscall(trapframe_t* tf)
101 {
102 tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13],
103 tf->gpr[14], tf->gpr[15], tf->gpr[17]);
104 tf->epc += 4;
105 }
```
还记得我们在例3.1中是将中断号写入x17寄存器嘛其对应的就是这里do_syscall的最后一个参数我们跟踪进入do_syscall函数其代码如下
```
313 long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n)
314 {
315 const static void* syscall_table[] = {
316 // your code here:
317 // add get_init_memsize syscall
318 [SYS_init_memsize ] = sys_get_init_memsize,
319 [SYS_exit] = sys_exit,
320 [SYS_exit_group] = sys_exit,
321 [SYS_read] = sys_read,
322 [SYS_pread] = sys_pread,
323 [SYS_write] = sys_write,
324 [SYS_openat] = sys_openat,
325 [SYS_close] = sys_close,
326 [SYS_fstat] = sys_fstat,
327 [SYS_lseek] = sys_lseek,
328 [SYS_renameat] = sys_renameat,
329 [SYS_mkdirat] = sys_mkdirat,
330 [SYS_getcwd] = sys_getcwd,
331 [SYS_brk] = sys_brk,
332 [SYS_uname] = sys_uname,
333 [SYS_prlimit64] = sys_stub_nosys,
334 [SYS_rt_sigaction] = sys_rt_sigaction,
335 [SYS_times] = sys_times,
336 [SYS_writev] = sys_writev,
337 [SYS_readlinkat] = sys_stub_nosys,
338 [SYS_rt_sigprocmask] = sys_stub_success,
339 [SYS_ioctl] = sys_stub_nosys,
340 [SYS_getrusage] = sys_stub_nosys,
341 [SYS_getrlimit] = sys_stub_nosys,
342 [SYS_setrlimit] = sys_stub_nosys,
343 [SYS_set_tid_address] = sys_stub_nosys,
344 [SYS_set_robust_list] = sys_stub_nosys,
345 };
346
347 syscall_t f = 0;
348
349 if (n < ARRAY_SIZE(syscall_table))
350 f = syscall_table[n];
351 if (!f)
352 panic("bad syscall #%ld!",n);
353
354 return f(a0, a1, a2, a3, a4, a5, n);
355 }
```
do_syscall中通过传入的系统调用号n查询syscall_table得到对应的函数并最终执行系统调用。

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

@ -1,463 +1,488 @@
## 第六章实验5进程的封装 # 第六章实验5进程的封装
### 6.1 实验内容 ## 6.1 实验内容
实验要求在APP里写fork调用其执行过程将fork出一个子进程。在代理内核中实现fork的处理例程trap使其能够支撑APP程序的正确执行。 #### 应用: ####
在本次实验的app4.c文件中将会测试fork函数。代码中170及172系统调用分别对应着sys_fork和sys_getpid系统调用。调用fork函数后将会有两个返回。在父进程中fork返回新创建子进程的进程ID而在子进程中fork返回0。你需要阅读proc.c文件完善相关代码是的app4.c可以正常运行。 app5.c源文件如下
int main(){
**6.1.1 练习一alloc_proc需要编程**
if(fork() == 0) {
完善"pk/proc.c"中的alloc_proc(),你需要对以下属性进行初始化: printf("this is child process;my pid = %d\n",getpid());
}else {
l enum proc_state state; printf("this is farther process;my pid = %d\n",getpid());
}
l int pid;
return 0;
l int runs; }
l uintptr_t kstack; 以上代码中进行了fork调用其执行过程将fork出一个子进程。
l volatile bool need_resched;
l struct proc_struct *parent;
#### 任务一 : alloc_proc编程 ####
l struct mm_struct *mm;
任务描述:
l struct context context;
l struct trapframe *tf; 完善"pk/proc.c"中的alloc_proc(),你需要对以下属性进行初始化:
l uintptr_t pagetable; l enum proc_state state;
l uint32_t flags; l int pid;
l char name[PROC_NAME_LEN + 1]; l int runs;
l uintptr_t kstack;
**6.1.2 练习二do_fork需要编程** l volatile bool need_resched;
完善"pk/proc.c"中的do_fork函数你需要进行以下工作 l struct proc_struct *parent;
l 调用alloc_proc()来为子进程创建进程控制块 l struct mm_struct *mm;
l 调用setup_kstack来设置栈空间 l struct context context;
l 用copy_mm来拷贝页表 l struct trapframe *tf;
l 调用copy_thread来拷贝进程 l uintptr_t pagetable;
l 为子进程设置pid l uint32_t flags;
l 设置子进程状态为就绪 l char name[PROC_NAME_LEN + 1];
```
l 将子进程加入到链表中
完成以上代码后,你可以进行如下测试,然后输入如下命令:
#### 任务二 : do_fork编程 ####
`$ riscv64-unknown-elf-gcc ../app/app5.c -o ../app/elf/app5`
任务描述:
`$ spike ./obj/pke app/elf/app5`
预期的输出如下: l 调用alloc_proc()来为子进程创建进程控制块
``` l 调用setup_kstack来设置栈空间
PKE IS RUNNING
page fault vaddr:0x00000000000100c2 l 用copy_mm来拷贝页表
page fault vaddr:0x000000000001e17f
page fault vaddr:0x0000000000018d5a l 调用copy_thread来拷贝进程
page fault vaddr:0x000000000001a8ba
page fault vaddr:0x000000000001d218 l 为子进程设置pid
page fault vaddr:0x000000007f7e8bf0
page fault vaddr:0x0000000000014a68 l 设置子进程状态为就绪
page fault vaddr:0x00000000000162ce
page fault vaddr:0x000000000001c6e0 l 将子进程加入到链表中
page fault vaddr:0x0000000000012572
page fault vaddr:0x0000000000011fa6
page fault vaddr:0x0000000000019064
page fault vaddr:0x0000000000015304 预期输出:
page fault vaddr:0x0000000000017fd4
this is farther process;my pid = 1
sys_exit pid=1 完成以上代码后,你可以进行如下测试,然后输入如下命令:
page fault vaddr:0x0000000000010166
page fault vaddr:0x000000000001e160 `$ riscv64-unknown-elf-gcc ../app/app5.c -o ../app/elf/app5`
page fault vaddr:0x000000000001d030
page fault vaddr:0x0000000000014a68 `$ spike ./obj/pke app/elf/app5`
page fault vaddr:0x00000000000162ce
page fault vaddr:0x000000000001c6e0 预期的输出如下:
page fault vaddr:0x0000000000012572
page fault vaddr:0x0000000000011fa6 ```
page fault vaddr:0x0000000000019064 PKE IS RUNNING
page fault vaddr:0x000000000001abb6 page fault vaddr:0x00000000000100c2
page fault vaddr:0x0000000000015304 page fault vaddr:0x000000000001e17f
page fault vaddr:0x0000000000017fd4 page fault vaddr:0x0000000000018d5a
page fault vaddr:0x0000000000018cd4 page fault vaddr:0x000000000001a8ba
this is child process;my pid = 2 page fault vaddr:0x000000000001d218
sys_exit pid=2 page fault vaddr:0x000000007f7e8bf0
``` page fault vaddr:0x0000000000014a68
page fault vaddr:0x00000000000162ce
如果你的app可以正确输出的话那么运行检查的python脚本 page fault vaddr:0x000000000001c6e0
page fault vaddr:0x0000000000012572
`./pke-lab5` page fault vaddr:0x0000000000011fa6
page fault vaddr:0x0000000000019064
若得到如下输出,那么恭喜你,你已经成功完成了实验六!!! page fault vaddr:0x0000000000015304
page fault vaddr:0x0000000000017fd4
this is farther process;my pid = 1
sys_exit pid=1
``` page fault vaddr:0x0000000000010166
build pk : OK page fault vaddr:0x000000000001e160
running app5 : OK page fault vaddr:0x000000000001d030
test fork : OK page fault vaddr:0x0000000000014a68
Score: 20/20 page fault vaddr:0x00000000000162ce
``` page fault vaddr:0x000000000001c6e0
page fault vaddr:0x0000000000012572
### 6.2 基础知识 page fault vaddr:0x0000000000011fa6
page fault vaddr:0x0000000000019064
**6.2.1 进程结构** page fault vaddr:0x000000000001abb6
page fault vaddr:0x0000000000015304
在pk/proc.h中我们定义进程的结构如下 page fault vaddr:0x0000000000017fd4
page fault vaddr:0x0000000000018cd4
``` this is child process;my pid = 2
42 struct proc_struct { sys_exit pid=2
43 enum proc_state state; ```
44 int pid;
45 int runs; 如果你的app可以正确输出的话那么运行检查的python脚本
46 uintptr_t kstack;
47 volatile bool need_resched; `./pke-lab5`
48 struct proc_struct *parent;
50 struct context context; 若得到如下输出,那么恭喜你,你已经成功完成了实验六!!!
51 trapframe_t *tf;
52 uintptr_t pagetable;
53 uint32_t flags;
54 char name[PROC_NAME_LEN + 1]; ```
55 list_entry_t list_link; build pk : OK
56 list_entry_t hash_link; running app5 : OK
57 }; test fork : OK
``` Score: 20/20
```
可以看到在41行的枚举中我们定义了进程的四种状态其定义如下
## 6.2 实验指导
```
11 enum proc_state { **6.2.1 进程结构**
12 PROC_UNINIT = 0,
13 PROC_SLEEPING, 在pk/proc.h中我们定义进程的结构如下
14 PROC_RUNNABLE,
15 PROC_ZOMBIE, ```
16 }; 42 struct proc_struct {
``` 43 enum proc_state state;
44 int pid;
四种状态分别为未初始化(PROC_UNINIT)、睡眠PROC_SLEEPING、可运行PROC_RUNNABLE以及僵死PROC_ZOMBIE状态。 45 int runs;
46 uintptr_t kstack;
除却状态,进程还有以下重要属性: 47 volatile bool need_resched;
48 struct proc_struct *parent;
l pid进程id是进程的标识符 50 struct context context;
51 trapframe_t *tf;
l runs进程已经运行的时间 52 uintptr_t pagetable;
53 uint32_t flags;
l kstack进程的内核栈空间 54 char name[PROC_NAME_LEN + 1];
55 list_entry_t list_link;
l need_resched是否需要释放CPU 56 list_entry_t hash_link;
57 };
l parent进程的父进程 ```
l context进程的上下文 可以看到在41行的枚举中我们定义了进程的四种状态其定义如下
l tf当前中断的栈帧 ```
11 enum proc_state {
l pagetable进程的页表地址 12 PROC_UNINIT = 0,
13 PROC_SLEEPING,
l name进程名 14 PROC_RUNNABLE,
15 PROC_ZOMBIE,
除了上述属性可以看到在55、56行还维护了两个进程的链表这是操作系统内进程的组织方式系统维护一个进程链表以组织要管理的进程。 16 };
```
四种状态分别为未初始化(PROC_UNINIT)、睡眠PROC_SLEEPING、可运行PROC_RUNNABLE以及僵死PROC_ZOMBIE状态。
**6.2.2 设置第一个内核进程idleproc**
除却状态,进程还有以下重要属性:
在"pk/pk.c"的rest_of_boot_loader函数中调用了proc_init来设置第一个内核进程
l pid进程id是进程的标识符
```
317 void l runs进程已经运行的时间
318 proc_init() {
319 int i; l kstack进程的内核栈空间
320 extern uintptr_t kernel_stack_top;
321 l need_resched是否需要释放CPU
322 list_init(&proc_list);
323 for (i = 0; i < HASH_LIST_SIZE; i ++) { l parent进程的父进程
324 list_init(hash_list + i);
325 } l context进程的上下文
326
327 if ((idleproc = alloc_proc()) == NULL) { l tf当前中断的栈帧
328 panic("cannot alloc idleproc.\n");
329 } l pagetable进程的页表地址
330
331 idleproc->pid = 0; l name进程名
332 idleproc->state = PROC_RUNNABLE;
333 idleproc->kstack = kernel_stack_top; 除了上述属性可以看到在55、56行还维护了两个进程的链表这是操作系统内进程的组织方式系统维护一个进程链表以组织要管理的进程。
334 idleproc->need_resched = 1;
335 set_proc_name(idleproc, "idle");
336 nr_process ++;
337 **6.2.2 设置第一个内核进程idleproc**
338 currentproc = idleproc;
339 在"pk/pk.c"的rest_of_boot_loader函数中调用了proc_init来设置第一个内核进程
340 }
``` ```
317 void
322行的proc_list是系统所维护的进程链表324行的hash_list是一个大小为1024的list_entry_t的hash数组。在对系统所维护的两个list都初始化完成后系统为idleproc分配进程结构体。然后对idleproc的各个属性进行设置最终将currentproc改为idleproc。 318 proc_init() {
319 int i;
在上述代码中我们只是为idleproc分配了进程控制块但并没有切换到idleproc真正的切换代码在proc_init函数后面的run_loaded_program以及cpu_idle函数中进行。 320 extern uintptr_t kernel_stack_top;
321
322 list_init(&proc_list);
323 for (i = 0; i < HASH_LIST_SIZE; i ++) {
**6.2.3 do_fork** 324 list_init(hash_list + i);
325 }
在run_loaded_program中有如下代码 326
327 if ((idleproc = alloc_proc()) == NULL) {
``` 328 panic("cannot alloc idleproc.\n");
140 trapframe_t tf; 329 }
141 init_tf(&tf, current.entry, stack_top); 330
142 __clear_cache(0, 0); 331 idleproc->pid = 0;
143 do_fork(0,stack_top,&tf); 332 idleproc->state = PROC_RUNNABLE;
144 write_csr(sscratch, kstack_top); 333 idleproc->kstack = kernel_stack_top;
``` 334 idleproc->need_resched = 1;
335 set_proc_name(idleproc, "idle");
在这里声明了一个trapframe并且将它的gpr[2]sp设置为内核栈指针将它的epc设置为current.entry其中current.entry是elf文件的入口地址也就是app的起始执行位置随即我们调用了do_frok函数其中传入参数stack为0表示我们正在fork一个内核进程。 336 nr_process ++;
337
在do_frok函数中你会调用alloc_proc()来为子进程创建进程控制块、调用setup_kstack来设置栈空间调用copy_mm来拷贝页表调用copy_thread来拷贝进程。现在我们来对以上函数进行分析。 338 currentproc = idleproc;
339
setup_kstack函数代码如下在函数中我们为进程分配栈空间然后返回 340 }
```
```
210 static int 322行的proc_list是系统所维护的进程链表324行的hash_list是一个大小为1024的list_entry_t的hash数组。在对系统所维护的两个list都初始化完成后系统为idleproc分配进程结构体。然后对idleproc的各个属性进行设置最终将currentproc改为idleproc。
211 setup_kstack(struct proc_struct *proc) {
212 proc->kstack = (uintptr_t)__page_alloc(); 在上述代码中我们只是为idleproc分配了进程控制块但并没有切换到idleproc真正的切换代码在proc_init函数后面的run_loaded_program以及cpu_idle函数中进行。
213 return 0;
214 }
```
**6.2.3 do_fork**
copy_mm k函数代码如下在函数中我们对页表进行拷贝。
在run_loaded_program中有如下代码
```
228 static int ```
229 copy_mm(uint32_t clone_flags, struct proc_struct *proc) { 140 trapframe_t tf;
230 //assert(currentproc->mm == NULL); 141 init_tf(&tf, current.entry, stack_top);
231 /* do nothing in this project */ 142 __clear_cache(0, 0);
232 uintptr_t pagetable=(uintptr_t)__page_alloc(); 143 do_fork(0,stack_top,&tf);
233 memcpy((void *)pagetable,(void *)proc->pagetable,RISCV_PGSIZE); 144 write_csr(sscratch, kstack_top);
234 proc->pagetable=pagetable; ```
235 return 0;
236 } 在这里声明了一个trapframe并且将它的gpr[2]sp设置为内核栈指针将它的epc设置为current.entry其中current.entry是elf文件的入口地址也就是app的起始执行位置随即我们调用了do_frok函数其中传入参数stack为0表示我们正在fork一个内核进程。
```
在do_frok函数中你会调用alloc_proc()来为子进程创建进程控制块、调用setup_kstack来设置栈空间调用copy_mm来拷贝页表调用copy_thread来拷贝进程。现在我们来对以上函数进行分析。
最后是copy_thread函数
setup_kstack函数代码如下在函数中我们为进程分配栈空间然后返回
```
240 static void ```
241 copy_thread(struct proc_struct *proc, uintptr_t esp, trapframe_t *tf) { 210 static int
242 proc->tf = (trapframe_t *)(proc->kstack + KSTACKSIZE - sizeof(trapframe_t)); 211 setup_kstack(struct proc_struct *proc) {
243 *(proc->tf) = *tf; 212 proc->kstack = (uintptr_t)__page_alloc();
244 213 return 0;
245 proc->tf->gpr[10] = 0; 214 }
246 proc->tf->gpr[2] = (esp == 0) ? (uintptr_t)proc->tf -4 : esp; ```
247
248 proc->context.ra = (uintptr_t)forkret; copy_mm k函数代码如下在函数中我们对页表进行拷贝。
249 proc->context.sp = (uintptr_t)(proc->tf);
250 } ```
``` 228 static int
229 copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
在函数中首先对传入的栈帧进行拷贝并且将上下文中的ra设置为地址forkret将sp设置为该栈帧。 230 //assert(currentproc->mm == NULL);
231 /* do nothing in this project */
完成以上几步后我们为子进程设置pid将其加入到进程链表当中并且设置其状态为就绪。 232 uintptr_t pagetable=(uintptr_t)__page_alloc();
233 memcpy((void *)pagetable,(void *)proc->pagetable,RISCV_PGSIZE);
234 proc->pagetable=pagetable;
235 return 0;
**6.2.3 上下文切换** 236 }
```
每个进程都有着自己的上下文,在进程间切换时,需要对上下文一并切换。
最后是copy_thread函数
在pk/proc.c的cpu_idle中有以下代码
```
``` 240 static void
374 void 241 copy_thread(struct proc_struct *proc, uintptr_t esp, trapframe_t *tf) {
375 cpu_idle(void) { 242 proc->tf = (trapframe_t *)(proc->kstack + KSTACKSIZE - sizeof(trapframe_t));
376 while (1) { 243 *(proc->tf) = *tf;
377 if (currentproc->need_resched) { 244
378 schedule(); 245 proc->tf->gpr[10] = 0;
379 } 246 proc->tf->gpr[2] = (esp == 0) ? (uintptr_t)proc->tf -4 : esp;
380 } 247
381 } 248 proc->context.ra = (uintptr_t)forkret;
``` 249 proc->context.sp = (uintptr_t)(proc->tf);
250 }
在当前进程处于need_resched状态时会执行调度算法schedule其代码如下 ```
``` 在函数中首先对传入的栈帧进行拷贝并且将上下文中的ra设置为地址forkret将sp设置为该栈帧。
16 void
17 schedule(void) { 完成以上几步后我们为子进程设置pid将其加入到进程链表当中并且设置其状态为就绪。
18 list_entry_t *le, *last;
19 struct proc_struct *next = NULL;
20 {
21 currentproc->need_resched = 0; **6.2.3 上下文切换**
22 last = (currentproc == idleproc) ? &proc_list : &(currentproc->list_link);
23 le = last; 每个进程都有着自己的上下文,在进程间切换时,需要对上下文一并切换。
24 do {
25 if ((le = list_next(le)) != &proc_list) { 在pk/proc.c的cpu_idle中有以下代码
26 next = le2proc(le, list_link);
27 if (next->state == PROC_RUNNABLE) { ```
28 break; 374 void
29 } 375 cpu_idle(void) {
30 } 376 while (1) {
31 } while (le != last); 377 if (currentproc->need_resched) {
32 if (next == NULL || next->state != PROC_RUNNABLE) { 378 schedule();
33 next = idleproc; 379 }
34 } 380 }
35 next->runs ++; 381 }
36 if (next != currentproc) { ```
37 proc_run(next);
38 } 在当前进程处于need_resched状态时会执行调度算法schedule其代码如下
39 }
40 } ```
``` 16 void
17 schedule(void) {
在schedule函数中找到下一个需要执行的进程并执行执行代码proc_run如下 18 list_entry_t *le, *last;
19 struct proc_struct *next = NULL;
``` 20 {
145 void 21 currentproc->need_resched = 0;
146 proc_run(struct proc_struct *proc) { 22 last = (currentproc == idleproc) ? &proc_list : &(currentproc->list_link);
147 if (proc != currentproc) { 23 le = last;
148 bool intr_flag; 24 do {
149 struct proc_struct *prev = currentproc, *next = proc; 25 if ((le = list_next(le)) != &proc_list) {
150 currentproc = proc; 26 next = le2proc(le, list_link);
151 write_csr(sptbr, ((uintptr_t)next->pagetable >> RISCV_PGSHIFT) | SATP_MODE_CHOICE); 27 if (next->state == PROC_RUNNABLE) {
152 switch_to(&(prev->context), &(next->context)); 28 break;
153 29 }
154 } 30 }
155 } 31 } while (le != last);
``` 32 if (next == NULL || next->state != PROC_RUNNABLE) {
33 next = idleproc;
当传入的proc不为当前进程时执行切换操作 34 }
35 next->runs ++;
``` 36 if (next != currentproc) {
7 switch_to: 37 proc_run(next);
8 # save from's registers 38 }
9 STORE ra, 0*REGBYTES(a0) 39 }
10 STORE sp, 1*REGBYTES(a0) 40 }
11 STORE s0, 2*REGBYTES(a0) ```
12 STORE s1, 3*REGBYTES(a0)
13 STORE s2, 4*REGBYTES(a0) 在schedule函数中找到下一个需要执行的进程并执行执行代码proc_run如下
14 STORE s3, 5*REGBYTES(a0)
15 STORE s4, 6*REGBYTES(a0) ```
16 STORE s5, 7*REGBYTES(a0) 145 void
17 STORE s6, 8*REGBYTES(a0) 146 proc_run(struct proc_struct *proc) {
18 STORE s7, 9*REGBYTES(a0) 147 if (proc != currentproc) {
19 STORE s8, 10*REGBYTES(a0) 148 bool intr_flag;
20 STORE s9, 11*REGBYTES(a0) 149 struct proc_struct *prev = currentproc, *next = proc;
21 STORE s10, 12*REGBYTES(a0) 150 currentproc = proc;
22 STORE s11, 13*REGBYTES(a0) 151 write_csr(sptbr, ((uintptr_t)next->pagetable >> RISCV_PGSHIFT) | SATP_MODE_CHOICE);
23 152 switch_to(&(prev->context), &(next->context));
24 # restore to's registers 153
25 LOAD ra, 0*REGBYTES(a1) 154 }
26 LOAD sp, 1*REGBYTES(a1) 155 }
27 LOAD s0, 2*REGBYTES(a1) ```
28 LOAD s1, 3*REGBYTES(a1)
29 LOAD s2, 4*REGBYTES(a1) 当传入的proc不为当前进程时执行切换操作
30 LOAD s3, 5*REGBYTES(a1)
31 LOAD s4, 6*REGBYTES(a1) ```
32 LOAD s5, 7*REGBYTES(a1) 7 switch_to:
33 LOAD s6, 8*REGBYTES(a1) 8 # save from's registers
34 LOAD s7, 9*REGBYTES(a1) 9 STORE ra, 0*REGBYTES(a0)
35 LOAD s8, 10*REGBYTES(a1) 10 STORE sp, 1*REGBYTES(a0)
36 LOAD s9, 11*REGBYTES(a1) 11 STORE s0, 2*REGBYTES(a0)
37 LOAD s10, 12*REGBYTES(a1) 12 STORE s1, 3*REGBYTES(a0)
38 LOAD s11, 13*REGBYTES(a1) 13 STORE s2, 4*REGBYTES(a0)
39 14 STORE s3, 5*REGBYTES(a0)
40 ret 15 STORE s4, 6*REGBYTES(a0)
``` 16 STORE s5, 7*REGBYTES(a0)
17 STORE s6, 8*REGBYTES(a0)
可以看到在switch_to中我们正真执行了上一个进程的上下文保存以及下一个进程的上下文加载。在switch_to的最后一行我们执行ret指令该指令是一条从子过程返回的伪指令会将pc设置为x1ra寄存器的值还记得我们在copy_thread中层将ra设置为forkret嘛现在程序将从forkret继续执行 18 STORE s7, 9*REGBYTES(a0)
19 STORE s8, 10*REGBYTES(a0)
``` 20 STORE s9, 11*REGBYTES(a0)
160 static void 21 STORE s10, 12*REGBYTES(a0)
161 forkret(void) { 22 STORE s11, 13*REGBYTES(a0)
162 extern elf_info current; 23
163 load_elf(current.file_name,&current); 24 # restore to's registers
164 25 LOAD ra, 0*REGBYTES(a1)
165 int pid=currentproc->pid; 26 LOAD sp, 1*REGBYTES(a1)
166 struct proc_struct * proc=find_proc(pid); 27 LOAD s0, 2*REGBYTES(a1)
167 write_csr(sscratch, proc->tf); 28 LOAD s1, 3*REGBYTES(a1)
168 set_csr(sstatus, SSTATUS_SUM | SSTATUS_FS); 29 LOAD s2, 4*REGBYTES(a1)
169 currentproc->tf->status = (read_csr(sstatus) &~ SSTATUS_SPP &~ SSTATUS_SIE) | SSTATUS_SPIE; 30 LOAD s3, 5*REGBYTES(a1)
170 forkrets(currentproc->tf); 31 LOAD s4, 6*REGBYTES(a1)
171 } 32 LOAD s5, 7*REGBYTES(a1)
``` 33 LOAD s6, 8*REGBYTES(a1)
34 LOAD s7, 9*REGBYTES(a1)
35 LOAD s8, 10*REGBYTES(a1)
36 LOAD s9, 11*REGBYTES(a1)
我们进入forkrets 37 LOAD s10, 12*REGBYTES(a1)
38 LOAD s11, 13*REGBYTES(a1)
``` 39
121 forkrets: 40 ret
122 # set stack to this new process's trapframe ```
123 move sp, a0
124 addi sp,sp,320 可以看到在switch_to中我们正真执行了上一个进程的上下文保存以及下一个进程的上下文加载。在switch_to的最后一行我们执行ret指令该指令是一条从子过程返回的伪指令会将pc设置为x1ra寄存器的值还记得我们在copy_thread中层将ra设置为forkret嘛现在程序将从forkret继续执行
125 csrw sscratch,sp
126 j start_user ```
``` 160 static void
161 forkret(void) {
162 extern elf_info current;
163 load_elf(current.file_name,&current);
164
165 int pid=currentproc->pid;
``` 166 struct proc_struct * proc=find_proc(pid);
76 .globl start_user 167 write_csr(sscratch, proc->tf);
77 start_user: 168 set_csr(sstatus, SSTATUS_SUM | SSTATUS_FS);
78 LOAD t0, 32*REGBYTES(a0) 169 currentproc->tf->status = (read_csr(sstatus) &~ SSTATUS_SPP &~ SSTATUS_SIE) | SSTATUS_SPIE;
79 LOAD t1, 33*REGBYTES(a0) 170 forkrets(currentproc->tf);
80 csrw sstatus, t0 171 }
81 csrw sepc, t1 ```
82
83 # restore x registers
84 LOAD x1,1*REGBYTES(a0)
85 LOAD x2,2*REGBYTES(a0) 我们进入forkrets
86 LOAD x3,3*REGBYTES(a0)
87 LOAD x4,4*REGBYTES(a0) ```
88 LOAD x5,5*REGBYTES(a0) 121 forkrets:
89 LOAD x6,6*REGBYTES(a0) 122 # set stack to this new process's trapframe
90 LOAD x7,7*REGBYTES(a0) 123 move sp, a0
91 LOAD x8,8*REGBYTES(a0) 124 addi sp,sp,320
92 LOAD x9,9*REGBYTES(a0) 125 csrw sscratch,sp
93 LOAD x11,11*REGBYTES(a0) 126 j start_user
94 LOAD x12,12*REGBYTES(a0) ```
95 LOAD x13,13*REGBYTES(a0)
96 LOAD x14,14*REGBYTES(a0)
97 LOAD x15,15*REGBYTES(a0)
98 LOAD x16,16*REGBYTES(a0)
99 LOAD x17,17*REGBYTES(a0)
100 LOAD x18,18*REGBYTES(a0) ```
101 LOAD x19,19*REGBYTES(a0) 76 .globl start_user
102 LOAD x20,20*REGBYTES(a0) 77 start_user:
103 LOAD x21,21*REGBYTES(a0) 78 LOAD t0, 32*REGBYTES(a0)
104 LOAD x22,22*REGBYTES(a0) 79 LOAD t1, 33*REGBYTES(a0)
105 LOAD x23,23*REGBYTES(a0) 80 csrw sstatus, t0
106 LOAD x24,24*REGBYTES(a0) 81 csrw sepc, t1
107 LOAD x25,25*REGBYTES(a0) 82
108 LOAD x26,26*REGBYTES(a0) 83 # restore x registers
109 LOAD x27,27*REGBYTES(a0) 84 LOAD x1,1*REGBYTES(a0)
110 LOAD x28,28*REGBYTES(a0) 85 LOAD x2,2*REGBYTES(a0)
111 LOAD x29,29*REGBYTES(a0) 86 LOAD x3,3*REGBYTES(a0)
112 LOAD x30,30*REGBYTES(a0) 87 LOAD x4,4*REGBYTES(a0)
113 LOAD x31,31*REGBYTES(a0) 88 LOAD x5,5*REGBYTES(a0)
114 # restore a0 last 89 LOAD x6,6*REGBYTES(a0)
115 LOAD x10,10*REGBYTES(a0) 90 LOAD x7,7*REGBYTES(a0)
116 91 LOAD x8,8*REGBYTES(a0)
117 # gtfo 92 LOAD x9,9*REGBYTES(a0)
118 sret 93 LOAD x11,11*REGBYTES(a0)
``` 94 LOAD x12,12*REGBYTES(a0)
95 LOAD x13,13*REGBYTES(a0)
可以看到在forkrets最后执行了sret程序就此由内核切换至用户程序执行 96 LOAD x14,14*REGBYTES(a0)
97 LOAD x15,15*REGBYTES(a0)
98 LOAD x16,16*REGBYTES(a0)
99 LOAD x17,17*REGBYTES(a0)
100 LOAD x18,18*REGBYTES(a0)
101 LOAD x19,19*REGBYTES(a0)
102 LOAD x20,20*REGBYTES(a0)
103 LOAD x21,21*REGBYTES(a0)
104 LOAD x22,22*REGBYTES(a0)
105 LOAD x23,23*REGBYTES(a0)
106 LOAD x24,24*REGBYTES(a0)
107 LOAD x25,25*REGBYTES(a0)
108 LOAD x26,26*REGBYTES(a0)
109 LOAD x27,27*REGBYTES(a0)
110 LOAD x28,28*REGBYTES(a0)
111 LOAD x29,29*REGBYTES(a0)
112 LOAD x30,30*REGBYTES(a0)
113 LOAD x31,31*REGBYTES(a0)
114 # restore a0 last
115 LOAD x10,10*REGBYTES(a0)
116
117 # gtfo
118 sret
```
可以看到在forkrets最后执行了sret程序就此由内核切换至用户程序执行
Loading…
Cancel
Save