|
|
|
@ -6,7 +6,7 @@
|
|
|
|
|
|
|
|
|
|
**4.1.1 练习一:OS内存的初始化过程**
|
|
|
|
|
|
|
|
|
|
**在**"pk/mmap.c"内有 pk_vm_init()函数,阅读该函数,了解OS内存初始化的过程。
|
|
|
|
|
在"pk/mmap.c"内有 pk_vm_init()函数,阅读该函数,了解OS内存初始化的过程。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
364 uintptr_t pk_vm_init()
|
|
|
|
@ -47,9 +47,7 @@
|
|
|
|
|
391 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
以上代码中,我们给出了大体的注释,请根据以上代码,尝试画出RISCV的物理内存结构图。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
以上代码中,我们给出了大体的注释,请根据以上代码,读者可以尝试画一下PK的逻辑地址空间结构图,以及逻辑地址空间到物理地址空间的映射关系。
|
|
|
|
|
|
|
|
|
|
**4.1.2 练习二:first_fit内存页分配算法(需要编程)**
|
|
|
|
|
|
|
|
|
@ -108,22 +106,22 @@ Score: 20/20
|
|
|
|
|
|
|
|
|
|
### 4.2 基础知识
|
|
|
|
|
|
|
|
|
|
**4.2.1 物理内存空间与空间编址**
|
|
|
|
|
**4.2.1 物理内存空间与编址**
|
|
|
|
|
|
|
|
|
|
计算机的存储结构可以抽象的看做由N个连续的字节组成的数组。想一想,在数组中我们如何找到一个元素?对了!是下标!!那么我们如何在内存中如何找打一个元素呢?自然也是‘下标’。这个下标的起始位置和位数由机器本身决定,我们称之为“物理地址”。
|
|
|
|
|
|
|
|
|
|
在riscv中,内存地址是从0x80000000 开始的。在pke的连接文件pke.lds中,我们可以看到这样两行:
|
|
|
|
|
至于物理内存的大小,由于我们的RISC-V目标机(也就是我们的pke以及app运行的环境)是由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;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
至于内存的大小,还记得实验二中的-m选项吗?spike模拟器可以通过-m选项配置物理内存的大小。
|
|
|
|
|
可见,PK内核的逻辑地址的起始也是0x8000-0000!这也就意味着PK内核实际上采用的是直接地址映射的办法保证在未打开分页情况下,逻辑地址到物理地址的映射的。代理内核的本质也是一段程序,他本身是需要内存空间的,而这一段空间在PK的设计中是静态分配给内核使用的,不能被再分配给任何应用。那么静态分配给代理内核的内存空间具体是哪一段内存区域呢?
|
|
|
|
|
|
|
|
|
|
现在,思考一下问题:首先,为什么需要物理内存的管理?这个问题可以用另一个问题回答:当程序如malloc申请一段内存空间的时候,你如何准确的给出一片符合大小要求的,且安全可用的内存空间。
|
|
|
|
|
|
|
|
|
|
其次,我们现在管理的可用内存是那一部分? 代理内核的本质也是一段程序,他本身是需要内存空间的,而这一段空间自然不能再被分配。除去内核本身占的空间,内核可支配的物理空间从0x80016000开始,大小在PKE设定为8M,2048个页面,故而供内核支配的的内存的范围为(first_free_page~first_free_paddr)。如下图所示。
|
|
|
|
|
通过阅读PK的代码,我们可知PK内核占据了以下这一段:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
KERNTOP------->+---------------------------------+ 0x80816000
|
|
|
|
@ -131,31 +129,26 @@ Score: 20/20
|
|
|
|
|
| Kern Physical Memory |
|
|
|
|
|
| | 8M 2048pages
|
|
|
|
|
(first_free_page)| |
|
|
|
|
|
KERNBSE -----> +---------------------------------+ 0x80016000
|
|
|
|
|
DRAM_BASE----> +---------------------------------+ 0x80016000
|
|
|
|
|
| Kern Text/Data/BBS |
|
|
|
|
|
KERN------>+---------------------------------+ 0x80000000
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
再往上便是我们可以支配的空间了(KERNTOP~Top Memory):
|
|
|
|
|
也就是说,[0x8000-0000, 0x8081-6000]这段物理内存空间是被PK内核所“保留”的,余下的物理内存空间为[0x8081-6000,PHYMEM_TOP],也就是下图中的Empty Memory(*)部分,这部分内存将会是我们的操作系统需要真正用于动态分配(给应用程序)的空间,**而本实验就是要管理这部分物理内存空间**。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
kernel/user
|
|
|
|
|
4G ------------> +-------------------------------------------------+
|
|
|
|
|
PHYMEM_TOP ----> +-------------------------------------------------+
|
|
|
|
|
| |
|
|
|
|
|
| Empty Memory (*) |
|
|
|
|
|
| |
|
|
|
|
|
Top Memmory ---> +-------------------------------------------------+ 随物理内存大小移动
|
|
|
|
|
KERNTOP ---> +-------------------------------------------------+ 0x80816000
|
|
|
|
|
(first_free_paddr)| |
|
|
|
|
|
| PK kernel resevered |
|
|
|
|
|
| |
|
|
|
|
|
| User Remapped Memory |
|
|
|
|
|
| |
|
|
|
|
|
| |
|
|
|
|
|
first_free_paddr->+-------------------------------------------------+ 0x80816000
|
|
|
|
|
KERN ----> +-------------------------------------------------+ 0x80000000
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最后,我们来看物理内存分配的单位:操作系统中,物理页是物理内存分配的基本单位。一个物理页的大小是4KB,我们使用结构体Page来表示,其结构如图:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
@ -194,14 +187,11 @@ Page结构体对应着物理页,我们来看Page结构体同物理地址之间
|
|
|
|
|
<img src="pictures/fig4_2.png" alt="fig4_2" style="zoom:80%;" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**3.2.2** **中断的处理过程**
|
|
|
|
|
|
|
|
|
|
考虑一下,当程序执行到中断之前,程序是有自己的运行状态的,例如寄存器里保持的上下文数据。当中断发生,硬件在自动设置完中断原因和中断地址后,就会调转到中断处理程序,而中断处理程序同样会使用寄存器,于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器,我们称之为callee-saved寄存器。
|
|
|
|
|
当程序执行到中断之前,程序是有自己的运行状态的,例如寄存器里保持的上下文数据。当中断发生,硬件在自动设置完中断原因和中断地址后,就会调转到中断处理程序,而中断处理程序同样会使用寄存器,于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器,我们称之为callee-saved寄存器。
|
|
|
|
|
|
|
|
|
|
在PK的machine/minit.c中间中,便通过delegate_traps(),将部分中断及同步异常委托给S模式。(同学们可以查看具体是哪些中断及同步异常)
|
|
|
|
|
在PK的machine/minit.c中间中,便通过delegate_traps(),将部分中断及同步异常委托给S模式。(同学们可以查看具体是哪些中断及同步异常)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
43 // send S-mode interrupts and most exceptions straight to S-mode
|
|
|
|
@ -226,9 +216,9 @@ Page结构体对应着物理页,我们来看Page结构体同物理地址之间
|
|
|
|
|
62 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里介绍一下RISCV的中断委托机制,在默认的情况下,所有的异常都会被交由机器模式处理。但正如我们知道的那样,大部分的系统调用都是在S模式下处理的,因此RISCV提供了这一委托机制,可以选择性的将中断交由S模式处理,从而完全绕过M模式。
|
|
|
|
|
这里介绍一下RISCV的中断委托机制,在默认的情况下,所有的异常都会被交由机器模式处理。但正如我们知道的那样,大部分的系统调用都是在S模式下处理的,因此RISCV提供了这一委托机制,可以选择性的将中断交由S模式处理,从而完全绕过M模式。
|
|
|
|
|
|
|
|
|
|
接下,我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中,stvec保存着发生异常时处理器需要跳转到的地址,也就是说当中断发生,我们将跳转至trap_entry,现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中,其代码如下:
|
|
|
|
|
接下,我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中,stvec保存着发生异常时处理器需要跳转到的地址,也就是说当中断发生,我们将跳转至trap_entry,现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中,其代码如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
60 trap_entry:
|
|
|
|
@ -241,11 +231,11 @@ Page结构体对应着物理页,我们来看Page结构体同物理地址之间
|
|
|
|
|
67 jal handle_trap
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在61行,交换了sp与sscratch的值,这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。
|
|
|
|
|
在61行,交换了sp与sscratch的值,这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。
|
|
|
|
|
|
|
|
|
|
如果sp也就是传入的sscratch值不为零,则跳转至64行,若sscratch的值为零,则恢复原sp中的值。这是因为,当中断来源于S模式是,sscratch的值为0,sp中存储的就是内核的堆栈地址。而当中断来源于U模式时,sp中存储的是用户的堆栈地址,sscratch中存储的则是内核的堆栈地址,需要交换二者,是sp指向内核的堆栈地址。
|
|
|
|
|
如果sp也就是传入的sscratch值不为零,则跳转至64行,若sscratch的值为零,则恢复原sp中的值。这是因为,当中断来源于S模式是,sscratch的值为0,sp中存储的就是内核的堆栈地址。而当中断来源于U模式时,sp中存储的是用户的堆栈地址,sscratch中存储的则是内核的堆栈地址,需要交换二者,是sp指向内核的堆栈地址。
|
|
|
|
|
|
|
|
|
|
接着在64,65行保存上下文,最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中,代码如下:
|
|
|
|
|
接着在64,65行保存上下文,最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中,代码如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
112 void handle_trap(trapframe_t* tf)
|
|
|
|
@ -271,7 +261,7 @@ Page结构体对应着物理页,我们来看Page结构体同物理地址之间
|
|
|
|
|
132 };
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
handle_trap函数中实现了S模式下各类中断的处理。可以看到,代码的126行就对应着系统调用的处理,handle_syscall的实现如下:
|
|
|
|
|
handle_trap函数中实现了S模式下各类中断的处理。可以看到,代码的126行就对应着系统调用的处理,handle_syscall的实现如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
100 static void handle_syscall(trapframe_t* tf)
|
|
|
|
@ -282,7 +272,7 @@ Page结构体对应着物理页,我们来看Page结构体同物理地址之间
|
|
|
|
|
105 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
还记得我们在例3.1中是将中断号写入x17寄存器嘛?其对应的就是这里do_syscall的最后一个参数,我们跟踪进入do_syscall函数,其代码如下:
|
|
|
|
|
还记得我们在例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)
|
|
|
|
@ -330,4 +320,4 @@ Page结构体对应着物理页,我们来看Page结构体同物理地址之间
|
|
|
|
|
355 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
do_syscall中通过传入的系统调用号n,查询syscall_table得到对应的函数,并最终执行系统调用。
|
|
|
|
|
do_syscall中通过传入的系统调用号n,查询syscall_table得到对应的函数,并最终执行系统调用。
|
|
|
|
|