添加第三个挑战实验

pull/18/head
liguo 9 months ago
parent 7177201b01
commit c48981d840

@ -29,6 +29,11 @@
- [给定应用](#lab1_challenge2_app) - [给定应用](#lab1_challenge2_app)
- [实验内容](#lab1_challenge2_content) - [实验内容](#lab1_challenge2_content)
- [实验指导](#lab1_challenge2_guide) - [实验指导](#lab1_challenge2_guide)
- [3.7 lab1_challenge3 多核启动及运行(难度:★★★★☆](#lab1_challenge3_multicore)
- [给定应用](#lab1_challenge3_app)
- [实验内容](#lab1_challenge3_content)
- [实验指导](#lab1_challenge3_guide)
<a name="fundamental"></a> <a name="fundamental"></a>
@ -1877,3 +1882,121 @@ $ git merge lab1_3_irq -m "continue to work on lab1_challenge1"
* 在异常中断处理函数中,通过相应寄存器找到触发异常的指令地址,然后在上述表中查找地址对应的源代码行号和文件名输出 * 在异常中断处理函数中,通过相应寄存器找到触发异常的指令地址,然后在上述表中查找地址对应的源代码行号和文件名输出
**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。**
<a name="lab1_challenge3_multicore"></a>
## 3.7 lab1_challenge3 多核启动及运行(难度:&#9733;&#9733;&#9733;&#9733;&#9734;
之前的实验都在单核环境下进行。在本次实验中,你需要修改操作系统内核使其支持两核并发运行,并且在每个核上加载一个程序运行,等到两个程序都执行完毕后退出并关闭模拟器。
<a name="lab1_challenge3_app"></a>
#### 给定应用
- 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);
}
```
在本次实验中,给定两个简单的用户程序,每个程序会输出一句话。你需要让每个核分别加载一个程序,并能够正确运行,输出相应内容然后退出。
<a name="lab1_challenge3_content"></a>
#### 实验内容
本实验为挑战实验基础代码将继承和使用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.
```
<a name="lab1_challenge3_guide"></a>
#### 实验指导
**多核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`提供了一个简单的同步屏障的实现,你可以通过这个同步原语实现并发控制,或者自行设计其它同步原语。

@ -24,11 +24,14 @@
- [给定应用](#lab2_challenge1_app) - [给定应用](#lab2_challenge1_app)
- [实验内容](#lab2_challenge1_content) - [实验内容](#lab2_challenge1_content)
- [实验指导](#lab2_challenge1_guide) - [实验指导](#lab2_challenge1_guide)
- [4.6 lab2_challenge2 堆空间管理(难度:&#9733;&#9733;&#9733;&#9733;&#9734;](#lab2_challenge2_singlepageheap) - [4.6 lab2_challenge2 堆空间管理(难度:&#9733;&#9733;&#9733;&#9733;&#9734;](#lab2_challenge2_singlepageheap)
- [给定应用](#lab2_challenge2_app) - [给定应用](#lab2_challenge2_app)
- [实验内容](#lab2_challenge2_content) - [实验内容](#lab2_challenge2_content)
- [实验指导](#lab2_challenge2_guide) - [实验指导](#lab2_challenge2_guide)
- [4.7 lab2_challenge3 多核内存管理(难度:&#9733;&#9733;&#9734;&#9734;&#9734;](#lab2_challenge3_multicoremem)
- [给定应用](#lab2_challenge3_app)
- [实验内容](#lab2_challenge3_content)
- [实验指导](#lab2_challenge3_guide)
<a name="fundamental"></a> <a name="fundamental"></a>
@ -1036,3 +1039,205 @@ $ git merge lab2_3_pagefault -m "continue to work on lab2_challenge2"
**注意本挑战的创新思路及难点就在于分配和回收策略的设计对应malloc和free读者应同时思考两个函数如何实现基于紧凑性和高效率的设计目标设计自己认为高效的分配策略。完成设计后请读者另外编写应用设计不同场景使用better_malloc和better_free函数验证挑战目标以及对自己实现进行检测。** **注意本挑战的创新思路及难点就在于分配和回收策略的设计对应malloc和free读者应同时思考两个函数如何实现基于紧凑性和高效率的设计目标设计自己认为高效的分配策略。完成设计后请读者另外编写应用设计不同场景使用better_malloc和better_free函数验证挑战目标以及对自己实现进行检测。**
**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。**
<a name="lab2_challenge3_multicoremem"></a>
## 4.7 lab2_challenge3 多核内存管理(难度:&#9733;&#9733;&#9734;&#9734;&#9734;
在进行此实验之前你应当完成lab1_challenge3。
<a name="lab2_challenge3_app"></a>
#### 给定应用
- 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`。
<a name="lab2_challenge3_content"></a>
#### 实验内容
本实验为挑战实验基础代码将继承和使用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.
```
<a name="lab2_challenge3_guide"></a>
#### 实验指导
参照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`得到的虚拟内存页不连续。
在实验的基础代码中,有一些打印分配内存地址的代码,你需要将这部分代码改为支持多核执行的版本,然后便可以阅读执行后的输出来判断是否为每个进程分配得到的物理内存页和对应的虚拟内存。

@ -26,6 +26,10 @@
- [给定应用](#lab3_challenge2_app) - [给定应用](#lab3_challenge2_app)
- [实验内容](#lab3_challenge2_content) - [实验内容](#lab3_challenge2_content)
- [实验指导](#lab3_challenge2_guide) - [实验指导](#lab3_challenge2_guide)
- [5.7 lab3_challenge3 写时复制Copy On Write难度&#9733;&#9733;&#9733;&#9734;&#9734;](#lab3_challenge3_cow)
- [给定应用](#lab3_challenge3_app)
- [实验内容](#lab3_challenge3_content)
- [实验指导](#lab3_challenge3_guide)
<a name="fundamental"></a> <a name="fundamental"></a>
@ -1422,3 +1426,162 @@ $ git merge lab3_3_rrsched -m "continue to work on lab3_challenge1"
**注意:完成实验内容后,请读者另外编写应用,对自己的实现进行检测。** **注意:完成实验内容后,请读者另外编写应用,对自己的实现进行检测。**
**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。**
<a name="lab3_challenge3_cow"></a>
## 5.7 lab3_challenge3 写时复制Copy On Write难度&#9733;&#9733;&#9733;&#9734;&#9734;
<a name="lab3_challenge3_app"></a>
#### 给定应用
- 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操作输出子进程在对堆数据写入前后对应的物理地址。
<a name="lab3_challenge3_content"></a>
#### 实验内容
本实验为挑战实验基础代码将继承和使用lab3_3完成后的代码
- 先提交lab3_3的答案然后切换到lab3_challenge3_cow、继承lab3_3注意不是继承lab3_challenge1_wait和lab3_challenge2_semaphorePKE的挑战实验之间无继承关联中所做修改并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验证输出的语句。
<a name="lab3_challenge3_guide"></a>
#### 实验指导
写时复制是一种非常常见的优化手段。在Shell中我们经常fork完一个进程之后会立即调用exec来将子进程替换为新的进程。如果我们在fork时将父进程的所有地址空间全部复制给子进程而子进程在exec后做的第一件事情是丢弃这个地址空间也就是我们做了一些无用的复制操作产生了额外的开销。所以我们可以用“先映射但不实际复制”的方式来优化上述提到的开销只有子进程真正需要写入映射到父进程的地址空间时我们才真正的执行“申请空间-复制”这一操作这就是写时复制Copy on WriteCOW技术。
- 为完成该挑战你需要对pke中进程空间有更详细的了解判断哪些地址范围可能会被访问。
- 你可以参考Linux或其他主流操作系统中写时复制机制的实现。
- 你对内核代码的修改可能包含以下内容:
- 在堆段的复制发生时,实现”先映射但不实际复制“。
- 对页表项的标志位进行修改。当子进程拥有映射到父进程的地址的页表项时页表项应该具有哪些权限当COW发生后页表项又该具有哪些权限
- 内核如何分辨现在是一个copy-on-write fork的场景**提示PTE的标志位中有两位RSW是没有使用的。**
- 如果有父进程fork了多个子进程意味着该父进程的物理页会被多个子进程”共享“。举个例子当父进程退出时我们需要更加的小心因为我们要判断是否能立即释放相应的物理页。如果有子进程还在使用这些物理页而内核又释放了这些物理页将会出现问题。该“共享”页在什么条件下才能释放
**注意:本挑战的难点在于对页表项的控制以及对页面释放的判断,读者应思考两个问题如何解决,完成设计并验证后,读者可以继续尝试在数据段也加入写时复制机制。**
**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。**

@ -28,6 +28,10 @@
- [给定应用](#lab4_challenge2_app) - [给定应用](#lab4_challenge2_app)
- [实验内容](#lab4_challenge2_content) - [实验内容](#lab4_challenge2_content)
- [实验指导](#lab4_challenge2_guide) - [实验指导](#lab4_challenge2_guide)
- [6.7 lab4_challenge3 简易Shell难度&#9733;&#9733;&#9733;&#9733;&#9733;](#lab4_challenge3_shell)
- [给定应用](#lab4_challenge3_app)
- [实验内容](#lab4_challenge3_content)
- [实验指导](#lab4_challenge3_guide)
<a name="fundamental"></a> <a name="fundamental"></a>
@ -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。
<a name="lab4_challenge2_guide"></a> <a name="lab4_challenge2_guide"></a>
@ -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系统调用等待子进程的结束并继续执行后续代码。** **注意完成实验内容后若读者同时完成了之前的“lab3_challenge1 进程等待和数据段复制”挑战实验则可以自行对app_exec程序进行修改将其改为一个简单的“shell”程序。该“shell”程序应该首先通过fork系统调用创建一个新的子进程并在子进程中调用exec执行一个新的程序如app_ls同时父进程会调用wait系统调用等待子进程的结束并继续执行后续代码。**
**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。** **另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。**
<a name="lab4_challenge3_shell"></a>
## 6.7 lab4_challenge3 简易Shell难度&#9733;&#9733;&#9733;&#9733;&#9733;
<a name="lab4_challenge3_app"></a>
#### 给定应用
- 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
```
可以看到我们提供了五种简化版的命令分别是lsmkdirtouchechocat。命令的对应功能参考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.
```
<a name="lab4_challenge3_content"></a>
#### 实验内容
本实验为挑战实验基础代码将继承和使用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那么可以直接使用对应成果。
<a name="lab4_challenge3_guide"></a>
#### 实验指导
shell是Linux中的一个重要概念主要用于和用户的交互。其基本流程为用户输入命令->shell创建一个新进程并使用exec函数加载命令对应的程序->将结果通过shell返回用户。其中fork函数的功能已在前面的实验中实现本实验主要实现exec函数的功能。如果你已完成lab4_challenge2的话对本实验会非常有帮助
不考虑参数传递问题的话exec要做的工作基本为替换elf内容和重置堆栈等地址空间。但是因为存在新进程需要得知命令参数的问题exec除了提到的工作之外还需要考虑以下问题
- main函数中的argcargv数组的含义以及main函数是从哪里得到它们的。**提示main函数也是函数函数的参数一般保存在哪里呢**
- 在lab1中我们初步了解了系统调用可以发现系统调用使用了a0~a7寄存器。请读者思考**处理系统调用的函数的返回值保存在哪里,有什么作用呢?**
- 在exec替换完elf后我们将堆栈重置。此时的sp指向什么位置是否可以修改以达到传递参数的效果。
- 注意地址对齐。读者可自行参阅RISC-V的相关资料了解该指令集下的地址对齐规则。
**注意完成实验内容后若读者同时完成了之前的“lab3_challenge3 写时复制”挑战实验则可以将fork添加写时复制功能来减少不必要的开销。此外本挑战实验具有很强的扩展性读者可通过自行比对与真实shell的差别为pke-shell添加更完善的功能。**
**另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。**

Loading…
Cancel
Save