From 7908a91c8b4a488d6ec488e00b3b5d4c090dd41f Mon Sep 17 00:00:00 2001 From: Zhiyuan Shao Date: Mon, 21 Dec 2020 17:48:45 +0800 Subject: [PATCH] adjust the formats of the labs --- chapter3.md | 827 +++++++++++++++++++++++---------------------- chapter4.md | 728 ++++++++++++++++++++++------------------ chapter5.md | 537 ++++++++++++++++------------- chapter6.md | 949 +++++++++++++++++++++++++++------------------------- 4 files changed, 1617 insertions(+), 1424 deletions(-) diff --git a/chapter3.md b/chapter3.md index b01e762..28db8d0 100644 --- a/chapter3.md +++ b/chapter3.md @@ -1,399 +1,430 @@ -## 第三章.(实验2)系统调用的实现 - -### 3.1 实验环境搭建 - -实验2需要在实验1的基础之上完成,所以首先你需要切换到lab2_small的branch,然后commit你的代码。 - -首先,查看本地拥有的branch,输入如下命令: - -`$ git branch` - -如果有如下输出: - -``` -lab1_small -lab2_small -lab3_small -lab4_small -lab5_small -``` - -则你本地就有实验二的branch,那么可以直接切换分支到lab2,输入如下命令: - -`$ git checkout lab2_small` - -当然,如果本地没有lab2的相关代码,也可以直接从远端拉取: - -`$ git checkout -b lab2_small origin/ lab2_small` - -然后merge实验一中你的代码: - -`$ git merge -m "merge lab1" lab1_small` - -完成一切后,我们就可以正式进入实验二了! - -### 3.2 实验内容 - -实验要求:了解系统调用的执行过程,并实现一个自定义的系统调用。 - -**3.2.1 练习一:在app中使用系统调用** - -系统调用的英文名字是System Call,用户通过系统调用来执行一些需要特权的任务,那么我们具体在app中是如何使用内核所提供的系统调用的呢? - -RISC-V中提供了ecall指令,用于向运行时环境发出请求,我们可以使用内联汇编使用该指令,进行系统调用,代码如下: - -``` - 1 #define ecall() ({\ - 2 asm volatile(\ - 3 "li x17,81\n"\ - 4 "ecall");\ - 5 }) - 6 - 7 int main(void){ - 8 //调用自定义的81号系统调用 - 9 ecall(); - 10 return 0; - 11 } -``` - -例3.1 ecall - -以上代码中,我们将系统调用号81通过x17寄存器传给内核,再通过ecall指令进行系统调用,当然目前代理内核中并没有81号系统调用的实现,这需要我们在后面的实验中完成。 - -**3.2.2 练习二:系统调用过程跟踪** - -在我们执行了ecall指令后,代理内核中又是如何执行的呢? - -在第一章的表1.7中,我们可以看到Environment call from U-mode是exception(trap)的一种,其对应的code是8。我们在实验二中已经讨论过中断入口函数位置的设置,现在继续跟踪中断入口函数,找出系统调用的执行过程。 - -**3.2.3 练习三:自定义系统调用(需要编程)** - -阅读pk目录syscall.c文件,增加一个系统调用sys_get_memsize(),系统调用返回spike设置的内存空间大小, 系统调用号为81。 - -提示:在pk目录下的mmap.c文件中,函数pk_vm_init中定义了代理内核的内存空间大小。 - -spike 通过-m来指定分配的物理内存,单位是MiB,默认为2048。如: - -`$ spike obj/pke app/elf/app2_1` - -得到的输出如下: - -``` -PKE IS RUNNING -physical mem_size = 0x80000000 -``` - -可以看到,默认情况下,spike的物理内存是2GB - -你可以修改-m选项,运行app3观察输出。 - -`$ spike -m1024 obj/pke app/elf/app2_1` - -预计得到输出格式: - -``` -PKE IS RUNNING -physical mem_size = 0x40000000 -``` - -如果你的app可以正确输出的话,那么运行检查的python脚本: - -`$ ./pke-lab2` - -若得到如下输出,那么恭喜你,你已经成功完成了实验二!!! - -``` -build pk : OK -running app3 m2048 : OK -test3_m2048 : OK -running app3 m1024 : OK -test3_m1024 : OK -Score: 20/20 -``` - -### 3.3 基础知识 - -**3.3.1 系统调用** - -​首先,我们要知道什么是系统调用。 - -例如读文件(read)、写文件(write)等,其实我们已经接触过形形色色的系统调用。系统调用和函数调用的外形相似,但他们有着本质的不同。 - -系统调用的英文名字是System Call。由于用户进程只能在操作系统给它圈定好的“用户环境”中执行,但“用户环境”限制了用户进程能够执行的指令,即用户进程只能执行一般的指令,无法执行特权指令。如果用户进程想执行一些需要特权指令的任务,比如通过网卡发网络包等,只能让操作系统来代劳了。系统调用就是用户模式下请求操作系统执行某些特权指令的任务的机制。 - -相较于函数调用在普通的用户模式下运行,系统调用则运行在内核模式中。 - -见下图: - - fig3_1 - -图3.1系统调用过程 - -系统调用属于一种中断,当用户申请系统调用时,系统会从用户态陷入到内核态,完成相应的服务后,再回到原来的用户态上下文中。 - -**3.3.2 代理内核与应用程序的加载** - -阅读pke.lds文件可以看到整个PK程序的入口为:reset_vector函数: - -``` -3 OUTPUT_ARCH( "riscv" ) -4 -5 ENTRY( reset_vector ) -``` - -我们在machine/mentry.S中找的这个符号。 - -``` -36 reset_vector: -37 j do_reset -``` - -首先初始化x0~x31共32个寄存器,其中x10(a0)寄存器与x11(a1)寄存器存储着从之前boot loader中传来的参数而不复位。 - -``` -223 do_reset: -224 li x1, 0 - ..... -255 li x31, 0 -``` - -将mscratch寄存器置0 - -``` -256 csrw mscratch, x0 -``` - -将trap_vector的地址写入t0寄存器,trap_vector是mechine模式下异常处理的入口地址。再将t0的值写入mtvec寄存器中。然后读取mtvec寄存器中的地址到t1寄存器。比较t0于t1。 - -``` -259 la t0, trap_vector -260 mtvec, t0 -261 rr t1, mtvec -262 1:bne t0, t1, 1b -``` - -正常情况下,t1自然是的等于t0的,于是程序顺序执行,将栈地址写入sp寄存器中 - -``` -264 la sp, stacks + RISCV_PGSIZE - MENTRY_FRAME_SIZE -``` - -读取mhartid到a3寄存器,调整sp - -``` -266 csrr a3, mhartid -267 slli a2, a3, RISCV_PGSHIFT -268 add sp, sp, a2 -``` - -当a3不等于0时,跳转到 init_first_hart - -``` -270 # Boot on the first hart -271 beqz a3, init_first_hart -``` - -此时进入"machine/minit.c"文件,在init_first_hart中对外设进行初始化 - -``` -154 void init_first_hart(uintptr_t hartid, uintptr_t dtb) -155 { - …… //初始化外设 -180 boot_loader(dtb); -181 } -``` - -在init_first_hart的最后一行,调用boot_loader函数 - -``` -160 void boot_loader(uintptr_t dtb) -161 { - ……. //CSR寄存器设置 -169 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0); -170 } -``` - -在boot_loader中,经历设置中断入口地址,清零sscratch寄存器,关中断等一系列操作后。最后会调用enter_supervisor_mode函数正式切换至Supervisor模式。 - -``` -204 void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1) -205 { -206 uintptr_t mstatus = read_csr(mstatus); -207 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, PRV_S); -208 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0); -209 write_csr(mstatus, mstatus); -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 -213 *p_fcsr = 0; -214 #endif -215 write_csr(mepc, fn); -216 -217 register uintptr_t a0 asm ("a0") = arg0; -218 register uintptr_t a1 asm ("a1") = arg1; -219 asm volatile ("mret" : : "r" (a0), "r" (a1)); -220 __builtin_unreachable(); -221 } -``` - -在enter_supervisor_mode函数中,将 mstatus的MPP域设置为1,表示中断发生之前的模式是Supervisor,将mstatus的MPIE域设置为0,表示中断发生前MIE的值为0。随即将机器模式的内核栈顶写入mscratch寄存器中,设置mepc为rest_of_boot_loader的地址,并将kernel_stack_top与0作为参数存入a0和a1。 - -最后,执行mret指令,该指令执行时,程序从机器模式的异常返回,将程序计数器pc设置为mepc,即rest_of_boot_loader的地址;将特权级设置为mstatus寄存器的MPP域,即方才所设置的代表Supervisor的1,MPP设置为0;将mstatus寄存器的MIE域设置为MPIE,即方才所设置的表示中断关闭的0,MPIE设置为1。 - -于是,当mret指令执行完毕,程序将从rest_of_boot_loader继续执行。 - -``` -144 static void rest_of_boot_loader(uintptr_t kstack_top) -145 { -146 arg_buf args; -147 size_t argc = parse_args(&args); -148 if (!argc) -149 panic("tell me what ELF to load!"); -150 -151 // load program named by argv[0] -152 long phdrs[128]; -153 current.phdr = (uintptr_t)phdrs; -154 current.phdr_size = sizeof(phdrs); -155 load_elf(args.argv[0], ¤t); -156 -157 run_loaded_program(argc, args.argv, kstack_top); -158 } -``` - -这个函数中,我们对应用程序的ELF文件进行解析,并且最终运行应用程序。 - -**3.3.3 中断的处理过程** - -考虑一下,当程序执行到中断之前,程序是有自己的运行状态的,例如寄存器里保持的上下文数据。当中断发生,硬件在自动设置完中断原因和中断地址后,就会调转到中断处理程序,而中断处理程序同样会使用寄存器,于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器,我们称之为callee-saved寄存器。 - -在PK的machine/minit.c中间中,便通过delegate_traps(),将部分中断及同步异常委托给S模式。(同学们可以查看具体是哪些中断及同步异常) - -``` - 43 // send S-mode interrupts and most exceptions straight to S-mode - 44 static void delegate_traps() - 45 { - 46 if (!supports_extension('S')) - 47 return; - 48 - 49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; - 50 uintptr_t exceptions = - 51 (1U << CAUSE_MISALIGNED_FETCH) | - 52 (1U << CAUSE_FETCH_PAGE_FAULT) | - 53 (1U << CAUSE_BREAKPOINT) | - 54 (1U << CAUSE_LOAD_PAGE_FAULT) | - 55 (1U << CAUSE_STORE_PAGE_FAULT) | - 56 (1U << CAUSE_USER_ECALL); - 57 - 58 write_csr(mideleg, interrupts); - 59 write_csr(medeleg, exceptions); - 60 assert(read_csr(mideleg) == interrupts); - 61 assert(read_csr(medeleg) == exceptions); - 62 } -``` - -这里介绍一下RISCV的中断委托机制,在默认的情况下,所有的异常都会被交由机器模式处理。但正如我们知道的那样,大部分的系统调用都是在S模式下处理的,因此RISCV提供了这一委托机制,可以选择性的将中断交由S模式处理,从而完全绕过M模式。 - -接下,我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中,stvec保存着发生异常时处理器需要跳转到的地址,也就是说当中断发生,我们将跳转至trap_entry,现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中,其代码如下: - -``` - 60 trap_entry: - 61 csrrw sp, sscratch, sp - 62 bnez sp, 1f - 63 csrr sp, sscratch - 64 1:addi sp,sp,-320 - 65 save_tf - 66 move a0,sp - 67 jal handle_trap -``` - -在61行,交换了sp与sscratch的值,这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。 - -如果sp也就是传入的sscratch值不为零,则跳转至64行,若sscratch的值为零,则恢复原sp中的值。这是因为,当中断来源于S模式时,sscratch的值为0,sp中存储的就是内核的堆栈地址。而当中断来源于U模式时,sp中存储的是用户的堆栈地址,sscratch中存储的则是内核的堆栈地址,需要交换二者,是sp指向内核的堆栈地址。 - -接着在64,65行保存上下文,最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中,代码如下: - -``` -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得到对应的函数,并最终执行系统调用。 +# 第三章.(实验2)系统调用的实现 + +## 3.1 实验环境搭建 + +实验2需要在实验1的基础之上完成,所以首先你需要切换到lab2_small的branch,然后commit你的代码。 + +首先,查看本地拥有的branch,输入如下命令: + +`$ git branch` + +如果有如下输出: + +``` +lab1_small +lab2_small +lab3_small +lab4_small +lab5_small +``` + +则你本地就有实验二的branch,那么可以直接切换分支到lab2,输入如下命令: + +`$ git checkout lab2_small` + +当然,如果本地没有lab2的相关代码,也可以直接从远端拉取: + +`$ git checkout -b lab2_small origin/ lab2_small` + +然后merge实验一中你的代码: + +`$ git merge -m "merge lab1" lab1_small` + +完成一切后,我们就可以正式进入实验二了! + +## 3.2 实验内容 + + +#### 应用: #### + +app2_1.c源文件如下: + + 1 #define ecall() ({\ + 2 asm volatile(\ + 3 "li x17,81\n"\ + 4 "ecall");\ + 5 }) + 6 + 7 int main(void){ + 8 //调用自定义的81号系统调用 + 9 ecall(); + 10 return 0; + 11 } + + + + +以上代码中,我们将系统调用号81通过x17寄存器传给内核,再通过ecall指令进行系统调用,当然目前代理内核中并没有81号系统调用的实现,这需要我们在后面的实验中完成。 + + + + + +#### 任务一 : 在app中使用系统调用(理解) #### + +任务描述: + +系统调用的英文名字是System Call,用户通过系统调用来执行一些需要特权的任务,那么我们具体在app中是如何使用内核所提供的系统调用的呢? + +RISC-V中提供了ecall指令,用于向运行时环境发出请求,我们可以使用内联汇编使用该指令,进行系统调用。 + + +预期输出: + +理解app2_1的调用过程。 + + + +#### 任务二 : 系统调用过程跟踪(理解) #### + +任务描述: + + +在我们执行了ecall指令后,代理内核中又是如何执行的呢? + +在第一章的表1.7中,我们可以看到Environment call from U-mode是exception(trap)的一种,其对应的code是8。 + + +预期输出: + +我们在实验二中已经讨论过中断入口函数位置的设置,现在继续跟踪中断入口函数,找出系统调用的执行过程。 + + +#### 任务三 : 自定义系统调用(编程) #### + +任务描述: + + +阅读pk目录syscall.c文件,增加一个系统调用sys_get_memsize(),系统调用返回spike设置的内存空间大小, 系统调用号为81。 + + +预期输出: + + +在pk目录下的mmap.c文件中,函数pk_vm_init中定义了代理内核的内存空间大小。 + +spike 通过-m来指定分配的物理内存,单位是MiB,默认为2048。如: + +`$ spike obj/pke app/elf/app2_1` + +得到的输出如下: + +``` +PKE IS RUNNING +physical mem_size = 0x80000000 +``` + +可以看到,默认情况下,spike的物理内存是2GB + +你可以修改-m选项,运行app3观察输出。 + +`$ spike -m1024 obj/pke app/elf/app2_1` + +预计得到输出格式: + +``` +PKE IS RUNNING +physical mem_size = 0x40000000 +``` + +如果你的app可以正确输出的话,那么运行检查的python脚本: + +`$ ./pke-lab2` + +若得到如下输出,那么恭喜你,你已经成功完成了实验二!!! + +``` +build pk : OK +running app3 m2048 : OK +test3_m2048 : OK +running app3 m1024 : OK +test3_m1024 : OK +Score: 20/20 +``` + +## 3.3 实验指导 + +**3.3.1 系统调用** + +​首先,我们要知道什么是系统调用。 + +例如读文件(read)、写文件(write)等,其实我们已经接触过形形色色的系统调用。系统调用和函数调用的外形相似,但他们有着本质的不同。 + +系统调用的英文名字是System Call。由于用户进程只能在操作系统给它圈定好的“用户环境”中执行,但“用户环境”限制了用户进程能够执行的指令,即用户进程只能执行一般的指令,无法执行特权指令。如果用户进程想执行一些需要特权指令的任务,比如通过网卡发网络包等,只能让操作系统来代劳了。系统调用就是用户模式下请求操作系统执行某些特权指令的任务的机制。 + +相较于函数调用在普通的用户模式下运行,系统调用则运行在内核模式中。 + +见下图: + + fig3_1 + +图3.1系统调用过程 + +系统调用属于一种中断,当用户申请系统调用时,系统会从用户态陷入到内核态,完成相应的服务后,再回到原来的用户态上下文中。 + +**3.3.2 代理内核与应用程序的加载** + +阅读pke.lds文件可以看到整个PK程序的入口为:reset_vector函数: + +``` +3 OUTPUT_ARCH( "riscv" ) +4 +5 ENTRY( reset_vector ) +``` + +我们在machine/mentry.S中找的这个符号。 + +``` +36 reset_vector: +37 j do_reset +``` + +首先初始化x0~x31共32个寄存器,其中x10(a0)寄存器与x11(a1)寄存器存储着从之前boot loader中传来的参数而不复位。 + +``` +223 do_reset: +224 li x1, 0 + ..... +255 li x31, 0 +``` + +将mscratch寄存器置0 + +``` +256 csrw mscratch, x0 +``` + +将trap_vector的地址写入t0寄存器,trap_vector是mechine模式下异常处理的入口地址。再将t0的值写入mtvec寄存器中。然后读取mtvec寄存器中的地址到t1寄存器。比较t0于t1。 + +``` +259 la t0, trap_vector +260 mtvec, t0 +261 rr t1, mtvec +262 1:bne t0, t1, 1b +``` + +正常情况下,t1自然是的等于t0的,于是程序顺序执行,将栈地址写入sp寄存器中 + +``` +264 la sp, stacks + RISCV_PGSIZE - MENTRY_FRAME_SIZE +``` + +读取mhartid到a3寄存器,调整sp + +``` +266 csrr a3, mhartid +267 slli a2, a3, RISCV_PGSHIFT +268 add sp, sp, a2 +``` + +当a3不等于0时,跳转到 init_first_hart + +``` +270 # Boot on the first hart +271 beqz a3, init_first_hart +``` + +此时进入"machine/minit.c"文件,在init_first_hart中对外设进行初始化 + +``` +154 void init_first_hart(uintptr_t hartid, uintptr_t dtb) +155 { + …… //初始化外设 +180 boot_loader(dtb); +181 } +``` + +在init_first_hart的最后一行,调用boot_loader函数 + +``` +160 void boot_loader(uintptr_t dtb) +161 { + ……. //CSR寄存器设置 +169 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0); +170 } +``` + +在boot_loader中,经历设置中断入口地址,清零sscratch寄存器,关中断等一系列操作后。最后会调用enter_supervisor_mode函数正式切换至Supervisor模式。 + +``` +204 void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1) +205 { +206 uintptr_t mstatus = read_csr(mstatus); +207 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, PRV_S); +208 mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0); +209 write_csr(mstatus, mstatus); +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 +213 *p_fcsr = 0; +214 #endif +215 write_csr(mepc, fn); +216 +217 register uintptr_t a0 asm ("a0") = arg0; +218 register uintptr_t a1 asm ("a1") = arg1; +219 asm volatile ("mret" : : "r" (a0), "r" (a1)); +220 __builtin_unreachable(); +221 } +``` + +在enter_supervisor_mode函数中,将 mstatus的MPP域设置为1,表示中断发生之前的模式是Superior,将mstatus的MPIE域设置为0,表示中段发生前MIE的值为0。随机将机器模式的内核栈顶写入mscratch寄存器中,设置mepc为rest_of_boot_loader的地址,并将kernel_stack_top与0作为参数存入a0和a1。 + +最后,执行mret指令,该指令执行时,程序从机器模式的异常返回,将程序计数器pc设置为mepc,即rest_of_boot_loader的地址;将特权级设置为mstatus寄存器的MPP域,即方才所设置的代表Superior的1,MPP设置为0;将mstatus寄存器的MIE域设置为MPIE,即方才所设置的表示中断关闭的0,MPIE设置为1。 + +于是,当mret指令执行完毕,程序将从rest_of_boot_loader继续执行。 + +``` +144 static void rest_of_boot_loader(uintptr_t kstack_top) +145 { +146 arg_buf args; +147 size_t argc = parse_args(&args); +148 if (!argc) +149 panic("tell me what ELF to load!"); +150 +151 // load program named by argv[0] +152 long phdrs[128]; +153 current.phdr = (uintptr_t)phdrs; +154 current.phdr_size = sizeof(phdrs); +155 load_elf(args.argv[0], ¤t); +156 +157 run_loaded_program(argc, args.argv, kstack_top); +158 } +``` + +这个函数中,我们对应用程序的ELF文件进行解析,并且最终运行应用程序。 + +**3.3.3 中断的处理过程** + +考虑一下,当程序执行到中断之前,程序是有自己的运行状态的,例如寄存器里保持的上下文数据。当中断发生,硬件在自动设置完中断原因和中断地址后,就会调转到中断处理程序,而中断处理程序同样会使用寄存器,于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器,我们称之为callee-saved寄存器。 + +在PK的machine/minit.c中间中,便通过delegate_traps(),将部分中断及同步异常委托给S模式。(同学们可以查看具体是哪些中断及同步异常) + +``` + 43 // send S-mode interrupts and most exceptions straight to S-mode + 44 static void delegate_traps() + 45 { + 46 if (!supports_extension('S')) + 47 return; + 48 + 49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; + 50 uintptr_t exceptions = + 51 (1U << CAUSE_MISALIGNED_FETCH) | + 52 (1U << CAUSE_FETCH_PAGE_FAULT) | + 53 (1U << CAUSE_BREAKPOINT) | + 54 (1U << CAUSE_LOAD_PAGE_FAULT) | + 55 (1U << CAUSE_STORE_PAGE_FAULT) | + 56 (1U << CAUSE_USER_ECALL); + 57 + 58 write_csr(mideleg, interrupts); + 59 write_csr(medeleg, exceptions); + 60 assert(read_csr(mideleg) == interrupts); + 61 assert(read_csr(medeleg) == exceptions); + 62 } +``` + +这里介绍一下RISCV的中断委托机制,在默认的情况下,所有的异常都会被交由机器模式处理。但正如我们知道的那样,大部分的系统调用都是在S模式下处理的,因此RISCV提供了这一委托机制,可以选择性的将中断交由S模式处理,从而完全绕过M模式。 + +接下,我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中,stvec保存着发生异常时处理器需要跳转到的地址,也就是说当中断发生,我们将跳转至trap_entry,现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中,其代码如下: + +``` + 60 trap_entry: + 61 csrrw sp, sscratch, sp + 62 bnez sp, 1f + 63 csrr sp, sscratch + 64 1:addi sp,sp,-320 + 65 save_tf + 66 move a0,sp + 67 jal handle_trap +``` + +在61行,交换了sp与sscratch的值,这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。 + +如果sp也就是传入的sscratch值不为零,则跳转至64行,若sscratch的值为零,则恢复原sp中的值。这是因为,当中断来源于S模式是,sscratch的值为0,sp中存储的就是内核的堆栈地址。而当中断来源于U模式时,sp中存储的是用户的堆栈地址,sscratch中存储的则是内核的堆栈地址,需要交换二者,是sp指向内核的堆栈地址。 + +接着在64,65行保存上下文,最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中,代码如下: + +``` +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得到对应的函数,并最终执行系统调用。 \ No newline at end of file diff --git a/chapter4.md b/chapter4.md index e6e36c6..af87d2d 100644 --- a/chapter4.md +++ b/chapter4.md @@ -1,323 +1,405 @@ -## 第四章.(实验3)物理内存管理 - -### 4.1 实验内容 - -实验要求:了解物理内存,管理物理内存。 - -**4.1.1 练习一:OS内存的初始化过程** - -在"pk/mmap.c"内有 pk_vm_init()函数,阅读该函数,了解OS内存初始化的过程。 - -``` -364 uintptr_t pk_vm_init() -365 { -366 // HTIF address signedness and va2pa macro both cap memory size to 2 GiB - //设置物理内存大小 -367 mem_size = MIN(mem_size, 1U << 31); - //计算物理页的数量 -368 size_t mem_pages = mem_size >> RISCV_PGSHIFT; -369 free_pages = MAX(8, mem_pages >> (RISCV_PGLEVEL_BITS-1)); -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; -374 - //映射内核的物理空间 -375 root_page_table = (void*)__page_alloc(); -376 __map_kernel_range(DRAM_BASE, DRAM_BASE, first_free_paddr - DRAM_BASE, PROT_READ|PROT_WRITE|PROT_EXEC); -377 - //crrent.mmap_max: 0x000000007f7ea000 -378 current.mmap_max = current.brk_max = -379 MIN(DRAM_BASE, mem_size - (first_free_paddr - DRAM_BASE)); -380 - //映射用户栈 -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); -383 kassert(stack_bottom != (uintptr_t)-1); -384 current.stack_top = stack_bottom + stack_size; -385 - //开启分页 -386 flush_tlb(); -387 write_csr(sptbr, ((uintptr_t)root_page_table >> RISCV_PGSHIFT) | SATP_MODE_CHOICE); -388 - //分配内核栈空间, -389 uintptr_t kernel_stack_top = __page_alloc() + RISCV_PGSIZE; -390 return kernel_stack_top; -391 } -``` - -以上代码中,我们给出了大体的注释,请根据以上代码,读者可以尝试画一下PK的逻辑地址空间结构图,以及逻辑地址空间到物理地址空间的映射关系。 - -**4.1.2 练习二:first_fit内存页分配算法(需要编程)** - -在"pk/pmm.c" 中,我们实现了对物理内存的管理。 - -构建了物理内存页管理器框架:struct pmm_manager结构如下: - -``` -135 const struct pmm_manager default_pmm_manager = { -136 .name = "default_pmm_manager", -137 .init = default_init, -138 .init_memmap = default_init_memmap, -139 .alloc_pages = default_alloc_pages, -140 .free_pages = default_free_pages, -141 .nr_free_pages = default_nr_free_pages, -142 .pmm_check = basic_check, -143 }; -``` - -默认的内存管理器有如下属性: - -l name:内存管理器的名字 - -l init:对内存管理算法所使用的数据结构进行初始化 - -l init_ memmap:根据物理内存设置内存管理算法的数据结构 - -l alloc_pages:分配物理页 - -l free_pages:释放物理页 - -l nr_free_pages:空闲物理页的数量 - -l pmm_check :检查校验函数 - -参考已经实现的函数,完成default_alloc_pages()和default_free_pages(),实现first_fit内存页分配算法。 - -first_fit分配算法需要维护一个查找有序(地址按从小到大排列)空闲块(以页为最小单位的连续地址空间)的数据结构,而双向链表是一个很好的选择。pk/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作,所以需要了解如何使用这个文件提供的各种函数,从而可以完成对双向链表的初始化/插入/删除等。 - -​ - -你可以使用python脚本检查你的输出: - -`./pke-lab3` - -若得到如下输出,你就已经成功完成了实验三!!! - -``` -build pk : OK -running app3 m2048 : OK - test3_m2048 : OK -running app3 m1024 : OK - test3_m1024 : OK -Score: 20/20 -``` - -### 4.2 基础知识 - -**4.2.1 物理内存空间与编址** - -计算机的存储结构可以抽象的看做由N个连续的字节组成的数组。想一想,在数组中我们如何找到一个元素?对了!是下标!!那么我们如何在内存中如何找打一个元素呢?自然也是‘下标’。这个下标的起始位置和位数由机器本身决定,我们称之为“物理地址”。 - -至于物理内存的大小,由于我们的RISC-V目标机(也就是我们的pke以及app运行的环境,这里我们假设目标机为64位机,即用到了56位的物理内存编址,虚拟地址采用Sv39方案,参见[第一章RISC-V体系结构的内容](chapter1.md/#paging))是由spike模拟器构造的,构造过程中可以通过命令行的-m选项来指定物理内存的大小。而且,spike会将目标机的物理内存地址从0x8000-0000开始编制。例如,如果物理内存空间的大小为2GB(spike的默认值),则目标机的物理地址范围为:[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]。 - -我们的PK内核的逻辑编址,可以通过查看pke.lds得知,pke.lds有以下规则: - -``` -14 /* Begining of code and text segment */ -15 . = 0x80000000; -``` - -可见,PK内核的逻辑地址的起始也是0x8000-0000!这也就意味着PK内核实际上采用的是直接地址映射的办法保证在未打开分页情况下,逻辑地址到物理地址的映射的。代理内核的本质也是一段程序,他本身是需要内存空间的,而这一段空间在PK的设计中是静态分配给内核使用的,不能被再分配给任何应用。那么静态分配给代理内核的内存空间具体是哪一段内存区域呢? - -通过阅读PK的代码,我们可知PK内核占据了以下这一段: - -``` - KERNTOP------->+---------------------------------+ 0x80816000 -(first_free_paddr)| | - | Kern Physical Memory | - | | 8M 2048pages - (first_free_page)| | - DRAM_BASE----> +---------------------------------+ 0x80016000 - | Kern Text/Data/BBS | - KERN------>+---------------------------------+ 0x80000000 -``` - -也就是说,[0x8000-0000, 0x8081-6000]这段物理内存空间是被PK内核所“保留”的,余下的物理内存空间为[0x8081-6000,PHYMEM_TOP],也就是下图中的Empty Memory(*)部分,这部分内存将会是我们的操作系统需要真正用于动态分配(给应用程序)的空间,**而本实验就是要管理这部分物理内存空间**。 - -``` - PHYMEM_TOP ----> +-------------------------------------------------+ - | | - | Empty Memory (*) | - | | - KERNTOP ---> +-------------------------------------------------+ 0x80816000 -(first_free_paddr)| | - | PK kernel resevered | - | | - | | - KERN ----> +-------------------------------------------------+ 0x80000000 -``` - -最后,我们来看物理内存分配的单位:操作系统中,物理页是物理内存分配的基本单位。一个物理页的大小是4KB,我们使用结构体Page来表示,其结构如图: - -``` -struct Page { - sint_t ref; - uint_t flags; - uint_t property; - list_entry_t page_link; -}; -``` - -l ref表示这样页被页表的引用记数 - -l flags表示此物理页的状态标记 - -l property用来记录某连续内存空闲块的大小(即地址连续的空闲页的个数) - -l page_link是维持空闲物理页链表的重要结构。 - -Page结构体对应着物理页,我们来看Page结构体同物理地址之间是如何转换的。首先,我们需要先了解一下物理地址。 - -fig4_1 - -图4.1 RISCV64 物理地址 - -总的来说,物理地址分为两部分:页号(PPN)和offset - -页号可以理解为物理页的编码,而offset则为页内偏移量。现在考虑一下12位的offset对应的内存大小是多少呢? - -2<<12=4096也就是4KB,还记得我们讲过的物PA理页大小是多少吗?没错是4KB。12位的offset设计便是由此而来。 - -有了物理地址(PA)这一概念,那PA和Pages结构体又是如何转换? - -实际上在初始化空闲页链表之前,系统会定义一个Page结构体的数组,而链表的节点也正是来自于这些数组,这个数组的每一项代表着一个物理页,而且它们的数组下标就代表着每一项具体代表的是哪一个物理页,就如下图所示: - - fig4_2 - - -**3.2.2** **中断的处理过程** - -当程序执行到中断之前,程序是有自己的运行状态的,例如寄存器里保持的上下文数据。当中断发生,硬件在自动设置完中断原因和中断地址后,就会调转到中断处理程序,而中断处理程序同样会使用寄存器,于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器,我们称之为callee-saved寄存器。 - -在PK的machine/minit.c中间中,便通过delegate_traps(),将部分中断及同步异常委托给S模式。(同学们可以查看具体是哪些中断及同步异常) - -``` - 43 // send S-mode interrupts and most exceptions straight to S-mode - 44 static void delegate_traps() - 45 { - 46 if (!supports_extension('S')) - 47 return; - 48 - 49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; - 50 uintptr_t exceptions = - 51 (1U << CAUSE_MISALIGNED_FETCH) | - 52 (1U << CAUSE_FETCH_PAGE_FAULT) | - 53 (1U << CAUSE_BREAKPOINT) | - 54 (1U << CAUSE_LOAD_PAGE_FAULT) | - 55 (1U << CAUSE_STORE_PAGE_FAULT) | - 56 (1U << CAUSE_USER_ECALL); - 57 - 58 write_csr(mideleg, interrupts); - 59 write_csr(medeleg, exceptions); - 60 assert(read_csr(mideleg) == interrupts); - 61 assert(read_csr(medeleg) == exceptions); - 62 } -``` - -​这里介绍一下RISCV的中断委托机制,在默认的情况下,所有的异常都会被交由机器模式处理。但正如我们知道的那样,大部分的系统调用都是在S模式下处理的,因此RISCV提供了这一委托机制,可以选择性的将中断交由S模式处理,从而完全绕过M模式。 - -​接下,我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中,stvec保存着发生异常时处理器需要跳转到的地址,也就是说当中断发生,我们将跳转至trap_entry,现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中,其代码如下: - -``` - 60 trap_entry: - 61 csrrw sp, sscratch, sp - 62 bnez sp, 1f - 63 csrr sp, sscratch - 64 1:addi sp,sp,-320 - 65 save_tf - 66 move a0,sp - 67 jal handle_trap -``` - -​在61行,交换了sp与sscratch的值,这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。 - -​如果sp也就是传入的sscratch值不为零,则跳转至64行,若sscratch的值为零,则恢复原sp中的值。这是因为,当中断来源于S模式是,sscratch的值为0,sp中存储的就是内核的堆栈地址。而当中断来源于U模式时,sp中存储的是用户的堆栈地址,sscratch中存储的则是内核的堆栈地址,需要交换二者,是sp指向内核的堆栈地址。 - -​接着在64,65行保存上下文,最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中,代码如下: - -``` -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得到对应的函数,并最终执行系统调用。 +# 第四章.(实验3)物理内存管理 + +## 4.1 实验内容 + + + +#### 应用: #### + + +app3_1.c源文件如下: + + 1 #define ecall() ({\ + 2 asm volatile(\ + 3 "li x17,81\n"\ + 4 "ecall");\ + 5 }) + 6 + 7 int main(void){ + 8 //调用自定义的81号系统调用 + 9 ecall(); + 10 return 0; + 11 } + +对于操作系统来说,内存分配的过程需要对应用层透明,故而实验三的app同实验二相同,并在内核中对于的内存分配单元做如下校验: + + + static void + basic_check(void) { + struct Page *p0, *p1, *p2; + p0 = p1 = p2 = NULL; + assert((p0 = alloc_page()) != NULL); + assert((p1 = alloc_page()) != NULL); + assert((p2 = alloc_page()) != NULL); + + assert(p0 != p1 && p0 != p2 && p1 != p2); + assert(p0->ref == 0 && p1->ref == 0 && p2->ref == 0); + + + list_entry_t free_list_store = free_list; + list_init(&free_list); + assert(list_empty(&free_list)); + + unsigned int nr_free_store = nr_free; + nr_free = 0; + free_page(p0); + free_page(p1); + free_page(p2); + assert(nr_free == 3); + + assert((p0 = alloc_page()) != NULL); + assert((p1 = alloc_page()) != NULL); + assert((p2 = alloc_page()) != NULL); + + assert(alloc_page() == NULL); + + free_page(p0); + assert(!list_empty(&free_list)); + + struct Page *p; + assert((p = alloc_page()) == p0); + assert(alloc_page() == NULL); + + assert(nr_free == 0); + free_list = free_list_store; + nr_free = nr_free_store; + + free_page(p); + free_page(p1); + free_page(p2); + } + + + + +#### 任务一 : OS内存的初始化过程(理解) #### + +任务描述: + +在"pk/mmap.c"内有 pk_vm_init()函数,阅读该函数,了解OS内存初始化的过程。 + + +预期输出: + + +``` +364 uintptr_t pk_vm_init() +365 { +366 // HTIF address signedness and va2pa macro both cap memory size to 2 GiB + //设置物理内存大小 +367 mem_size = MIN(mem_size, 1U << 31); + //计算物理页的数量 +368 size_t mem_pages = mem_size >> RISCV_PGSHIFT; +369 free_pages = MAX(8, mem_pages >> (RISCV_PGLEVEL_BITS-1)); +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; +374 + //映射内核的物理空间 +375 root_page_table = (void*)__page_alloc(); +376 __map_kernel_range(DRAM_BASE, DRAM_BASE, first_free_paddr - DRAM_BASE, PROT_READ|PROT_WRITE|PROT_EXEC); +377 + //crrent.mmap_max: 0x000000007f7ea000 +378 current.mmap_max = current.brk_max = +379 MIN(DRAM_BASE, mem_size - (first_free_paddr - DRAM_BASE)); +380 + //映射用户栈 +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); +383 kassert(stack_bottom != (uintptr_t)-1); +384 current.stack_top = stack_bottom + stack_size; +385 + //开启分页 +386 flush_tlb(); +387 write_csr(sptbr, ((uintptr_t)root_page_table >> RISCV_PGSHIFT) | SATP_MODE_CHOICE); +388 + //分配内核栈空间, +389 uintptr_t kernel_stack_top = __page_alloc() + RISCV_PGSIZE; +390 return kernel_stack_top; +391 } +``` + +以上代码中,我们给出了大体的注释,请根据以上代码,读者可以尝试画一下PK的逻辑地址空间结构图,以及逻辑地址空间到物理地址空间的映射关系。 + + +#### 任务二 : first_fit内存页分配算法(编程) #### + +任务描述: + + +在"pk/pmm.c" 中,我们实现了对物理内存的管理。 + +构建了物理内存页管理器框架:struct pmm_manager结构如下: + +``` +135 const struct pmm_manager default_pmm_manager = { +136 .name = "default_pmm_manager", +137 .init = default_init, +138 .init_memmap = default_init_memmap, +139 .alloc_pages = default_alloc_pages, +140 .free_pages = default_free_pages, +141 .nr_free_pages = default_nr_free_pages, +142 .pmm_check = basic_check, +143 }; +``` + +默认的内存管理器有如下属性: + +l name:内存管理器的名字 + +l init:对内存管理算法所使用的数据结构进行初始化 + +l init_ memmap:根据物理内存设置内存管理算法的数据结构 + +l alloc_pages:分配物理页 + +l free_pages:释放物理页 + +l nr_free_pages:空闲物理页的数量 + +l pmm_check :检查校验函数 + +参考已经实现的函数,完成default_alloc_pages()和default_free_pages(),实现first_fit内存页分配算法。 + +first_fit分配算法需要维护一个查找有序(地址按从小到大排列)空闲块(以页为最小单位的连续地址空间)的数据结构,而双向链表是一个很好的选择。pk/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作,所以需要了解如何使用这个文件提供的各种函数,从而可以完成对双向链表的初始化/插入/删除等。 + + +预期输出: + +我们在实验二中已经讨论过中断入口函数位置的设置,现在继续跟踪中断入口函数,找出系统调用的执行过程。 + + +你可以使用python脚本检查你的输出: + +`./pke-lab3` + +若得到如下输出,你就已经成功完成了实验三!!! + +``` +build pk : OK +running app3 m2048 : OK + test3_m2048 : OK +running app3 m1024 : OK + test3_m1024 : OK +Score: 20/20 +``` + +## 4.2 实验指导 + +**4.2.1 物理内存空间与编址** + +计算机的存储结构可以抽象的看做由N个连续的字节组成的数组。想一想,在数组中我们如何找到一个元素?对了!是下标!!那么我们如何在内存中如何找打一个元素呢?自然也是‘下标’。这个下标的起始位置和位数由机器本身决定,我们称之为“物理地址”。 + +至于物理内存的大小,由于我们的RISC-V目标机(也就是我们的pke以及app运行的环境,这里我们假设目标机为64位机,即用到了56位的物理内存编址,虚拟地址采用Sv39方案,参见[第一章RISC-V体系结构的内容](chapter1.md/#paging))是由spike模拟器构造的,构造过程中可以通过命令行的-m选项来指定物理内存的大小。而且,spike会将目标机的物理内存地址从0x8000-0000开始编制。例如,如果物理内存空间的大小为2GB(spike的默认值),则目标机的物理地址范围为:[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]。 + +我们的PK内核的逻辑编址,可以通过查看pke.lds得知,pke.lds有以下规则: + +``` +14 /* Begining of code and text segment */ +15 . = 0x80000000; +``` + +可见,PK内核的逻辑地址的起始也是0x8000-0000!这也就意味着PK内核实际上采用的是直接地址映射的办法保证在未打开分页情况下,逻辑地址到物理地址的映射的。代理内核的本质也是一段程序,他本身是需要内存空间的,而这一段空间在PK的设计中是静态分配给内核使用的,不能被再分配给任何应用。那么静态分配给代理内核的内存空间具体是哪一段内存区域呢? + +通过阅读PK的代码,我们可知PK内核占据了以下这一段: + +``` + KERNTOP------->+---------------------------------+ 0x80816000 +(first_free_paddr)| | + | Kern Physical Memory | + | | 8M 2048pages + (first_free_page)| | + DRAM_BASE----> +---------------------------------+ 0x80016000 + | Kern Text/Data/BBS | + KERN------>+---------------------------------+ 0x80000000 +``` + +也就是说,[0x8000-0000, 0x8081-6000]这段物理内存空间是被PK内核所“保留”的,余下的物理内存空间为[0x8081-6000,PHYMEM_TOP],也就是下图中的Empty Memory(*)部分,这部分内存将会是我们的操作系统需要真正用于动态分配(给应用程序)的空间,**而本实验就是要管理这部分物理内存空间**。 + +``` + PHYMEM_TOP ----> +-------------------------------------------------+ + | | + | Empty Memory (*) | + | | + KERNTOP ---> +-------------------------------------------------+ 0x80816000 +(first_free_paddr)| | + | PK kernel resevered | + | | + | | + KERN ----> +-------------------------------------------------+ 0x80000000 +``` + +最后,我们来看物理内存分配的单位:操作系统中,物理页是物理内存分配的基本单位。一个物理页的大小是4KB,我们使用结构体Page来表示,其结构如图: + +``` +struct Page { + sint_t ref; + uint_t flags; + uint_t property; + list_entry_t page_link; +}; +``` + +l ref表示这样页被页表的引用记数 + +l flags表示此物理页的状态标记 + +l property用来记录某连续内存空闲块的大小(即地址连续的空闲页的个数) + +l page_link是维持空闲物理页链表的重要结构。 + +Page结构体对应着物理页,我们来看Page结构体同物理地址之间是如何转换的。首先,我们需要先了解一下物理地址。 + +fig4_1 + +图4.1 RISCV64 物理地址 + +总的来说,物理地址分为两部分:页号(PPN)和offset + +页号可以理解为物理页的编码,而offset则为页内偏移量。现在考虑一下12位的offset对应的内存大小是多少呢? + +2<<12=4096也就是4KB,还记得我们讲过的物PA理页大小是多少吗?没错是4KB。12位的offset设计便是由此而来。 + +有了物理地址(PA)这一概念,那PA和Pages结构体又是如何转换? + +实际上在初始化空闲页链表之前,系统会定义一个Page结构体的数组,而链表的节点也正是来自于这些数组,这个数组的每一项代表着一个物理页,而且它们的数组下标就代表着每一项具体代表的是哪一个物理页,就如下图所示: + + fig4_2 + + +**3.2.2** **中断的处理过程** + +当程序执行到中断之前,程序是有自己的运行状态的,例如寄存器里保持的上下文数据。当中断发生,硬件在自动设置完中断原因和中断地址后,就会调转到中断处理程序,而中断处理程序同样会使用寄存器,于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器,我们称之为callee-saved寄存器。 + +在PK的machine/minit.c中间中,便通过delegate_traps(),将部分中断及同步异常委托给S模式。(同学们可以查看具体是哪些中断及同步异常) + +``` + 43 // send S-mode interrupts and most exceptions straight to S-mode + 44 static void delegate_traps() + 45 { + 46 if (!supports_extension('S')) + 47 return; + 48 + 49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; + 50 uintptr_t exceptions = + 51 (1U << CAUSE_MISALIGNED_FETCH) | + 52 (1U << CAUSE_FETCH_PAGE_FAULT) | + 53 (1U << CAUSE_BREAKPOINT) | + 54 (1U << CAUSE_LOAD_PAGE_FAULT) | + 55 (1U << CAUSE_STORE_PAGE_FAULT) | + 56 (1U << CAUSE_USER_ECALL); + 57 + 58 write_csr(mideleg, interrupts); + 59 write_csr(medeleg, exceptions); + 60 assert(read_csr(mideleg) == interrupts); + 61 assert(read_csr(medeleg) == exceptions); + 62 } +``` + +​这里介绍一下RISCV的中断委托机制,在默认的情况下,所有的异常都会被交由机器模式处理。但正如我们知道的那样,大部分的系统调用都是在S模式下处理的,因此RISCV提供了这一委托机制,可以选择性的将中断交由S模式处理,从而完全绕过M模式。 + +​接下,我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中,stvec保存着发生异常时处理器需要跳转到的地址,也就是说当中断发生,我们将跳转至trap_entry,现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中,其代码如下: + +``` + 60 trap_entry: + 61 csrrw sp, sscratch, sp + 62 bnez sp, 1f + 63 csrr sp, sscratch + 64 1:addi sp,sp,-320 + 65 save_tf + 66 move a0,sp + 67 jal handle_trap +``` + +​在61行,交换了sp与sscratch的值,这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。 + +​如果sp也就是传入的sscratch值不为零,则跳转至64行,若sscratch的值为零,则恢复原sp中的值。这是因为,当中断来源于S模式是,sscratch的值为0,sp中存储的就是内核的堆栈地址。而当中断来源于U模式时,sp中存储的是用户的堆栈地址,sscratch中存储的则是内核的堆栈地址,需要交换二者,是sp指向内核的堆栈地址。 + +​接着在64,65行保存上下文,最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中,代码如下: + +``` +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得到对应的函数,并最终执行系统调用。 diff --git a/chapter5.md b/chapter5.md index 8596343..86f4b0c 100644 --- a/chapter5.md +++ b/chapter5.md @@ -1,242 +1,297 @@ -## 第五章.(实验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物理地址的定义: - -fig5_1 - -图5.1 RISCV64 物理地址 - -​可以看到,物理地址由PPN(物理页号)与Offset(偏移量)组成。这里的PPN就对应着上述的物理页。 - -​现在,我们来看RISCV虚拟地址的定义: - -fig5_2 - -图5.2 RISCV64 虚拟地址 - -​ 可以看到虚拟地址同样由页号和偏移量组成。而这二者之间是如何转换的呢?RV64支持多种分页方案,如Sv32、Sv39、Sv48,它们的原理相似,这里我们对pke中所使用的Sv39进行讲述。Sv39中维护着一个三级的页表,其页表项定义如下: - - fig1_7 - -图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); -``` - - - - fig5_4 - - 图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则对应着各级VPN,pt_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即虚拟地址的位数为39,RISCV_PGSHIFT即代表虚拟地址中Offset的位数,二者相减,剩下的就是VPN0、VPN1……VPNX的位数,在除以VPN的位数,得到就是VPN的数量。由于pke中式Sv39,故而VPN的数量为3,即VPN0、VPN1、VPN2。 - -接着我们使用pt_idx函数得到各级VPN的值,依据图5.2所示逐级查询,一直找到该虚拟地址对应的页表项,而该页表项中存着该虚拟地址所对应的物理页号,再加上虚拟地址中的偏离量,我们就可以找到最终的物理地址了!! - - - -**5.2.3** **缺页异常处理** - -``` - 1 #include - 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函数。 - +# 第五章.(实验4)缺页异常的处理 + +## 5.1 实验内容 + + +#### 应用: #### + + +app4_1.c源文件如下: + + + 1 #include + 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进行访问,将触发缺页异常。随后,对同一页内的地址0x7f000010、0x7f000fff进行访问,此时由于页0x7f000000已近完成映射,故而不会发生异常。最后有对新的一页进行访问,将再次引发缺页异常。 + +app4_2.c源文件如下: + + 1 #include + 2 void fun(int num){ + 3 if(num==0){ + 4 return; + 5 } + 6 fun(num-1); + 7 } + 8 int main(){ + 9 int num=10000; + 10 fun(num); + 11 printf("end \n"); + 12 return 0; + 13 } + + +以上代码中进行了大量递归,这将产生缺页。 + + + +#### 任务一 : 缺页中断实例的完善(编程) #### + +任务描述: + + + **在**"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物理地址的定义: + +fig5_1 + +图5.1 RISCV64 物理地址 + +​可以看到,物理地址由PPN(物理页号)与Offset(偏移量)组成。这里的PPN就对应着上述的物理页。 + +​现在,我们来看RISCV虚拟地址的定义: + +fig5_2 + +图5.2 RISCV64 虚拟地址 + +​ 可以看到虚拟地址同样由页号和偏移量组成。而这二者之间是如何转换的呢?RV64支持多种分页方案,如Sv32、Sv39、Sv48,它们的原理相似,这里我们对pke中所使用的Sv39进行讲述。Sv39中维护着一个三级的页表,其页表项定义如下: + + fig1_7 + +图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); +``` + + + + fig5_4 + + 图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则对应着各级VPN,pt_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即虚拟地址的位数为39,RISCV_PGSHIFT即代表虚拟地址中Offset的位数,二者相减,剩下的就是VPN0、VPN1……VPNX的位数,在除以VPN的位数,得到就是VPN的数量。由于pke中式Sv39,故而VPN的数量为3,即VPN0、VPN1、VPN2。 + +接着我们使用pt_idx函数得到各级VPN的值,依据图5.2所示逐级查询,一直找到该虚拟地址对应的页表项,而该页表项中存着该虚拟地址所对应的物理页号,再加上虚拟地址中的偏离量,我们就可以找到最终的物理地址了!! + + + +**5.2.3** **缺页异常处理** + +``` + 1 #include + 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函数。 + 以上,就是本次实验需要大家完成的部分了! \ No newline at end of file diff --git a/chapter6.md b/chapter6.md index d5be5b7..26132c3 100644 --- a/chapter6.md +++ b/chapter6.md @@ -1,463 +1,488 @@ -## 第六章.(实验5)进程的封装 - -### 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可以正常运行。 - - - -**6.1.1 练习一:alloc_proc(需要编程)** - -完善"pk/proc.c"中的alloc_proc(),你需要对以下属性进行初始化: - -l enum proc_state state; - -l int pid; - -l int runs; - -l uintptr_t kstack; - -l volatile bool need_resched; - -l struct proc_struct *parent; - -l struct mm_struct *mm; - -l struct context context; - -l struct trapframe *tf; - -l uintptr_t pagetable; - -l uint32_t flags; - -l char name[PROC_NAME_LEN + 1]; - - - -**6.1.2 练习二:do_fork(需要编程)** - -完善"pk/proc.c"中的do_fork函数,你需要进行以下工作: - -l 调用alloc_proc()来为子进程创建进程控制块 - -l 调用setup_kstack来设置栈空间 - -l 用copy_mm来拷贝页表 - -l 调用copy_thread来拷贝进程 - -l 为子进程设置pid - -l 设置子进程状态为就绪 - -l 将子进程加入到链表中 - - - -完成以上代码后,你可以进行如下测试,然后输入如下命令: - -`$ riscv64-unknown-elf-gcc ../app/app5.c -o ../app/elf/app5` - -`$ spike ./obj/pke app/elf/app5` - -预期的输出如下: - -``` -PKE IS RUNNING -page fault vaddr:0x00000000000100c2 -page fault vaddr:0x000000000001e17f -page fault vaddr:0x0000000000018d5a -page fault vaddr:0x000000000001a8ba -page fault vaddr:0x000000000001d218 -page fault vaddr:0x000000007f7e8bf0 -page fault vaddr:0x0000000000014a68 -page fault vaddr:0x00000000000162ce -page fault vaddr:0x000000000001c6e0 -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 -page fault vaddr:0x000000000001d030 -page fault vaddr:0x0000000000014a68 -page fault vaddr:0x00000000000162ce -page fault vaddr:0x000000000001c6e0 -page fault vaddr:0x0000000000012572 -page fault vaddr:0x0000000000011fa6 -page fault vaddr:0x0000000000019064 -page fault vaddr:0x000000000001abb6 -page fault vaddr:0x0000000000015304 -page fault vaddr:0x0000000000017fd4 -page fault vaddr:0x0000000000018cd4 -this is child process;my pid = 2 -sys_exit pid=2 -``` - -如果你的app可以正确输出的话,那么运行检查的python脚本: - -`./pke-lab5` - -若得到如下输出,那么恭喜你,你已经成功完成了实验六!!! - - - -``` -build pk : OK -running app5 : OK - test fork : OK -Score: 20/20 -``` - -### 6.2 基础知识 - -**6.2.1 进程结构** - - 在pk/proc.h中,我们定义进程的结构如下: - -``` - 42 struct proc_struct { - 43 enum proc_state state; - 44 int pid; - 45 int runs; - 46 uintptr_t kstack; - 47 volatile bool need_resched; - 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; - 56 list_entry_t hash_link; - 57 }; -``` - -​ 可以看到在41行的枚举中,我们定义了进程的四种状态,其定义如下: - -``` - 11 enum proc_state { - 12 PROC_UNINIT = 0, - 13 PROC_SLEEPING, - 14 PROC_RUNNABLE, - 15 PROC_ZOMBIE, - 16 }; -``` - -​ 四种状态分别为未初始化(PROC_UNINIT)、睡眠(PROC_SLEEPING)、可运行(PROC_RUNNABLE)以及僵死(PROC_ZOMBIE)状态。 - -​ 除却状态,进程还有以下重要属性: - -l pid:进程id,是进程的标识符 - -l runs:进程已经运行的时间 - -l kstack:进程的内核栈空间 - -l need_resched:是否需要释放CPU - -l parent:进程的父进程 - -l context:进程的上下文 - -l tf:当前中断的栈帧 - -l pagetable:进程的页表地址 - -l name:进程名 - -除了上述属性,可以看到在55、56行还维护了两个进程的链表,这是操作系统内进程的组织方式,系统维护一个进程链表,以组织要管理的进程。 - - - -**6.2.2 设置第一个内核进程idleproc** - - 在"pk/pk.c"的rest_of_boot_loader函数中调用了proc_init来设置第一个内核进程: - -``` -317 void -318 proc_init() { -319 int i; -320 extern uintptr_t kernel_stack_top; -321 -322 list_init(&proc_list); -323 for (i = 0; i < HASH_LIST_SIZE; i ++) { -324 list_init(hash_list + i); -325 } -326 -327 if ((idleproc = alloc_proc()) == NULL) { -328 panic("cannot alloc idleproc.\n"); -329 } -330 -331 idleproc->pid = 0; -332 idleproc->state = PROC_RUNNABLE; -333 idleproc->kstack = kernel_stack_top; -334 idleproc->need_resched = 1; -335 set_proc_name(idleproc, "idle"); -336 nr_process ++; -337 -338 currentproc = idleproc; -339 -340 } -``` - -​ 322行的proc_list是系统所维护的进程链表,324行的hash_list是一个大小为1024的list_entry_t的hash数组。在对系统所维护的两个list都初始化完成后,系统为idleproc分配进程结构体。然后对idleproc的各个属性进行设置,最终将currentproc改为idleproc。 - -​ 在上述代码中,我们只是为idleproc分配了进程控制块,但并没有切换到idleproc,真正的切换代码在proc_init函数后面的run_loaded_program以及cpu_idle函数中进行。 - - - -**6.2.3 do_fork** - -​ 在run_loaded_program中有如下代码: - -``` -140 trapframe_t tf; -141 init_tf(&tf, current.entry, stack_top); -142 __clear_cache(0, 0); -143 do_fork(0,stack_top,&tf); -144 write_csr(sscratch, kstack_top); -``` - -​ 在这里,声明了一个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来拷贝进程。现在,我们来对以上函数进行分析。 - -​ setup_kstack函数代码如下,在函数中,我们为进程分配栈空间,然后返回: - -``` -210 static int -211 setup_kstack(struct proc_struct *proc) { -212 proc->kstack = (uintptr_t)__page_alloc(); -213 return 0; -214 } -``` - -copy_mm k函数代码如下,在函数中,我们对页表进行拷贝。 - -``` -228 static int -229 copy_mm(uint32_t clone_flags, struct proc_struct *proc) { -230 //assert(currentproc->mm == NULL); -231 /* do nothing in this project */ -232 uintptr_t pagetable=(uintptr_t)__page_alloc(); -233 memcpy((void *)pagetable,(void *)proc->pagetable,RISCV_PGSIZE); -234 proc->pagetable=pagetable; -235 return 0; -236 } -``` - -​ 最后是copy_thread函数: - -``` -240 static void -241 copy_thread(struct proc_struct *proc, uintptr_t esp, trapframe_t *tf) { -242 proc->tf = (trapframe_t *)(proc->kstack + KSTACKSIZE - sizeof(trapframe_t)); -243 *(proc->tf) = *tf; -244 -245 proc->tf->gpr[10] = 0; -246 proc->tf->gpr[2] = (esp == 0) ? (uintptr_t)proc->tf -4 : esp; -247 -248 proc->context.ra = (uintptr_t)forkret; -249 proc->context.sp = (uintptr_t)(proc->tf); -250 } -``` - -​ 在函数中,首先对传入的栈帧进行拷贝,并且将上下文中的ra设置为地址forkret,将sp设置为该栈帧。 - -​ 完成以上几步后,我们为子进程设置pid,将其加入到进程链表当中,并且设置其状态为就绪。 - -​ - -**6.2.3 上下文切换** - -​ 每个进程都有着自己的上下文,在进程间切换时,需要对上下文一并切换。 - -​ 在pk/proc.c的cpu_idle中有以下代码: - -``` -374 void -375 cpu_idle(void) { -376 while (1) { -377 if (currentproc->need_resched) { -378 schedule(); -379 } -380 } -381 } -``` - -​ 在当前进程处于need_resched状态时,会执行调度算法schedule,其代码如下: - -``` - 16 void - 17 schedule(void) { - 18 list_entry_t *le, *last; - 19 struct proc_struct *next = NULL; - 20 { - 21 currentproc->need_resched = 0; - 22 last = (currentproc == idleproc) ? &proc_list : &(currentproc->list_link); - 23 le = last; - 24 do { - 25 if ((le = list_next(le)) != &proc_list) { - 26 next = le2proc(le, list_link); - 27 if (next->state == PROC_RUNNABLE) { - 28 break; - 29 } - 30 } - 31 } while (le != last); - 32 if (next == NULL || next->state != PROC_RUNNABLE) { - 33 next = idleproc; - 34 } - 35 next->runs ++; - 36 if (next != currentproc) { - 37 proc_run(next); - 38 } - 39 } - 40 } -``` - -​ 在schedule函数中找到下一个需要执行的进程,并执行,执行代码proc_run如下: - -``` -145 void -146 proc_run(struct proc_struct *proc) { -147 if (proc != currentproc) { -148 bool intr_flag; -149 struct proc_struct *prev = currentproc, *next = proc; -150 currentproc = proc; -151 write_csr(sptbr, ((uintptr_t)next->pagetable >> RISCV_PGSHIFT) | SATP_MODE_CHOICE); -152 switch_to(&(prev->context), &(next->context)); -153 -154 } -155 } -``` - -​ 当传入的proc不为当前进程时,执行切换操作: - -``` -7 switch_to: - 8 # save from's registers - 9 STORE ra, 0*REGBYTES(a0) - 10 STORE sp, 1*REGBYTES(a0) - 11 STORE s0, 2*REGBYTES(a0) - 12 STORE s1, 3*REGBYTES(a0) - 13 STORE s2, 4*REGBYTES(a0) - 14 STORE s3, 5*REGBYTES(a0) - 15 STORE s4, 6*REGBYTES(a0) - 16 STORE s5, 7*REGBYTES(a0) - 17 STORE s6, 8*REGBYTES(a0) - 18 STORE s7, 9*REGBYTES(a0) - 19 STORE s8, 10*REGBYTES(a0) - 20 STORE s9, 11*REGBYTES(a0) - 21 STORE s10, 12*REGBYTES(a0) - 22 STORE s11, 13*REGBYTES(a0) - 23 - 24 # restore to's registers - 25 LOAD ra, 0*REGBYTES(a1) - 26 LOAD sp, 1*REGBYTES(a1) - 27 LOAD s0, 2*REGBYTES(a1) - 28 LOAD s1, 3*REGBYTES(a1) - 29 LOAD s2, 4*REGBYTES(a1) - 30 LOAD s3, 5*REGBYTES(a1) - 31 LOAD s4, 6*REGBYTES(a1) - 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) - 37 LOAD s10, 12*REGBYTES(a1) - 38 LOAD s11, 13*REGBYTES(a1) - 39 - 40 ret -``` - -​ 可以看到,在switch_to中,我们正真执行了上一个进程的上下文保存,以及下一个进程的上下文加载。在switch_to的最后一行,我们执行ret指令,该指令是一条从子过程返回的伪指令,会将pc设置为x1(ra)寄存器的值,还记得我们在copy_thread中层将ra设置为forkret嘛?现在程序将从forkret继续执行: - -``` -160 static void -161 forkret(void) { -162 extern elf_info current; -163 load_elf(current.file_name,¤t); -164 -165 int pid=currentproc->pid; -166 struct proc_struct * proc=find_proc(pid); -167 write_csr(sscratch, proc->tf); -168 set_csr(sstatus, SSTATUS_SUM | SSTATUS_FS); -169 currentproc->tf->status = (read_csr(sstatus) &~ SSTATUS_SPP &~ SSTATUS_SIE) | SSTATUS_SPIE; -170 forkrets(currentproc->tf); -171 } -``` - - - -​ 我们进入forkrets: - -``` -121 forkrets: -122 # set stack to this new process's trapframe -123 move sp, a0 -124 addi sp,sp,320 -125 csrw sscratch,sp -126 j start_user -``` - - - - - -``` - 76 .globl start_user - 77 start_user: - 78 LOAD t0, 32*REGBYTES(a0) - 79 LOAD t1, 33*REGBYTES(a0) - 80 csrw sstatus, t0 - 81 csrw sepc, t1 - 82 - 83 # restore x registers - 84 LOAD x1,1*REGBYTES(a0) - 85 LOAD x2,2*REGBYTES(a0) - 86 LOAD x3,3*REGBYTES(a0) - 87 LOAD x4,4*REGBYTES(a0) - 88 LOAD x5,5*REGBYTES(a0) - 89 LOAD x6,6*REGBYTES(a0) - 90 LOAD x7,7*REGBYTES(a0) - 91 LOAD x8,8*REGBYTES(a0) - 92 LOAD x9,9*REGBYTES(a0) - 93 LOAD x11,11*REGBYTES(a0) - 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) -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,程序就此由内核切换至用户程序执行!! - +# 第六章.(实验5)进程的封装 + +## 6.1 实验内容 + +#### 应用: #### + +app5.c源文件如下: + +int main(){ + + + if(fork() == 0) { + printf("this is child process;my pid = %d\n",getpid()); + }else { + printf("this is farther process;my pid = %d\n",getpid()); + } + + return 0; +} + +以上代码中,进行了fork调用,其执行过程将fork出一个子进程。 + + + + +#### 任务一 : alloc_proc(编程) #### + +任务描述: + + +完善"pk/proc.c"中的alloc_proc(),你需要对以下属性进行初始化: + +l enum proc_state state; + +l int pid; + +l int runs; + +l uintptr_t kstack; + +l volatile bool need_resched; + +l struct proc_struct *parent; + +l struct mm_struct *mm; + +l struct context context; + +l struct trapframe *tf; + +l uintptr_t pagetable; + +l uint32_t flags; + +l char name[PROC_NAME_LEN + 1]; +``` + + + + + +#### 任务二 : do_fork(编程) #### + +任务描述: + + +l 调用alloc_proc()来为子进程创建进程控制块 + +l 调用setup_kstack来设置栈空间 + +l 用copy_mm来拷贝页表 + +l 调用copy_thread来拷贝进程 + +l 为子进程设置pid + +l 设置子进程状态为就绪 + +l 将子进程加入到链表中 + + + +预期输出: + + +完成以上代码后,你可以进行如下测试,然后输入如下命令: + +`$ riscv64-unknown-elf-gcc ../app/app5.c -o ../app/elf/app5` + +`$ spike ./obj/pke app/elf/app5` + +预期的输出如下: + +``` +PKE IS RUNNING +page fault vaddr:0x00000000000100c2 +page fault vaddr:0x000000000001e17f +page fault vaddr:0x0000000000018d5a +page fault vaddr:0x000000000001a8ba +page fault vaddr:0x000000000001d218 +page fault vaddr:0x000000007f7e8bf0 +page fault vaddr:0x0000000000014a68 +page fault vaddr:0x00000000000162ce +page fault vaddr:0x000000000001c6e0 +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 +page fault vaddr:0x000000000001d030 +page fault vaddr:0x0000000000014a68 +page fault vaddr:0x00000000000162ce +page fault vaddr:0x000000000001c6e0 +page fault vaddr:0x0000000000012572 +page fault vaddr:0x0000000000011fa6 +page fault vaddr:0x0000000000019064 +page fault vaddr:0x000000000001abb6 +page fault vaddr:0x0000000000015304 +page fault vaddr:0x0000000000017fd4 +page fault vaddr:0x0000000000018cd4 +this is child process;my pid = 2 +sys_exit pid=2 +``` + +如果你的app可以正确输出的话,那么运行检查的python脚本: + +`./pke-lab5` + +若得到如下输出,那么恭喜你,你已经成功完成了实验六!!! + + + +``` +build pk : OK +running app5 : OK + test fork : OK +Score: 20/20 +``` + +## 6.2 实验指导 + +**6.2.1 进程结构** + + 在pk/proc.h中,我们定义进程的结构如下: + +``` + 42 struct proc_struct { + 43 enum proc_state state; + 44 int pid; + 45 int runs; + 46 uintptr_t kstack; + 47 volatile bool need_resched; + 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; + 56 list_entry_t hash_link; + 57 }; +``` + +​ 可以看到在41行的枚举中,我们定义了进程的四种状态,其定义如下: + +``` + 11 enum proc_state { + 12 PROC_UNINIT = 0, + 13 PROC_SLEEPING, + 14 PROC_RUNNABLE, + 15 PROC_ZOMBIE, + 16 }; +``` + +​ 四种状态分别为未初始化(PROC_UNINIT)、睡眠(PROC_SLEEPING)、可运行(PROC_RUNNABLE)以及僵死(PROC_ZOMBIE)状态。 + +​ 除却状态,进程还有以下重要属性: + +l pid:进程id,是进程的标识符 + +l runs:进程已经运行的时间 + +l kstack:进程的内核栈空间 + +l need_resched:是否需要释放CPU + +l parent:进程的父进程 + +l context:进程的上下文 + +l tf:当前中断的栈帧 + +l pagetable:进程的页表地址 + +l name:进程名 + +除了上述属性,可以看到在55、56行还维护了两个进程的链表,这是操作系统内进程的组织方式,系统维护一个进程链表,以组织要管理的进程。 + + + +**6.2.2 设置第一个内核进程idleproc** + + 在"pk/pk.c"的rest_of_boot_loader函数中调用了proc_init来设置第一个内核进程: + +``` +317 void +318 proc_init() { +319 int i; +320 extern uintptr_t kernel_stack_top; +321 +322 list_init(&proc_list); +323 for (i = 0; i < HASH_LIST_SIZE; i ++) { +324 list_init(hash_list + i); +325 } +326 +327 if ((idleproc = alloc_proc()) == NULL) { +328 panic("cannot alloc idleproc.\n"); +329 } +330 +331 idleproc->pid = 0; +332 idleproc->state = PROC_RUNNABLE; +333 idleproc->kstack = kernel_stack_top; +334 idleproc->need_resched = 1; +335 set_proc_name(idleproc, "idle"); +336 nr_process ++; +337 +338 currentproc = idleproc; +339 +340 } +``` + +​ 322行的proc_list是系统所维护的进程链表,324行的hash_list是一个大小为1024的list_entry_t的hash数组。在对系统所维护的两个list都初始化完成后,系统为idleproc分配进程结构体。然后对idleproc的各个属性进行设置,最终将currentproc改为idleproc。 + +​ 在上述代码中,我们只是为idleproc分配了进程控制块,但并没有切换到idleproc,真正的切换代码在proc_init函数后面的run_loaded_program以及cpu_idle函数中进行。 + + + +**6.2.3 do_fork** + +​ 在run_loaded_program中有如下代码: + +``` +140 trapframe_t tf; +141 init_tf(&tf, current.entry, stack_top); +142 __clear_cache(0, 0); +143 do_fork(0,stack_top,&tf); +144 write_csr(sscratch, kstack_top); +``` + +​ 在这里,声明了一个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来拷贝进程。现在,我们来对以上函数进行分析。 + +​ setup_kstack函数代码如下,在函数中,我们为进程分配栈空间,然后返回: + +``` +210 static int +211 setup_kstack(struct proc_struct *proc) { +212 proc->kstack = (uintptr_t)__page_alloc(); +213 return 0; +214 } +``` + +copy_mm k函数代码如下,在函数中,我们对页表进行拷贝。 + +``` +228 static int +229 copy_mm(uint32_t clone_flags, struct proc_struct *proc) { +230 //assert(currentproc->mm == NULL); +231 /* do nothing in this project */ +232 uintptr_t pagetable=(uintptr_t)__page_alloc(); +233 memcpy((void *)pagetable,(void *)proc->pagetable,RISCV_PGSIZE); +234 proc->pagetable=pagetable; +235 return 0; +236 } +``` + +​ 最后是copy_thread函数: + +``` +240 static void +241 copy_thread(struct proc_struct *proc, uintptr_t esp, trapframe_t *tf) { +242 proc->tf = (trapframe_t *)(proc->kstack + KSTACKSIZE - sizeof(trapframe_t)); +243 *(proc->tf) = *tf; +244 +245 proc->tf->gpr[10] = 0; +246 proc->tf->gpr[2] = (esp == 0) ? (uintptr_t)proc->tf -4 : esp; +247 +248 proc->context.ra = (uintptr_t)forkret; +249 proc->context.sp = (uintptr_t)(proc->tf); +250 } +``` + +​ 在函数中,首先对传入的栈帧进行拷贝,并且将上下文中的ra设置为地址forkret,将sp设置为该栈帧。 + +​ 完成以上几步后,我们为子进程设置pid,将其加入到进程链表当中,并且设置其状态为就绪。 + +​ + +**6.2.3 上下文切换** + +​ 每个进程都有着自己的上下文,在进程间切换时,需要对上下文一并切换。 + +​ 在pk/proc.c的cpu_idle中有以下代码: + +``` +374 void +375 cpu_idle(void) { +376 while (1) { +377 if (currentproc->need_resched) { +378 schedule(); +379 } +380 } +381 } +``` + +​ 在当前进程处于need_resched状态时,会执行调度算法schedule,其代码如下: + +``` + 16 void + 17 schedule(void) { + 18 list_entry_t *le, *last; + 19 struct proc_struct *next = NULL; + 20 { + 21 currentproc->need_resched = 0; + 22 last = (currentproc == idleproc) ? &proc_list : &(currentproc->list_link); + 23 le = last; + 24 do { + 25 if ((le = list_next(le)) != &proc_list) { + 26 next = le2proc(le, list_link); + 27 if (next->state == PROC_RUNNABLE) { + 28 break; + 29 } + 30 } + 31 } while (le != last); + 32 if (next == NULL || next->state != PROC_RUNNABLE) { + 33 next = idleproc; + 34 } + 35 next->runs ++; + 36 if (next != currentproc) { + 37 proc_run(next); + 38 } + 39 } + 40 } +``` + +​ 在schedule函数中找到下一个需要执行的进程,并执行,执行代码proc_run如下: + +``` +145 void +146 proc_run(struct proc_struct *proc) { +147 if (proc != currentproc) { +148 bool intr_flag; +149 struct proc_struct *prev = currentproc, *next = proc; +150 currentproc = proc; +151 write_csr(sptbr, ((uintptr_t)next->pagetable >> RISCV_PGSHIFT) | SATP_MODE_CHOICE); +152 switch_to(&(prev->context), &(next->context)); +153 +154 } +155 } +``` + +​ 当传入的proc不为当前进程时,执行切换操作: + +``` +7 switch_to: + 8 # save from's registers + 9 STORE ra, 0*REGBYTES(a0) + 10 STORE sp, 1*REGBYTES(a0) + 11 STORE s0, 2*REGBYTES(a0) + 12 STORE s1, 3*REGBYTES(a0) + 13 STORE s2, 4*REGBYTES(a0) + 14 STORE s3, 5*REGBYTES(a0) + 15 STORE s4, 6*REGBYTES(a0) + 16 STORE s5, 7*REGBYTES(a0) + 17 STORE s6, 8*REGBYTES(a0) + 18 STORE s7, 9*REGBYTES(a0) + 19 STORE s8, 10*REGBYTES(a0) + 20 STORE s9, 11*REGBYTES(a0) + 21 STORE s10, 12*REGBYTES(a0) + 22 STORE s11, 13*REGBYTES(a0) + 23 + 24 # restore to's registers + 25 LOAD ra, 0*REGBYTES(a1) + 26 LOAD sp, 1*REGBYTES(a1) + 27 LOAD s0, 2*REGBYTES(a1) + 28 LOAD s1, 3*REGBYTES(a1) + 29 LOAD s2, 4*REGBYTES(a1) + 30 LOAD s3, 5*REGBYTES(a1) + 31 LOAD s4, 6*REGBYTES(a1) + 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) + 37 LOAD s10, 12*REGBYTES(a1) + 38 LOAD s11, 13*REGBYTES(a1) + 39 + 40 ret +``` + +​ 可以看到,在switch_to中,我们正真执行了上一个进程的上下文保存,以及下一个进程的上下文加载。在switch_to的最后一行,我们执行ret指令,该指令是一条从子过程返回的伪指令,会将pc设置为x1(ra)寄存器的值,还记得我们在copy_thread中层将ra设置为forkret嘛?现在程序将从forkret继续执行: + +``` +160 static void +161 forkret(void) { +162 extern elf_info current; +163 load_elf(current.file_name,¤t); +164 +165 int pid=currentproc->pid; +166 struct proc_struct * proc=find_proc(pid); +167 write_csr(sscratch, proc->tf); +168 set_csr(sstatus, SSTATUS_SUM | SSTATUS_FS); +169 currentproc->tf->status = (read_csr(sstatus) &~ SSTATUS_SPP &~ SSTATUS_SIE) | SSTATUS_SPIE; +170 forkrets(currentproc->tf); +171 } +``` + + + +​ 我们进入forkrets: + +``` +121 forkrets: +122 # set stack to this new process's trapframe +123 move sp, a0 +124 addi sp,sp,320 +125 csrw sscratch,sp +126 j start_user +``` + + + + + +``` + 76 .globl start_user + 77 start_user: + 78 LOAD t0, 32*REGBYTES(a0) + 79 LOAD t1, 33*REGBYTES(a0) + 80 csrw sstatus, t0 + 81 csrw sepc, t1 + 82 + 83 # restore x registers + 84 LOAD x1,1*REGBYTES(a0) + 85 LOAD x2,2*REGBYTES(a0) + 86 LOAD x3,3*REGBYTES(a0) + 87 LOAD x4,4*REGBYTES(a0) + 88 LOAD x5,5*REGBYTES(a0) + 89 LOAD x6,6*REGBYTES(a0) + 90 LOAD x7,7*REGBYTES(a0) + 91 LOAD x8,8*REGBYTES(a0) + 92 LOAD x9,9*REGBYTES(a0) + 93 LOAD x11,11*REGBYTES(a0) + 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) +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,程序就此由内核切换至用户程序执行!! + ​ \ No newline at end of file