From c48981d84045d4aa940c6bde6b41035553257682 Mon Sep 17 00:00:00 2001 From: liguo <2925441676@qq.com> Date: Sun, 25 Feb 2024 20:38:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AC=AC=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E6=8C=91=E6=88=98=E5=AE=9E=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter3_traps.md | 125 ++++++++++++++- chapter4_memory.md | 207 +++++++++++++++++++++++- chapter5_process.md | 163 +++++++++++++++++++ chapter6_filesystem.md | 349 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 841 insertions(+), 3 deletions(-) diff --git a/chapter3_traps.md b/chapter3_traps.md index d19cc1e..65d70d1 100644 --- a/chapter3_traps.md +++ b/chapter3_traps.md @@ -29,6 +29,11 @@ - [给定应用](#lab1_challenge2_app) - [实验内容](#lab1_challenge2_content) - [实验指导](#lab1_challenge2_guide) +- [3.7 lab1_challenge3 多核启动及运行(难度:★★★★☆)](#lab1_challenge3_multicore) + - [给定应用](#lab1_challenge3_app) + - [实验内容](#lab1_challenge3_content) + - [实验指导](#lab1_challenge3_guide) + @@ -1876,4 +1881,122 @@ $ git merge lab1_3_irq -m "continue to work on lab1_challenge1" * 在适当的位置调用debug_line段解析函数,对调试信息进行解析,构造指令地址-源代码行号-源代码文件名的对应表,注意,连续行号对应的不一定是连续的地址,因为一条源代码可以对应多条指令。 * 在异常中断处理函数中,通过相应寄存器找到触发异常的指令地址,然后在上述表中查找地址对应的源代码行号和文件名输出 -**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** \ No newline at end of file +**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** + + + + + +## 3.7 lab1_challenge3 多核启动及运行(难度:★★★★☆) + +之前的实验都在单核环境下进行。在本次实验中,你需要修改操作系统内核使其支持两核并发运行,并且在每个核上加载一个程序运行,等到两个程序都执行完毕后退出并关闭模拟器。 + + + +#### 给定应用 + +- user/app0.c + +```c +#include "user_lib.h" +#include "util/types.h" + +int main(void) { + printu(">>> app0 is expected to be executed by hart0\n"); + exit(0); +} +``` + +- user/app1.c + +```c +#include "user_lib.h" +#include "util/types.h" + +int main(void) { + printu(">>> app1 is expected to be executed by hart1\n"); + exit(0); +} +``` + +在本次实验中,给定两个简单的用户程序,每个程序会输出一句话。你需要让每个核分别加载一个程序,并能够正确运行,输出相应内容然后退出。 + + + +#### 实验内容 + +本实验为挑战实验,基础代码将继承和使用lab1_3完成后的代码: + +- 先提交lab1_3的答案,然后切换到lab1_challenge3_multicore、继承**lab1_3**(注意,不是继承lab1_challenge1_backtrace和lab1_challenge2_errorline!**PKE的挑战实验之间无继承关联**)中所做修改: + + +```bash +//切换到lab1_challenge3_multicore +$ git checkout lab1_challenge3_multicore + +//继承lab1_3以及之前的答案 +$ git merge lab1_3_irq -m "continue to work on lab1_challenge3" +``` + +注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。** 同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。 + +在RISC-V处理器中,每一个CPU称作一个hart(hardware thread),并从0开始编号。此实验要求启动两个CPU,它们的编号分别为0和1。 + +- 本实验中,你需要修改内核代码,使得riscv-pke能够通过spike启动两个CPU(hart),并且让CPU0执行app0,让CPU1执行app1。 +- `user0.lds`和`user1.lds`中规定了app0从0x81000000处开始加载,app1从0x81500000处开始加载。 +- `kernel/config.h`中的`NCPU`规定了操作系统内核支持的核数,在本实验设置为2,即需要能够通过`spike -p2 riscv-pke app0 app1`让pke正确地启动两核并发的操作系统并执行app0和app1。 +- 内核中有很多使用`sprint`输出的内容,为了分别每一条输出是哪个核执行的,你需要**在相应的sprint处添加一个hartid的输出项**。你应当思考并阅读和核数有关的内核代码,并且任意添加和修改,使得最终运行的输出对齐以下输出 + +``` +HTIF is available! +(Emulated) memory size: 2048 MB +In m_start, hartid:0 +hartid = 0: Enter supervisor mode... +hartid = 0: Application: ./obj/app0 +hartid = 0: Application program entry point (virtual address): 0x0000000081000000 +In m_start, hartid:1 +hartid = 1: Enter supervisor mode... +hartid = 1: Application: ./obj/app1 +hartid = 1: Application program entry point (virtual address): 0x0000000085000000 +hartid = 1: Switch to user mode... +hartid = 0: Switch to user mode... +hardid = 1: >>> app1 is executed by hart1 +hardid = 0: >>> app0 is executed by hart0 +hartid = 1: User exit with code:0. +hartid = 0: User exit with code:0. +hartid = 0: shutdown with code:0. +System is shutting down with exit code 0. +``` + + + +#### 实验指导 + + +**多核riscv-pke的启动与运行** + +spike模拟器支持`-p`选项来模拟多个核,在pke中,通过`spike -p2 riscv-pke ...`会让两个核并发地从`kernel/machine/mentry.S`中的`_mentry`开始执行,并且每个核都会如同之前单核启动一样独立对操作系统进行初始化。在硬件语境下,每一个核都是一个硬件线程hart,可以类比软件线程理解。 + +更具体地来讲,每个核从`_mentry`开始执行,然后进入`minit.c`中的`m_start`,初始化模拟器设备和接口,设置中断信息等,再进入到S模式执行`s_start`对操作系统进行初始化,使用`load_user_program`加载应用并通过`switch_to`切换至用户程序。 + +这样启动和执行会带来一些需要处理的问题 + +1. spike模拟器通过HTIF与host机器的物理设备进行交互,在pke启动时,会在M mode对spike模拟器的一些虚拟设备和HTIF接口进行初始化。这个初始化的过程只能被执行一次,而非每个核都执行一次。并且在spike和HTIF初始化完成之前,需要用**同步机制**保证每个核都不会访问他们相应的资源。 +2. 每个核的时钟周期和时钟中断应该是独立的,不能受其他核的干扰。即你需要分开管理每个hart上运行应用的`tick`。 +3. 单核实验中,pke会从命令行第一个参数加载应用程序,但是本次实验需要从命令行前两个参数加载两个程序。你需要阅读并修改`elf.c`来让pke能从命令行加载第二个app。 +4. 之前的实验在单核的基础上开展,且没有内存管理机制,所以user app使用到的一些内存地址是固定的(见`kernel/config.h`)。即使实验基础代码在elf文件中为两个app指定了不同的起始地址,两个核同时加载并运行的它们时,其部分内存地址也会重叠并出错。你应当想办法分离两个app的内存。 +5. 在之前的单核实验中,有一个全局的`current`变量来指示当前正在执行的进程,以便处理中断。然而在多核环境下,每个核的中断是独立的,在处理好中断后,各个核应当恢复到其原本执行的用户app。你需要让每个核能知道他们在执行什么app。 +6. 在单核环境下,一个核的用户进程调用`exit`退出时,会立即关闭模拟器;而在多核环境下,这会强使其他核也停止工作。正确的退出方式是,同步等到所有核执行完毕之后再关闭模拟器。本次实验中,你应当让CPU0负责关闭模拟器。 + +**Tips** + +如果你对多核pke的运行原理感到困惑,可以带着以下的tips去复习操作系统以及阅读pke源码: + +1. 理解在多核场景下,什么资源是唯一的,什么资源是多份的。唯一的资源有:模拟器相关接口、内存和设备。多份的资源有:每个核的所有寄存器和状态信息。 +2. 对于唯一的资源,所有核会共享,所以需要控制并发。对于多份的资源,所有核应当独立占有,所以需要隔离资源。 + +如果你大致理解了多核pke的运行机制,以下内容可以帮助你实现对多核的支持: + +1. 对于riscv处理器,有一个寄存器`tp (thread pointer)`是专门用来保存hart相关信息的,可以利用此寄存器存储每个核关于自身的信息。 +2. `kernel/sync_utils.h`提供了一个简单的同步屏障的实现,你可以通过这个同步原语实现并发控制,或者自行设计其它同步原语。 + diff --git a/chapter4_memory.md b/chapter4_memory.md index e8e4706..688df91 100644 --- a/chapter4_memory.md +++ b/chapter4_memory.md @@ -24,11 +24,14 @@ - [给定应用](#lab2_challenge1_app) - [实验内容](#lab2_challenge1_content) - [实验指导](#lab2_challenge1_guide) - - [4.6 lab2_challenge2 堆空间管理(难度:★★★★☆)](#lab2_challenge2_singlepageheap) - [给定应用](#lab2_challenge2_app) - [实验内容](#lab2_challenge2_content) - [实验指导](#lab2_challenge2_guide) +- [4.7 lab2_challenge3 多核内存管理(难度:★★☆☆☆)](#lab2_challenge3_multicoremem) + - [给定应用](#lab2_challenge3_app) + - [实验内容](#lab2_challenge3_content) + - [实验指导](#lab2_challenge3_guide) @@ -1036,3 +1039,205 @@ $ git merge lab2_3_pagefault -m "continue to work on lab2_challenge2" **注意:本挑战的创新思路及难点就在于分配和回收策略的设计(对应malloc和free),读者应同时思考两个函数如何实现,基于紧凑性和高效率的设计目标,设计自己认为高效的分配策略。完成设计后,请读者另外编写应用,设计不同场景使用better_malloc和better_free函数,验证挑战目标以及对自己实现进行检测。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** + + + + + +## 4.7 lab2_challenge3 多核内存管理(难度:★★☆☆☆) + +在进行此实验之前,你应当完成lab1_challenge3。 + + + +#### 给定应用 + +- user/app_alloc0.c + +```c +#include "user_lib.h" +#include "util/types.h" + +#define N 5 +#define BASE 0 + +int main(void) { + void *p[N]; + + for (int i = 0; i < N; i++) { + p[i] = naive_malloc(); + int *pi = p[i]; + *pi = BASE + i; + printu("=== user alloc 0 @ vaddr 0x%x\n", p[i]); + } + + for (int i = 0; i < N; i++) { + int *pi = p[i]; + printu("=== user0: %d\n", *pi); + naive_free(p[i]); + } + + exit(0); +} + + +``` + +- user/app_alloc1.c + +```c +#include "user_lib.h" +#include "util/types.h" + +#define N 5 +#define BASE 5 + +int main(void) { + void *p[N]; + + for (int i = 0; i < N; i++) { + p[i] = naive_malloc(); + int *pi = p[i]; + *pi = BASE + i; + printu(">>> user alloc 1 @ vaddr 0x%x\n", p[i]); + } + + for (int i = 0; i < N; i++) { + int *pi = p[i]; + printu(">>> user 1: %d\n", *pi); + naive_free(p[i]); + } + + exit(0); +} + +``` + +在本次实验中,给定两个程序,每个程序会通过lab2_2实现的`naive_malloc`申请一些内存页,在内存页开始处写入一个int并打印内存页的虚拟地址。最后每个进程会打印自己写入内存页的数,并通过`naive_free`释放申请的内存页。`app_alloc0.c`会依次写入并输出`0,1,2,3,4`,`app_alloc1.c`会依次写入并输出`5,6,7,8,9`。 + + + +#### 实验内容 + +本实验为挑战实验,基础代码将继承和使用lab2_3完成后的代码: + +- 先提交lab2_3的答案,然后切换到lab2_challenge3_multicoremem,继承**lab2_3**(注意,不是继承lab2_challenge1_pagefaults和lab2_challenge2_singlepageheap!**PKE的挑战实验之间无继承关联**)中所做修改: + +```shell +//切换到lab2_challenge3_multicoremem +$ git checkout lab2_challenge3_multicoremem + +//继承lab2_3以及之前的答案 +$ git merge lab2_3_pagefault -m "continue to work on lab2_challenge3" +``` + +注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。**同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。 + +- 在lab1_challenge3中,你已经实现了一个不支持虚拟内存的简单的多核操作系统。现在在lab2中,因为虚拟内存概念的引入,需要你为这个简单操作系统添加额外的多核内存管理。 +- 如同lab1_challenge3,`kernel/config.h`中的`NCPU`规定了操作系统内核支持的核数,在本实验设置为2,即要求你能够正确执行`spike -p2 riscv-pke app_alloc0 app_alloc1`,每个核分别执行一个进程,进行内存的分配与释放。 +- 注意,每个进程分配得到的**虚拟地址应当是连续**的,并且不同核上的进程**不应该分配到同一个物理页**。 +- 最终你的输出应当如下所示: + +```text +HTIF is available! +(Emulated) memory size: 2048 MB +In m_start, hartid:0 +hartid = 0: Enter supervisor mode... +PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000a000, PKE kernel size: 0x000000000000a000 . +free physical memory address: [0x000000008000a000, 0x0000000087ffffff] +kernel memory manager is initializing ... +In m_start, hartid:1 +hartid = 1: Enter supervisor mode... +KERN_BASE 0x0000000080000000 +physical address of _etext is: 0x0000000080006000 +hartid = 0: User application is loading. +hartid = 1: User application is loading. +hartid = 0: user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000 +hartid = 0: Application: obj/app_alloc0 +hartid = 1: user frame 0x0000000087fb8000, user stack 0x000000007ffff000, user kstack 0x0000000087fb7000 +hartid = 1: Application: obj/app_alloc1 +hartid = 0: Application program entry point (virtual address): 0x00000000000100b0 +hartid = 1: Application program entry point (virtual address): 0x00000000000100b0 +hartid = 0: Switch to user mode... +hartid = 0: alloc page 0x87fa4000 +hartid = 0: alloc page 0x87fa3000 +hartid = 1: Switch to user mode... +hartid = 0: vaddr 0x00400000 is mapped to paddr 0x87fa4000 +hartid = 1: alloc page 0x87fa2000 +hartid = 1: alloc page 0x87fa1000 +=== user alloc 0 @ vaddr 0x00400000 +hartid = 1: vaddr 0x00400000 is mapped to paddr 0x87fa2000 +hartid = 0: alloc page 0x87fa0000 +hartid = 0: vaddr 0x00401000 is mapped to paddr 0x87fa0000 +>>> user alloc 1 @ vaddr 0x00400000 +=== user alloc 0 @ vaddr 0x00401000 +hartid = 1: alloc page 0x87f9f000 +hartid = 1: vaddr 0x00401000 is mapped to paddr 0x87f9f000 +hartid = 0: alloc page 0x87f9e000 +hartid = 0: vaddr 0x00402000 is mapped to paddr 0x87f9e000 +>>> user alloc 1 @ vaddr 0x00401000 +=== user alloc 0 @ vaddr 0x00402000 +hartid = 1: alloc page 0x87f9d000 +hartid = 1: vaddr 0x00402000 is mapped to paddr 0x87f9d000 +hartid = 0: alloc page 0x87f9c000 +hartid = 0: vaddr 0x00403000 is mapped to paddr 0x87f9c000 +>>> user alloc 1 @ vaddr 0x00402000 +=== user alloc 0 @ vaddr 0x00403000 +hartid = 1: alloc page 0x87f9b000 +hartid = 1: vaddr 0x00403000 is mapped to paddr 0x87f9b000 +hartid = 0: alloc page 0x87f9a000 +hartid = 0: vaddr 0x00404000 is mapped to paddr 0x87f9a000 +>>> user alloc 1 @ vaddr 0x00403000 +=== user alloc 0 @ vaddr 0x00404000 +hartid = 1: alloc page 0x87f99000 +hartid = 1: vaddr 0x00404000 is mapped to paddr 0x87f99000 +=== user0: 0 +>>> user alloc 1 @ vaddr 0x00404000 +=== user0: 1 +>>> user 1: 5 +=== user0: 2 +>>> user 1: 6 +=== user0: 3 +>>> user 1: 7 +=== user0: 4 +>>> user 1: 8 +hartid = 0: User exit with code: 0. +>>> user 1: 9 +hartid = 1: User exit with code: 0. +hartid = 0: shutdown with code: 0. +System is shutting down with exit code 0. +``` + + + +#### 实验指导 + +参照lab1_challenge3,在引入进程与内存的概念之后,需要对其内存管理进行并发控制和资源隔离。 + +在通过`s_start`进入S mode的时候,pke会对物理内存和内核页表进行初始化。如同lab1_challenge3中spike设备的初始化,这个过程也只应执行一次,并且初始化完毕后所有核才能够开启页表,继续执行之后的指令。 + +物理地址是所有核共享的,各个核可能会并发的操作物理地址,带来未知的错误。你需要实现一个互斥锁,使得同一时间只有一个核才能分配和释放物理地址。关于互斥锁的实现,你可以选择使用RISC-V原子指令`amoswap`制作一个简单的自旋锁。 + +基础代码中在分配物理页处有一行代码`sprint`,打印用户分配得到的物理页地址。`vm_alloc_stage`是用来帮助内核判断是否由用户进程在申请内存,其被初始化为0,表示一开始是内核在申请物理内存;并在`switch_to`被设置为`1`,表示是用户进程在申请物理内存。但是这个代码目前不支持多核,你需要进行相应修改使其适配多核,并能够正确打印出每个核上用户进程申请的物理页地址。 + +```c +void *alloc_page(void) { + list_node *n = g_free_mem_list.next; + uint64 hartid = 0; + if (vm_alloc_stage[hartid]) { + sprint("hartid = %ld: alloc page 0x%x\n", hartid, n); + } + if (n) g_free_mem_list.next = n->next; + return (void *)n; +} +``` + +此实验中,你需要利用`kernel/sync_utils.h`中的同步原语来管理内存,防止同一个物理内存页被两个进程同时占有。如果你没能正确控制物理内存管理的并发,可能会出现: + +1. `app_alloc0`的输出不依次为`0, 1, 2, 3, 4`,或者`app_alloc1`的输出不依次为`5, 6, 7, 8, 9` +2. 由于两个进程同时写入同一个物理页,虚拟机触发异常并导致操作系统内核崩溃 + +此外,在lab2之前的单进程实验中,虚拟地址是内核的全局变量在管理。然而在正确的实现中,每个进程的虚拟地址空间应该是隔离的。否则,在多进程情况下,每个进程的虚拟地址可能会发生一些重叠或者缺失的情况。你需要实更改单进程情况下的虚拟地址管理,使其能够支持多个进程。如果你的实现不正确,你会看见每个进程通过`naive_malloc`得到的虚拟内存页不连续。 + +在实验的基础代码中,有一些打印分配内存地址的代码,你需要将这部分代码改为支持多核执行的版本,然后便可以阅读执行后的输出来判断是否为每个进程分配得到的物理内存页和对应的虚拟内存。 + diff --git a/chapter5_process.md b/chapter5_process.md index c20c089..cdb8022 100644 --- a/chapter5_process.md +++ b/chapter5_process.md @@ -26,6 +26,10 @@ - [给定应用](#lab3_challenge2_app) - [实验内容](#lab3_challenge2_content) - [实验指导](#lab3_challenge2_guide) +- [5.7 lab3_challenge3 写时复制(Copy On Write)(难度:★★★☆☆)](#lab3_challenge3_cow) + - [给定应用](#lab3_challenge3_app) + - [实验内容](#lab3_challenge3_content) + - [实验指导](#lab3_challenge3_guide) @@ -1422,3 +1426,162 @@ $ git merge lab3_3_rrsched -m "continue to work on lab3_challenge1" **注意:完成实验内容后,请读者另外编写应用,对自己的实现进行检测。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** + + + + + +## 5.7 lab3_challenge3 写时复制(Copy On Write)(难度:★★★☆☆) + + + +#### 给定应用 + +- user/app_cow.c + +```c +/* + * This app fork a child process to read and write the heap data from parent process. + * Because implemented copy on write, when child process only read the heap data, + * the physical address is the same as the parent process. + * But after writing, child process heap will have different physical address. + */ + +#include "user/user_lib.h" +#include "util/types.h" + +int main(void) { + int *heap_data = naive_malloc(); + printu("the physical address of parent process heap is: "); + printpa(heap_data); + int pid = fork(); + if (pid == 0) { + printu("the physical address of child process heap before copy on write is: "); + printpa(heap_data); + heap_data[0] = 0; + printu("the physical address of child process heap after copy on write is: "); + printpa(heap_data); + } + exit(0); + return 0; +} +``` + +该应用执行如下操作: + +1. 在父进程的堆上申请一片区域,并输出其物理地址。 +2. 进行fork操作,输出子进程在对堆数据写入前后对应的物理地址。 + + + +#### 实验内容 + +本实验为挑战实验,基础代码将继承和使用lab3_3完成后的代码: + +- 先提交lab3_3的答案,然后)切换到lab3_challenge3_cow、继承lab3_3(注意,不是继承lab3_challenge1_wait和lab3_challenge2_semaphore!PKE的挑战实验之间无继承关联)中所做修改,并make后的直接运行结果: + + +```bash +//切换到lab3_challenge3_cow +$ git checkout lab3_challenge3_cow + +//继承lab3_3以及之前的答案 +$ git merge lab3_3_rrsched -m "continue to work on lab3_challenge3" + +$ spike obj/riscv-pke obj/app_cow +In m_start, hartid:0 +HTIF is available! +(Emulated) memory size: 2048 MB +Enter supervisor mode... +PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000b000, PKE kernel size: 0x000000000000b000 . +free physical memory address: [0x000000008000b000, 0x0000000087ffffff] +kernel memory manager is initializing ... +KERN_BASE 0x0000000080000000 +physical address of _etext is: 0x0000000080005000 +kernel page table is on +Switch to user mode... +in alloc_proc. user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000 +User application is loading. +Application: obj/app_two_long_loops +CODE_SEGMENT added at mapped info offset:4 +Application program entry point (virtual address): 0x0000000000010078 +going to insert process 0 to ready queue. +going to schedule process 0 to run. +the physical address of parent process heap is: 0000000087faf000 +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087fad000, user stack 0x000000007ffff000, user kstack 0x0000000087fac000 +do_fork map code segment at pa:0000000087fb2000 of parent to child at va:0000000000010000. +going to insert process 1 to ready queue. +User exit with code:0. +going to schedule process 1 to run. +the physical address of child process heap before copy on write is: 0000000087fa3000 +the physical address of child process heap after copy on write is: 0000000087fa3000 +User exit with code:0. +no more ready processes, system shutdown now. +System is shutting down with exit code 0. +``` + +可以看到,在fork之后,子进程的堆数据无论是否进行写入,其物理地址与父进程均不同。这是因为我们fork时直接为子进程分配了新的物理页面导致的。在实现了写时复制的fork中,是不会直接给子进程申请新的物理页面,而是当子进程执行写入操作时,才通过异常机制来分配新的物理页面,也就是说,子进程的堆数据物理地址在未执行写入前,和父进程应该是一样的。 + +注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。** 同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。 + +- 本实验要求你通过修改内核代码,使得在fork时不直接为新的堆空间分配内存,而是等写入之后才分配,即实现fork的写时复制(COW)机制。 +- 你的程序输出应该如下: + +``` +$ spike obj/riscv-pke obj/app_cow +In m_start, hartid:0 +HTIF is available! +(Emulated) memory size: 2048 MB +Enter supervisor mode... +PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008004b000, PKE kernel size: 0x000000000004b000 . +free physical memory address: [0x000000008004b000, 0x0000000087ffffff] +kernel memory manager is initializing ... +KERN_BASE 0x0000000080000000 +physical address of _etext is: 0x0000000080005000 +kernel page table is on +Switch to user mode... +in alloc_proc. user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000 +User application is loading. +Application: obj/app_cow +CODE_SEGMENT added at mapped info offset:4 +Application program entry point (virtual address): 0x0000000000010078 +going to insert process 0 to ready queue. +going to schedule process 0 to run. +the physical address of parent process heap is: 0000000087faf000 +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087fad000, user stack 0x000000007ffff000, user kstack 0x0000000087fac000 +do_fork map code segment at pa:0000000087fb2000 of parent to child at va:0000000000010000. +going to insert process 1 to ready queue. +User exit with code:0. +going to schedule process 1 to run. +the physical address of child process heap before copy on write is: 0000000087faf000 +handle_page_fault: 0000000000400000 +the physical address of child process heap after copy on write is: 0000000087fa0000 +User exit with code:0. +no more ready processes, system shutdown now. +System is shutting down with exit code 0. +``` + +可以看到,输出里多出了处理异常和cow验证输出的语句。 + + + +#### 实验指导 + +写时复制是一种非常常见的优化手段。在Shell中,我们经常fork完一个进程之后会立即调用exec来将子进程替换为新的进程。如果我们在fork时将父进程的所有地址空间全部复制给子进程,而子进程在exec后做的第一件事情是丢弃这个地址空间,也就是我们做了一些无用的复制操作,产生了额外的开销。所以我们可以用“先映射但不实际复制”的方式来优化上述提到的开销,只有子进程真正需要写入映射到父进程的地址空间时,我们才真正的执行“申请空间-复制”这一操作,这就是写时复制(Copy on Write,COW)技术。 + +- 为完成该挑战,你需要对pke中进程空间有更详细的了解,判断哪些地址范围可能会被访问。 +- 你可以参考Linux或其他主流操作系统中写时复制机制的实现。 +- 你对内核代码的修改可能包含以下内容: + - 在堆段的复制发生时,实现”先映射但不实际复制“。 + - 对页表项的标志位进行修改。当子进程拥有映射到父进程的地址的页表项时,页表项应该具有哪些权限?当COW发生后,页表项又该具有哪些权限? + - 内核如何分辨现在是一个copy-on-write fork的场景?**提示:PTE的标志位中有两位RSW是没有使用的。** + - 如果有父进程fork了多个子进程,意味着该父进程的物理页会被多个子进程”共享“。举个例子,当父进程退出时我们需要更加的小心,因为我们要判断是否能立即释放相应的物理页。如果有子进程还在使用这些物理页,而内核又释放了这些物理页,将会出现问题。该“共享”页在什么条件下才能释放? + +**注意:本挑战的难点在于对页表项的控制以及对页面释放的判断,读者应思考两个问题如何解决,完成设计并验证后,读者可以继续尝试在数据段也加入写时复制机制。** + +**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** + diff --git a/chapter6_filesystem.md b/chapter6_filesystem.md index 2f11bf1..e2ff9ed 100644 --- a/chapter6_filesystem.md +++ b/chapter6_filesystem.md @@ -28,6 +28,10 @@ - [给定应用](#lab4_challenge2_app) - [实验内容](#lab4_challenge2_content) - [实验指导](#lab4_challenge2_guide) +- [6.7 lab4_challenge3 简易Shell(难度:★★★★★)](#lab4_challenge3_shell) + - [给定应用](#lab4_challenge3_app) + - [实验内容](#lab4_challenge3_content) + - [实验指导](#lab4_challenge3_guide) @@ -2348,7 +2352,7 @@ $ git commit -a -m "my work on lab4_3 is done." - 本实验的具体要求为: - - 通过修改PKE内核和系统调用,为用户程序提供exec函数的功能。exec接受一个可执行程序的路径名,以及一个调用参数数组作为参数。exec函数在执行成功时不会返回,执行失败时返回-1。 + - 通过修改PKE内核和系统调用,为用户程序提供exec函数的功能。exec接受一个可执行程序的路径名作为参数,表示要重新载入的elf文件。exec函数在执行成功时不会返回,执行失败时返回-1。 @@ -2359,3 +2363,346 @@ $ git commit -a -m "my work on lab4_3 is done." **注意:完成实验内容后,若读者同时完成了之前的“lab3_challenge1 进程等待和数据段复制”挑战实验,则可以自行对app_exec程序进行修改,将其改为一个简单的“shell”程序。该“shell”程序应该首先通过fork系统调用创建一个新的子进程,并在子进程中调用exec执行一个新的程序(如app_ls),同时父进程会调用wait系统调用等待子进程的结束,并继续执行后续代码。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** + + + +## 6.7 lab4_challenge3 简易Shell(难度:★★★★★) + + + +#### 给定应用 + +- user/app_shell.c + +```c +/* + * This app starts a very simple shell and executes some simple commands. + * The commands are stored in the hostfs_root/shellrc + * The shell loads the file and executes the command line by line. + */ +#include "user_lib.h" +#include "string.h" +#include "util/types.h" + +int main(int argc, char *argv[]) { + printu("\n======== Shell Start ========\n\n"); + int fd; + int MAXBUF = 1024; + char buf[MAXBUF]; + char *token; + char delim[3] = " \n"; + fd = open("/shellrc", O_RDONLY); + + read_u(fd, buf, MAXBUF); + close(fd); + char *command = naive_malloc(); + char *para = naive_malloc(); + int start = 0; + while (1) + { + if(!start) { + token = strtok(buf, delim); + start = 1; + } + else + token = strtok(NULL, delim); + strcpy(command, token); + token = strtok(NULL, delim); + strcpy(para, token); + if(strcmp(command, "END") == 0 && strcmp(para, "END") == 0) + break; + printu("Next command: %s %s\n\n", command, para); + printu("==========Command Start============\n\n"); + int pid = fork(); + if(pid == 0) { + int ret = exec(command, para); + if (ret == -1) + printu("exec failed!\n"); + } + else + { + wait(pid); + printu("==========Command End============\n\n"); + } + } + exit(0); + return 0; +} + +``` + +该应用程序从位于hostfs_root下的shellrc文件中读取若干条命令,对于每条命令,我们都会fork一个新的进程,并exec对应的程序来完成命令。exec将用第一个参数对应的程序来替换fork出来的新进程,并将第二个参数的内容传入新的进程。 + +给出的shellrc文件如下: + +``` +/bin/app_mkdir /RAMDISK0/sub_dir +/bin/app_touch /RAMDISK0/sub_dir/ramfile1 +/bin/app_touch /RAMDISK0/sub_dir/ramfile2 +/bin/app_echo /RAMDISK0/sub_dir/ramfile1 +/bin/app_cat /RAMDISK0/sub_dir/ramfile1 +/bin/app_ls /RAMDISK0/sub_dir +/bin/app_ls /RAMDISK0 +END END + +``` + +可以看到,我们提供了五种简化版的命令,分别是ls,mkdir,touch,echo,cat。命令的对应功能参考Linux下的命令。(echo命令的内容有所不同,为了简单起见,提供的echo命令功能为:向指定的文件写入hello world。比如shellrc中的echo命令实际上是向/RAMDISK0/sub_dir/ramfile1文件中写入了hello world) + +同样是为了简单起见,我们规定在遇到两个END时结束程序。 + +shellrc中的其余命令实现均位于user目录下,这里不一一列出,请感兴趣的读者自行查看。 + +shell程序将会依次执行上述命令,如果你完成了本实验,你应当看到的输出如下: + +``` +$ spike obj/riscv-pke /bin/app_shell +In m_start, hartid:0 +HTIF is available! +(Emulated) memory size: 2048 MB +Enter supervisor mode... +PKE kernel start 0x0000000080000000, PKE kernel end: 0x0000000080011000, PKE kernel size: 0x0000000000011000 . +free physical memory address: [0x0000000080011000, 0x0000000087ffffff] +kernel memory manager is initializing ... +KERN_BASE 0x0000000080000000 +physical address of _etext is: 0x0000000080009000 +kernel page table is on +RAMDISK0: base address of RAMDISK0 is: 0x0000000087f35000 +RFS: format RAMDISK0 done! +Switch to user mode... +in alloc_proc. user frame 0x0000000087f29000, user stack 0x000000007ffff000, user kstack 0x0000000087f28000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +User application is loading. +Application: /bin/app_shell +CODE_SEGMENT added at mapped info offset:4 +DATA_SEGMENT added at mapped info offset:5 +Application program entry point (virtual address): 0x00000000000100b0 +going to insert process 0 to ready queue. +going to schedule process 0 to run. + +======== Shell Start ======== + +Next command: /bin/app_mkdir /RAMDISK0/sub_dir + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087f0e000, user stack 0x000000007ffff000, user kstack 0x0000000087f0c000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 1 to ready queue. +going to schedule process 1 to run. +Application: /bin/app_mkdir +CODE_SEGMENT added at mapped info offset:4 +Application program entry point (virtual address): 0x0000000000010078 + +======== mkdir command ======== +mkdir: /RAMDISK0/sub_dir +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +Next command: /bin/app_touch /RAMDISK0/sub_dir/ramfile1 + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087ef3000, user stack 0x000000007ffff000, user kstack 0x0000000087ef2000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 2 to ready queue. +going to schedule process 2 to run. +Application: /bin/app_touch +CODE_SEGMENT added at mapped info offset:4 +Application program entry point (virtual address): 0x0000000000010078 + +======== touch command ======== +touch: /RAMDISK0/sub_dir/ramfile1 +file descriptor fd: 0 +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +Next command: /bin/app_touch /RAMDISK0/sub_dir/ramfile2 + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087ee8000, user stack 0x000000007ffff000, user kstack 0x0000000087ee6000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 3 to ready queue. +going to schedule process 3 to run. +Application: /bin/app_touch +CODE_SEGMENT added at mapped info offset:4 +Application program entry point (virtual address): 0x0000000000010078 + +======== touch command ======== +touch: /RAMDISK0/sub_dir/ramfile2 +file descriptor fd: 0 +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +Next command: /bin/app_echo /RAMDISK0/sub_dir/ramfile1 + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087ed1000, user stack 0x000000007ffff000, user kstack 0x0000000087ecf000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 4 to ready queue. +going to schedule process 4 to run. +Application: /bin/app_echo +CODE_SEGMENT added at mapped info offset:4 +DATA_SEGMENT added at mapped info offset:5 +Application program entry point (virtual address): 0x00000000000100b0 + +======== echo command ======== +echo: /RAMDISK0/sub_dir/ramfile1 +file descriptor fd: 0 +write content: +hello world +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +Next command: /bin/app_cat /RAMDISK0/sub_dir/ramfile1 + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087eba000, user stack 0x000000007ffff000, user kstack 0x0000000087eb8000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 5 to ready queue. +going to schedule process 5 to run. +Application: /bin/app_cat +CODE_SEGMENT added at mapped info offset:4 +Application program entry point (virtual address): 0x0000000000010078 + +======== cat command ======== +cat: /RAMDISK0/sub_dir/ramfile1 +file descriptor fd: 0 +read content: +hello world +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +Next command: /bin/app_ls /RAMDISK0/sub_dir + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087ea2000, user stack 0x000000007ffff000, user kstack 0x0000000087ea0000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 6 to ready queue. +going to schedule process 6 to run. +Application: /bin/app_ls +CODE_SEGMENT added at mapped info offset:4 +DATA_SEGMENT added at mapped info offset:5 +Application program entry point (virtual address): 0x00000000000100b0 +---------- ls command ----------- +ls "/RAMDISK0/sub_dir": +[name] [inode_num] +ramfile1 2 +ramfile2 3 +------------------------------ +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +Next command: /bin/app_ls /RAMDISK0 + +==========Command Start============ + +User call fork. +will fork a child from parent 0. +in alloc_proc. user frame 0x0000000087e8b000, user stack 0x000000007ffff000, user kstack 0x0000000087f14000 +FS: created a file management struct for a process. +in alloc_proc. build proc_file_management successfully. +do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000. +going to insert process 7 to ready queue. +going to schedule process 7 to run. +Application: /bin/app_ls +CODE_SEGMENT added at mapped info offset:4 +DATA_SEGMENT added at mapped info offset:5 +Application program entry point (virtual address): 0x00000000000100b0 +---------- ls command ----------- +ls "/RAMDISK0": +[name] [inode_num] +sub_dir 1 +------------------------------ +User exit with code:0. +going to insert process 0 to ready queue. +going to schedule process 0 to run. +==========Command End============ + +User exit with code:0. +no more ready processes, system shutdown now. +System is shutting down with exit code 0. +``` + + + +#### 实验内容 + +本实验为挑战实验,基础代码将继承和使用lab4_3_hardlink完成后的代码: + +- 先提交lab4_3_hardlink的答案,然后)切换到lab4_challenge3_shell、继承lab4_3_hardlink中所做修改: + + +```bash +//切换到lab4_challenge3_shell +$ git checkout lab4_challenge3_shell + +//继承lab4_3_hardlink以及之前的答案 +$ git merge lab4_3_hardlink -m "continue to work on lab4_challenge3" +``` + +注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。** 同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。 + +- 本实验的具体要求为: + + - 通过修改PKE内核和系统调用,为用户程序提供exec函数的功能。exec接受一个可执行程序的路径名,以及**一个字符串**作为参数。(实际上,exec系统调用的第二个参数本应是一个参数数组,即char** 类型,这里为简单起见仅需传入一个参数即可。如果读者学有余力,可以自行实现多个参数传入版本的exec) + - 此外,为了保证shell的正规性,你需要额外实现wait功能,来保证程序的执行顺序。如果你已经完成了lab3_challenge1,那么可以直接使用对应成果。 + + + +#### 实验指导 + +shell是Linux中的一个重要概念,主要用于和用户的交互。其基本流程为:用户输入命令->shell创建一个新进程,并使用exec函数加载命令对应的程序->将结果通过shell返回用户。其中,fork函数的功能已在前面的实验中实现,本实验主要实现exec函数的功能。(如果你已完成lab4_challenge2的话,对本实验会非常有帮助) + +不考虑参数传递问题的话,exec要做的工作基本为替换elf内容和重置堆栈等地址空间。但是因为存在新进程需要得知命令参数的问题,exec除了提到的工作之外,还需要考虑以下问题: + +- main函数中的argc,argv数组的含义,以及main函数是从哪里得到它们的。(**提示:main函数也是函数,函数的参数一般保存在哪里呢**) +- 在lab1中我们初步了解了系统调用,可以发现,系统调用使用了a0~a7寄存器。请读者思考:**处理系统调用的函数的返回值保存在哪里,有什么作用呢?** +- 在exec替换完elf后,我们将堆栈重置。此时的sp指向什么位置,是否可以修改以达到传递参数的效果。 +- 注意地址对齐。读者可自行参阅RISC-V的相关资料,了解该指令集下的地址对齐规则。 + +**注意:完成实验内容后,若读者同时完成了之前的“lab3_challenge3 写时复制”挑战实验,则可以将fork添加写时复制功能来减少不必要的开销。此外,本挑战实验具有很强的扩展性,读者可通过自行比对与真实shell的差别,为pke-shell添加更完善的功能。** + +**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** +