You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pke-doc/chapter4_memory.md

65 KiB

第四章实验2内存管理

目录

4.1 实验2的基础知识

在过去的第一组实验(lab1)为了简化设计我们采用了Bare模式来完成虚拟地址到物理地址的转换实际上就是不转换认为虚拟地址=物理地址也未开启模拟RISC-V机器的分页功能。在本组实验实验2我们将开启和使用Sv39页式虚拟内存管理无论是操作系统内核还是应用都通过页表来实现逻辑地址到物理地址的转换。

实际上,我们在本书的第一章的1.5节曾介绍过RISC-V的Sv39页式虚拟内存的管理方式在本章我们将尽量结合PKE的实验代码讲解RISC-V的Sv39虚拟内存管理机制并通过3个基础实验加深读者对该管理机制的理解。

4.1.1 Sv39虚地址管理方案回顾

我们先回顾一下RISC-V的sv39虚地址管理方案在该方案中逻辑地址就是我们的程序中各个符号在链接时被赋予的地址通过页表转换为其对应的物理地址。由于我们考虑的机器采用了RV64G指令集意味着逻辑地址和物理地址理论上都是64位的。然而对于逻辑地址实际上我们的应用规模还用不到全部64位的寻找空间所以Sv39方案中只使用了64位虚地址中的低39位Sv48方案使用了低48位意味着我们的应用程序的地址空间可以到512GB对于物理地址目前的RISC-V设计只用到了其中的低56位。

Sv39将39位虚拟地址“划分”为4个段如下图所示

  • [38,30]共9位图中的VPN[2]用于在5122^9个页目录page directory项中检索页目录项page directory entry, PDE
  • [29,21]共9位图中的VPN[1]用于在5122^9个页中间目录page medium directory中检索PDE
  • [20,12]共9位图中的VPN[0]用于在5122^9个页表page medium directory中检索PTE
  • [11,0]共12位图中的offset充当4KB页的页内位移。

fig1_8

图4.1 Sv39中虚拟地址到物理地址的转换过程

由于每个物理页的大小为4KB同时每个目录项PDE或页表项PTE占据8个字节所以一个物理页能够容纳的PDE或PTE的数量为4KB/8B=512这也是为什么VPN[2]=VPN[1]=VPN[0]=512的原因。

8字节的PDE或者PTE的格式如下

fig1_7

图4.2 Sv39中PDE/PTE格式

其中的各个位的含意为:

● VValid位决定了该PDE/PTE是否有效V=1时有效即是否有对应的实页。

● RRead、WWrite和XeXecutable位分别表示此页对应的实页是否可读、可写和可执行。这3个位只对PTE有意义对于PDE而言这3个位都为0。

● UUser位表示该页是不是一个用户模式页。如果U=1表示用户模式下的代码可以访问该页否则就表示不能访问。S模式下的代码对U=1页面的访问取决于sstatus寄存器中的SUM字段取值。

● GGlobal位表示该PDE/PTE是不是全局的。我们可以把操作系统中运行的一个进程认为是一个独立的地址空间有时会希望某个虚地址空间转换可以在一组进程中共享这种情况下就可以将某个PDE的G位设置为1达到这种共享的效果。

● AAccess位表示该页是否被访问过。

● DDirty位表示该页的内容是否被修改。

● RSW位2位是保留位一般由运行在S模式的代码如操作系统来使用。

● PPN44位是物理页号Physical Page Number简写为PPN

其中PPN为44位的原因是对于物理地址现有的RISC-V规范只用了其中的56位同时这56位中的低12位为页内位移。所以PPN的长度=56-12=44

4.1.2 物理内存布局与规划

PKE实验用到的RISC-V机器实际上是spike模拟出来的例如采用以下命令

$ spike ./obj/riscv-pke ./obj/app_helloworld

spike将创建一个模拟的RISC-V机器该机器拥有一个支持RV64G指令集的处理器2GB的模拟物理内存。实际上我们可以通过在spike命令行中使用-m开关指定模拟机器的物理内存大小,如使用-m512即可获得拥有512MB物理内存的模拟机器。默认的2GB物理内存配置等效于-m2048在之后对模拟RISC-V机器物理内存布局的讨论中我们将只考虑默认配置以简化论述。另外也可以在spike命令行中使用-p开关指定模拟机器中处理器的个数这样就可以模拟出一个多核的RISC-V平台了。

需要注意的是,对于我们的模拟RISC-V机器而言2GB的物理内存并不是从0地址开始编址而是从0x80000000见kernel/memlayout.h文件中的DRAM_BASE宏定义开始编址的。这样做的理由是,部分低物理地址[0x0, 0x80000000]并无物理内存与之对应该空间留作了MMIO的用途。例如我们在lab1_3中遇到的CLINTCore Local Interruptertimer中断的产生就是通过往这个地址写数据控制的见kernel/riscv.h文件中的CLINT定义的地址是0x2000000就位于这一空间。从0x80000000开始对物理内存进行编址的好处是避免类似x86平台那样产生内存空洞memory hole如640KB~1MB的BIOS空间从而导致内存的浪费和管理上的复杂性。

我们的代理内核(构造出来的./obj/riscv-pke文件的逻辑地址也是从0x80000000开始的见kernel/kernel.lds文件中的内容spike将代理内核载入模拟物理内存时也是将该代理内核的代码段、数据段载入到0x80000000开始的内存空间如图4.3所示。

physical_mem_layout.png

图4.3 初始内存布局和载入操作系统内核后的内存布局

这样操作系统内核的逻辑地址和物理地址就有了一一对应的关系这也是我们在lab1中采用直模式Bare mode虚拟地址翻译机制也不会出错的原因。这里需要解释的是对内核的机器模式栈的处理。通过实验一我们知道机器模式栈是一个4KB的空间它位于内核数据段而不是专门分配一个额外的页面。这样简单处理的原因是PKE上运行的应用往往只有一个算是非常简单的多任务环境且操作系统利用机器模式栈的时机只有特殊的异常如lab1_2中的非法指令异常以及一些外部中断如lab1_3中的时钟中断

如图4.3(b)所示在spike将操作系统内核装入物理内存后剩余的内存空间应该是从内核数据段的结束_end符号到0xffffffff即4GB-1的地址。但是由于PKE操作系统内核的特殊性它只需要支持给定应用的运行lab2的代码将操作系统管理的空间进一步缩减定义了一个操作系统需要管理的最大内存空间kernel/config.h文件从而提升实验代码的执行速度

 10 // the maximum memory space that PKE is allowed to manage. added @lab2_1
 11 #define PKE_MAX_ALLOWABLE_RAM 128 * 1024 * 1024
 12
 13 // the ending physical address that PKE observes. added @lab2_1
 14 #define PHYS_TOP (DRAM_BASE + PKE_MAX_ALLOWABLE_RAM)

可以看到实验代码“人为”地将PKE操作系统所能管理的内存空间限制到了128MB即PKE_MAX_ALLOWABLE_RAM的定义同时定义了PHYS_TOP为新的内存物理地址上限。实际上kernel/pmm.c文件所定义的pmm_init()函数包含了PKE对物理内存进行管理的逻辑

 63 void pmm_init() {
 64   // start of kernel program segment
 65   uint64 g_kernel_start = KERN_BASE;
 66   uint64 g_kernel_end = (uint64)&_end;
 67
 68   uint64 pke_kernel_size = g_kernel_end - g_kernel_start;
 69   sprint("PKE kernel start 0x%lx, PKE kernel end: 0x%lx, PKE kernel size: 0x%lx .\n",
 70     g_kernel_start, g_kernel_end, pke_kernel_size);
 71
 72   // free memory starts from the end of PKE kernel and must be page-aligined
 73   free_mem_start_addr = ROUNDUP(g_kernel_end , PGSIZE);
 74
 75   // recompute g_mem_size to limit the physical memory space that our riscv-pke kernel
 76   // needs to manage
 77   g_mem_size = MIN(PKE_MAX_ALLOWABLE_RAM, g_mem_size);
 78   if( g_mem_size < pke_kernel_size )
 79     panic( "Error when recomputing physical memory size (g_mem_size).\n" );
 80
 81   free_mem_end_addr = g_mem_size + DRAM_BASE;
 82   sprint("free physical memory address: [0x%lx, 0x%lx] \n", free_mem_start_addr,
 83     free_mem_end_addr - 1);
 84
 85   sprint("kernel memory manager is initializing ...\n");
 86   // create the list of free pages
 87   create_freepage_list(free_mem_start_addr, free_mem_end_addr);
 88 }

在76行pmm_init()函数会计算g_mem_size其值在PKE_MAX_ALLOWABLE_RAM和spike所模拟的物理内存大小中取最小值也就是说除非spike命令行参数中-m参数后面所带的数字小于128即128Mg_mem_size的大小将为128MB。

另外,为了对空闲物理内存(地址范围为[_endg_mem_size+DRAM_BASE(即PHYS_TOP)]进行有效管理pmm_init()函数在86行通过调用create_freepage_list()函数定义了一个链表用于对空闲物理内存的分配和回收。kernel/pmm.c文件中包含了所有对物理内存的初始化、分配和回收的例程它们的实现非常的简单感兴趣的读者请对里面的函数进行阅读理解。

4.1.3 PKE操作系统和应用进程的逻辑地址空间结构

通过4.1.2的讨论我们知道对于PKE内核来说有逻辑地址=物理地址的关系成立这也是在实验一中我们可以采用Bare模式进行地址映射的原因。采用Bare模式的地址映射在进行内存访问时无需经过页表和硬件进行逻辑地址到物理地址的转换。然而在实验二中我们将采用Sv39虚拟地址管理方案通过页表和硬件spike模拟的MMU进行访存地址的转换。为实现这种转换首先需要确定的就是将要被转换的逻辑地址空间即需要对哪部分逻辑地址空间进行转换的问题。在PKE的实验二中存在两个需要被转换的实体一个是操作系统内核另一个是我们的实验给定的应用程序所对应的进程。下面我们对它们分别讨论

操作系统内核

操作系统内核的逻辑地址与物理地址存在一一对应的关系但是在开启了Sv39虚拟内存管理方案后所有的逻辑地址到物理地址的翻译都必须通过页表和MMU硬件进行所以为操作系统内核建立页表是必不可少的工作。操作系统的逻辑地址空间可以简单的认为是从内核代码段的起始即KERN_BASE=0x80000000到物理地址的顶端也就是PHYS_TOP因为操作系统是系统中拥有最高权限的软件需要实现对所有物理内存空间的直接管理。这段逻辑地址空间即[KERN_BASEPHYS_TOP],所映射的物理地址空间也是[KERN_BASEPHYS_TOP]。也就是说对于操作系统内核我们在实验二中通过Sv39的页表仍然保持和实验一一样的逻辑地址到物理地址的一一对应关系。在权限方面对于内核代码段所对应的页面来说是可读可执行对于数据段以及空闲内存空间其权限为可读可写。

kernel_address_mapping.png

图4.4 PKE操作系统内核的逻辑地址空间和它到物理地址空间的映射

操作系统内核建立页表的过程可以参考kernel/vmm.c文件中的kern_vm_init()函数的实现需要说明的是kern_vm_init()函数在PKE操作系统内核的S态初始化过程s_start函数中被调用

120 void kern_vm_init(void) {
121   // pagetable_t is defined in kernel/riscv.h. it's actually uint64*
122   pagetable_t t_page_dir;
123
124   // allocate a page (t_page_dir) to be the page directory for kernel. alloc_page is defined in kernel/pmm.c
125   t_page_dir = (pagetable_t)alloc_page();
126   // memset is defined in util/string.c
127   memset(t_page_dir, 0, PGSIZE);
128
129   // map virtual address [KERN_BASE, _etext] to physical address [DRAM_BASE, DRAM_BASE+(_etext - KERN_BASE)],
130   // to maintain (direct) text section kernel address mapping.
131   kern_vm_map(t_page_dir, KERN_BASE, DRAM_BASE, (uint64)_etext - KERN_BASE,
132          prot_to_type(PROT_READ | PROT_EXEC, 0));
133
134   sprint("KERN_BASE 0x%lx\n", lookup_pa(t_page_dir, KERN_BASE));
135
136   // also (direct) map remaining address space, to make them accessable from kernel.
137   // this is important when kernel needs to access the memory content of user's app
138   // without copying pages between kernel and user spaces.
139   kern_vm_map(t_page_dir, (uint64)_etext, (uint64)_etext, PHYS_TOP - (uint64)_etext,
140          prot_to_type(PROT_READ | PROT_WRITE, 0));
141
142   sprint("physical address of _etext is: 0x%lx\n", lookup_pa(t_page_dir, (uint64)_etext));
143
144   g_kernel_pagetable = t_page_dir;
145 }

我们看到kern_vm_init()函数会首先125行从空闲物理内存中获取分配一个t_page_dir指针所指向的物理页该页将作为内核页表的根目录page directory对应图4.1中的VPN[2]。接下来将该页的内容清零127行、映射代码段到它对应的物理地址131--132行、映射数据段的起始到PHYS_TOP到它对应的物理地址空间139--140行最后记录内核页表的根目录页144行

应用进程

对于实验一的所有应用,我们通过指定应用程序中所有的符号地址对应的逻辑地址的方法(参见第三章的3.1.3节将应用程序中的逻辑地址“强行”对应到图4.3中的“实际空闲内存”空间并在ELF加载时将程序段加载到了这块内存空间中的对应位置从而使得应用程序所对应的进程也可以采用类似操作系统内核那样的直映射Bare模式方式。然而这样做是因为实验一中的应用都是单线程应用它们的执行并不会产生新的执行体如子进程所以可以采用指定逻辑地址的办法进行简化。但是实际情况是我们在代理内核上是有可能执行多进程应用的特别是在开发板上验证多核RISC-V处理器的场景我们在实验三中也将开始讨论在PKE实验中实现和完善进程的管理。在这种场景下由于无法保证应用所要求的逻辑地址空间“恰好”能找到对应的物理地址空间且后者还未被占据。

这里我们可以观察一下在未指定逻辑地址的情况下的应用对应的逻辑地址。首先切换到lab2_1_pagetable然后构造内核和应用

// 切换到lab2_1_pagetable分支
$ git checkout lab2_1_pagetable
// 构造内核和应用
$ make
// 显示应用程序中将被加载的程序段
$ riscv64-unknown-elf-readelf -l ./obj/app_helloworld_no_lds

Elf file type is EXEC (Executable file)
Entry point 0x100f6
There is 1 program header, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000010000 0x0000000000010000
                 0x0000000000000360 0x0000000000000360  R E    0x1000

 Section to Segment mapping:
  Segment Sections...
   00     .text .rodata

通过以上结果我们看到lab2_1的应用app_helloworld_no_lds实际上就是lab1_1中的app_helloworld不同的地方在于没有用到lab1_1中的user/user.lds来约束逻辑地址只包含一个代码段它的起始地址为0x0000000000010000即0x10000

对比4.1.2节中讨论的物理内存布局我们知道spike模拟的RISC-V机器并无处于0x10000的物理地址空间与其对应。这样我们就需要通过Sv39虚地址管理方案将0x10000开始的代码段映射到app_helloworld_no_lds中代码段实际被加载到的物理内存显然位于图4.3中的“实际内存空间”所标识的区域)区域。

PKE实验二中的应用加载是通过kernel/kernel.c文件中的load_user_program函数来完成的

 38 void load_user_program(process *proc) {
 39   sprint("User application is loading.\n");
 40   // allocate a page to store the trapframe. alloc_page is defined in kernel/pmm.c. added @la    b2_1
 41   proc->trapframe = (trapframe *)alloc_page();
 42   memset(proc->trapframe, 0, sizeof(trapframe));
 43
 44   // allocate a page to store page directory. added @lab2_1
 45   proc->pagetable = (pagetable_t)alloc_page();
 46   memset((void *)proc->pagetable, 0, PGSIZE);
 47
 48   // allocate pages to both user-kernel stack and user app itself. added @lab2_1
 49   proc->kstack = (uint64)alloc_page() + PGSIZE;   //user kernel stack top
 50   uint64 user_stack = (uint64)alloc_page();       //phisical address of user stack bottom
 51
 52   // USER_STACK_TOP = 0x7ffff000, defined in kernel/memlayout.h
 53   proc->trapframe->regs.sp = USER_STACK_TOP;  //virtual address of user stack top
 54
 55   sprint("user frame 0x%lx, user stack 0x%lx, user kstack 0x%lx \n", proc->trapframe,
 56          proc->trapframe->regs.sp, proc->kstack);
 57
 58   // load_bincode_from_host_elf() is defined in kernel/elf.c
 59   load_bincode_from_host_elf(proc);
 60
 61   // populate the page table of user application. added @lab2_1
 62   // map user stack in userspace, user_vm_map is defined in kernel/vmm.c
 63   user_vm_map((pagetable_t)proc->pagetable, USER_STACK_TOP - PGSIZE, PGSIZE, user_stack,
 64          prot_to_type(PROT_WRITE | PROT_READ, 1));
 65
 66   // map trapframe in user space (direct mapping as in kernel space).
 67   user_vm_map((pagetable_t)proc->pagetable, (uint64)proc->trapframe, PGSIZE, (uint64)proc->trapframe,
 68          prot_to_type(PROT_WRITE | PROT_READ, 0));
 69
 70   // map S-mode trap vector section in user space (direct mapping as in kernel space)
 71   // here, we assume that the size of usertrap.S is smaller than a page.
 72   user_vm_map((pagetable_t)proc->pagetable, (uint64)trap_sec_start, PGSIZE, (uint64)trap_sec_start,
 73          prot_to_type(PROT_READ | PROT_EXEC, 0));
 74 }

load_user_program()函数对于应用进程逻辑空间的操作可以分成以下几个部分:

  • 41--42行分配一个物理页面将其作为栈帧trapframe即发生中断时保存用户进程执行上下文的内存空间。由于物理页面都是从位于物理地址范围[_endPHYS_TOP]的空间中分配的它的首地址也将位于该区间。所以第67--68行的映射也是做一个proc->trapframe到所分配页面的直映射逻辑地址=物理地址)。

  • 45--46行分配一个物理页面作为存放进程页表根目录page directory对应图4.1中的VPN[2])的空间。

  • 49行分配了一个物理页面作为用户进程的内核态栈该栈将在用户进程进入中断处理时用作S模式内核处理函数使用的栈。然而这个栈并未映射到用户进程的逻辑地址空间而是将其首地址保存在proc->kstack中。

  • 50--53行再次分配一个物理页面作为用户进程的用户态栈该栈供应用在用户模式下使用并在第63--64行映射到用户进程的逻辑地址USER_STACK_TOP。

  • 59行调用load_bincode_from_host_elf()函数该函数将读取应用所对应的ELF文件并将其中的代码段读取到新分配的内存空间物理地址位于[_endPHYS_TOP]区间)。

  • 72--73行将内核中的S态trap入口函数所在的物理页一一映射到用户进程的逻辑地址空间。

通过以上load_user_program()函数,我们可以大致画出用户进程的逻辑地址空间,以及该地址空间到物理地址空间的映射。

user_address_mapping.png

图4.5 用户进程的逻辑地址空间和到物理地址空间的映射

我们看到用户进程在装入后其逻辑地址空间有4个区间建立了和物理地址空间的映射。从上往下观察“用户进程trapframe”和“trap入口页面”的逻辑地址大于0x80000000且与承载它们的物理空间建立了一对一的映射关系。另外两个区间即“用户态栈”和“用户代码段”的逻辑地址都低于0x80000000它们所对应的物理空间则都位于实际空闲内存区域同时这种映射的逻辑地址显然不等于物理地址。

4.1.4 与页表操作相关的重要函数

实验二与页表操作相关的函数都放在kernel/vmm.c文件中其中比较重要的函数有

  • int map_pages(pagetable_t page_dir, uint64 va, uint64 size, uint64 pa, int perm);

该函数的第一个输入参数page_dir为根目录所在物理页面的首地址第二个参数va则是将要被映射的逻辑地址第三个参数size为所要建立映射的区间的长度第四个参数pa为逻辑地址va所要被映射到的物理地址首地址最后第五个的参数perm为映射建立后页面访问的权限。

总的来说该函数将在给定的page_dir所指向的根目录中建立[vava+size]到[papa+size]的映射。

  • pte_t *page_walk(pagetable_t page_dir, uint64 va, int alloc);

该函数的第一个输入参数page_dir为根目录所在物理页面的首地址第二个参数va为所要查找walk的逻辑地址第三个参数实际上是一个bool类型当它为1时如果它所要查找的逻辑地址并未建立与物理地址的映射图4.1中的Page Medium Directory不存在则通过分配内存空间建立从根目录到页表的完整映射并最终返回va所对应的页表项当它为0时如果它所要查找的逻辑地址并未建立与物理地址的映射则返回NULL否则返回va所对应的页表项。

  • uint64 lookup_pa(pagetable_t pagetable, uint64 va);

查找逻辑地址va所在虚拟页面地址即va将低12位置零对应的物理页面地址。如果没有与va对应的物理页面则返回NULL否则返回va对应的物理页面地址。

4.2 lab2_1 虚实地址转换

给定应用

  • user/app_helloworld_no_lds.c
  1 /*
  2  * Below is the given application for lab2_1.
  3  * This app runs in its own address space, in contrast with in direct mapping.
  4  */
  5
  6 #include "user_lib.h"
  7 #include "util/types.h"
  8
  9 int main(void) {
 10   printu("Hello world!\n");
 11   exit(0);
 12 }

该应用的代码跟lab1_1是一样的。但是不同的地方在于它的编译和链接并未指定程序中符号的逻辑地址。

  • 先提交lab1_3的答案然后切换到lab2_1继承lab1_3中所做的修改并make后的直接运行结果
//切换到lab2_1
$ git checkout lab2_1_pagetable

//继承lab1_3以及之前的答案
$ git merge lab1_3_irq -m "continue to work on lab2_1"

//重新构造
$ make clean; make

//运行构造结果
$ spike ./obj/riscv-pke ./obj/app_helloworld_no_lds
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080004000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: ./obj/app_helloworld_no_lds
Application program entry point (virtual address): 0x00000000000100f6
Switching to user mode...
You have to implement user_va_to_pa (convert user va to pa) to print messages in lab2_1.

System is shutting down with exit code -1.

从以上运行结果来看我们的应用app_helloworld_no_lds并未如愿地打印出“Hello world!\n”这是因为user/app_helloworld_no_lds.c的第10行printu("Hello world!\n");中的“Hello world!\n”字符串本质上是存储在.rodata段它被和代码段.text一起被装入内存。从逻辑地址结构来看它的逻辑地址就应该位于图4.5中的“用户代码段”显然低于0x80000000。

而printu是一个典型的系统调用参考lab1_1的内容它的执行逻辑是通过ecall指令陷入到内核S模式完成到屏幕的输出。然而对于内核而言显然不能继续使用“Hello world!\n”的逻辑地址对它进行访问而必须将其转换成物理地址因为如图4.4所示操作系统内核已建立了到“实际空闲内存”的直映射。而lab2_1的代码显然未实现这种转换。

实验内容

实现user_va_to_pa()函数,完成给定逻辑地址到物理地址的转换,并获得以下预期结果:

$ spike ./obj/riscv-pke ./obj/app_helloworld_no_lds
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080004000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: ./obj/app_helloworld_no_lds
Application program entry point (virtual address): 0x00000000000100f6
Switching to user mode...
Hello world!
User exit with code:0.
System is shutting down with exit code 0.

实验指导

读者可以参考lab1_1的内容重走从应用的printu到S态的系统调用的完整路径最终来到kernel/syscall.c文件的sys_user_print()函数:

 21 ssize_t sys_user_print(const char* buf, size_t n) {
 22   //buf is an address in user space on user stack,
 23   //so we have to transfer it into phisical address (kernel is running in direct mapping).
 24   assert( current );
 25   char* pa = (char*)user_va_to_pa((pagetable_t)(current->pagetable), (void*)buf);
 26   sprint(pa);
 27   return 0;
 28 }

该函数最终在第26行通过调用sprint将结果输出但是在输出前需要将buf地址转换为物理地址传递给sprint这一转换是通过user_va_to_pa()函数完成的。而user_va_to_pa()函数的定义在kernel/vmm.c文件中定义

150 void *user_va_to_pa(pagetable_t page_dir, void *va) {
151   // TODO (lab2_1): implement user_va_to_pa to convert a given user virtual address "va"
152   // to its corresponding physical address, i.e., "pa". To do it, we need to walk
153   // through the page table, starting from its directory "page_dir", to locate the PTE
154   // that maps "va". If found, returns the "pa" by using:
155   // pa = PYHS_ADDR(PTE) + (va - va & (1<<PGSHIFT -1))
156   // Here, PYHS_ADDR() means retrieving the starting address (4KB aligned), and
157   // (va - va & (1<<PGSHIFT -1)) means computing the offset of "va" in its page.
158   // Also, it is possible that "va" is not mapped at all. in such case, we can find
159   // invalid PTE, and should return NULL.
160   panic( "You have to implement user_va_to_pa (convert user va to pa) to print messages in lab2_1.\n" );
161
162 }

如注释中的提示为了在page_dir所指向的页表中查找逻辑地址va就必须通过调用页表操作相关函数找到包含va的页表项PTE通过该PTE的内容得知va所在的物理页面的首地址最后再通过计算va在页内的位移得到va最终对应的物理地址。

实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab2_1中所做的工作

$ git commit -a -m "my work on lab2_1 is done."

4.3 lab2_2 简单内存分配和回收

给定应用

  • user/app_naive_malloc.c
  1 /*
  2  * Below is the given application for lab2_2.
  3  */
  4
  5 #include "user_lib.h"
  6 #include "util/types.h"
  7
  8 struct my_structure {
  9   char c;
 10   int n;
 11 };
 12
 13 int main(void) {
 14   struct my_structure* s = (struct my_structure*)naive_malloc();
 15   s->c = 'a';
 16   s->n = 1;
 17
 18   printu("s: %lx, {%c %d}\n", s, s->c, s->n);
 19
 20   naive_free(s);
 21
 22   exit(0);
 23 }

该应用的逻辑非常简单首先分配一个空间内存页面来存放my_structure结构往my_structure结构的实例中存储信息打印信息并最终将之前所分配的空间释放掉。这里新定义了两个用户态函数naive_malloc()和naive_free(),它们最终会转换成系统调用,完成内存的分配和回收操作。

  • 先提交lab2_1的答案然后切换到lab2_2继承lab2_1以及之前实验所做的修改并make后的直接运行结果
//切换到lab2_2
$ git checkout lab2_2_allocatepage

//继承lab2_1以及之前的答案
$ git merge lab2_1_pagetable -m "continue to work on lab2_2"

//重新构造
$ make clean; make

//运行构造结果
$ spike ./obj/riscv-pke ./obj/app_naive_malloc
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080004000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: ./obj/app_naive_malloc
Application program entry point (virtual address): 0x0000000000010078
Switching to user mode...
s: 0000000000400000, {a 1}
You have to implement user_vm_unmap to free pages using naive_free in lab2_2.

System is shutting down with exit code -1.

从输出结果来看,s: 0000000000400000, {a 1}的输出说明分配内存已经做好也就是说naive_malloc函数及其内核功能的实现已完成且打印出了我们预期的结果。但是naive_free对应的功能并未完全做好。

实验内容

如输出提示所表明的那样需要完成naive_free对应的功能并获得以下预期的结果输出

$ spike ./obj/riscv-pke ./obj/app_naive_malloc
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080004000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: ./obj/app_naive_malloc
Application program entry point (virtual address): 0x0000000000010078
Switching to user mode...
s: 0000000000400000, {a 1}
User exit with code:0.
System is shutting down with exit code 0.

实验指导

一般来说应用程序执行过程中的动态内存分配和回收是操作系统中的堆Heap管理的内容。在本实验中我们实际上是为PKE操作系统内核实现一个简单到不能再简单的“堆”。为实现naive_free()的内存回收过程我们需要了解其对偶过程即内存是如何“分配”给应用程序并供后者使用的。为此我们先阅读kernel/syscall.c文件中的naive_malloc()函数的底层实现sys_user_allocate_page()

 42 uint64 sys_user_allocate_page() {
 43   void* pa = alloc_page();
 44   uint64 va = g_ufree_page;
 45   g_ufree_page += PGSIZE;
 46   user_vm_map((pagetable_t)current->pagetable, va, PGSIZE, (uint64)pa,
 47          prot_to_type(PROT_WRITE | PROT_READ, 1));
 48
 49   return va;
 50 }

这个函数在43行分配了一个首地址为pa的物理页面这个物理页面要以何种方式映射给应用进程使用呢第44行给出了pa对应的逻辑地址va = g_ufree_page并在45行对g_ufree_page进行了递增操作。最后在46--47行将pa映射给了va地址。这个过程中g_ufree_page是如何定义的呢我们可以找到它在kernel/process.c文件中的定义

 27 // points to the first free page in our simple heap. added @lab2_2
 28 uint64 g_ufree_page = USER_FREE_ADDRESS_START;

而USER_FREE_ADDRESS_START的定义在kernel/memlayout.h文件

 17 // start virtual address (4MB) of our simple heap. added @lab2_2
 18 #define USER_FREE_ADDRESS_START 0x00000000 + PGSIZE * 1024

可以看到在我们的PKE操作系统内核中应用程序执行过程中所动态分配类似malloc的内存是被映射到USER_FREE_ADDRESS_START4MB开始的地址的。那么这里的USER_FREE_ADDRESS_START对应图4.5中的用户进程的逻辑地址空间的哪个部分呢?这一点请读者自行判断并分析为什么是4MB以及能不能用其他的逻辑地址

以上了解了内存的分配过程后,我们就能够大概了解其反过程的回收应该怎么做了,大概分为以下步骤:

  • 找到一个给定va所对应的页表项PTE查找4.1.4节,看哪个函数能满足此需求);
  • 如果找到过滤找不到的情形通过该PTE的内容得知va所对应物理页的首地址pa
  • 回收pa对应的物理页并将PTE中的Valid位置为0。

本实验若出现M模式的非法异常unexpected exception happened in M-mode.,说明naive_malloc对应的系统调用未返回正确的逻辑地址。解决问题的方法是回头检查你在lab1_1中所做的答案

实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab2_2中所做的工作

$ git commit -a -m "my work on lab2_2 is done."

4.4 lab2_3 缺页异常

给定应用

  • user/app_sum_sequence.c
  1 /*
  2  * The application of lab2_3.
  3  */
  4
  5 #include "user_lib.h"
  6 #include "util/types.h"
  7
  8 //
  9 // compute the summation of an arithmetic sequence. for a given "n", compute
 10 // result = n + (n-1) + (n-2) + ... + 0
 11 // sum_sequence() calls itself recursively till 0. The recursive call, however,
 12 // may consume more memory (from stack) than a physical 4KB page, leading to a page fault.
 13 // PKE kernel needs to improved to handle such page fault by expanding the stack.
 14 //
 15 uint64 sum_sequence(uint64 n) {
 16   if (n == 0)
 17     return 0;
 18   else
 19     return sum_sequence( n-1 ) + n;
 20 }
 21
 22 int main(void) {
 23   // we need a large enough "n" to trigger pagefaults in the user stack
 24   uint64 n = 1000;
 25
 26   printu("Summation of an arithmetic sequence from 0 to %ld is: %ld \n", n, sum_sequence(1000) );
 27   exit(0);
 28 }

给定一个递增的等差数列:0, 1, 2, ..., n如何求该数列的和以上的应用给出了它的递归recursive解法。通过定义一个函数sum_sequence(n)将求和问题转换为sum_sequence(n-1) + n的问题。问题中n依次递减直至为0时令sum_sequence(0)=0。

  • 先提交lab2_2的答案然后切换到lab2_3、继承lab2_2及以前所做修改并make后的直接运行结果
//切换到lab2_3
$ git checkout lab2_3_pagefault

//继承lab2_2以及之前的答案
$ git merge lab2_2_allocatepage -m "continue to work on lab2_3"

//重新构造
$ make clean; make

//运行构造结果
$ spike ./obj/riscv-pke ./obj/app_sum_sequence
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080004000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: ./obj/app_sum_sequence
Application program entry point (virtual address): 0x0000000000010096
Switching to user mode...
handle_page_fault: 000000007fffdff8
You need to implement the operations that actually handle the page fault in lab2_3.

System is shutting down with exit code -1.

以上执行结果为什么会出现handle_page_fault呢这就跟我们给出的应用程序递归求解等差数列的和有关了。

递归解法的特点是函数调用的路径会被完整地保存在栈stack也就是说函数的下一次调用会将上次一调用的现场包括参数压栈直到n=0时依次返回到最开始给定的n值从而得到最终的计算结果。显然在以上计算等差数列的和的程序中n值给得越大就会导致越深的栈而栈越深需要的内存空间也就越多。

通过4.1.3节中对用户进程逻辑地址空间的讨论以及图4.5的图示我们知道应用程序最开始被载入并装配为用户进程它的用户态栈空间栈底在0x7ffff000即USER_STACK_TOP仅有1个4KB的页面。显然只要以上的程序给出的n值“足够”大就一定会“压爆”用户态栈。而以上运行结果中出问题的地方即handle_page_fault后出现的地址0x7fffdff8也恰恰在用户态栈所对应的空间。

以上分析表明,之所以运行./obj/app_sum_sequence会出现错误handle_page_fault是因为给sum_sequence()函数的n值太大把用户态栈“压爆”了。

实验内容

在PKE操作系统内核中完善用户态栈空间的管理使得它能够正确处理用户进程的“压栈”请求。

实验完成后的运行结果:

$ spike ./obj/riscv-pke ./obj/app_sum_sequence
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080004000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: ./obj/app_sum_sequence
Application program entry point (virtual address): 0x0000000000010096
Switching to user mode...
handle_page_fault: 000000007fffdff8
handle_page_fault: 000000007fffcff8
handle_page_fault: 000000007fffbff8
Summation of an arithmetic sequence from 0 to 1000 is: 500500
User exit with code:0.
System is shutting down with exit code 0.

实验指导

本实验需要结合lab1_2中的异常处理知识但要注意的是lab1_2中我们处理的是非法指令异常对该异常的处理足够操作系统将应用进程“杀死”。本实验中我们处理的是缺页异常app_sum_sequence.c执行的显然是“合法”操作不能也不应该将应用进程杀死。正确的做法是首先通过异常的类型判断我们处理的确实是缺页异常接下来判断发生缺页的是不是用户栈空间如果是则分配一个物理页空间最后将该空间通过vm_map“粘”到用户栈上以扩充用户栈空间。

另外lab1_2中处理的非法指令异常是在M模式下处理的原因是我们根本没有将该异常代理给S模式。但是对于本实验中的缺页异常是不是也是需要在M模式处理呢我们先回顾以下kernel/machine/minit.c文件中的delegate_traps()函数:

 55 static void delegate_traps() {
 56   // supports_extension macro is defined in kernel/riscv.h
 57   if (!supports_extension('S')) {
 58     // confirm that our processor supports supervisor mode. abort if it does not.
 59     sprint("S mode is not supported.\n");
 60     return;
 61   }
 62
 63   // macros used in following two statements are defined in kernel/riscv.h
 64   uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
 65   uintptr_t exceptions = (1U << CAUSE_MISALIGNED_FETCH) | (1U << CAUSE_FETCH_PAGE_FAULT) |
 66                          (1U << CAUSE_BREAKPOINT) | (1U << CAUSE_LOAD_PAGE_FAULT) |
 67                          (1U << CAUSE_STORE_PAGE_FAULT) | (1U << CAUSE_USER_ECALL);
 68
 69   // writes 64-bit values (interrupts and exceptions) to 'mideleg' and 'medeleg' (two
 70   // priviledged registers of RV64G machine) respectively.
 71   //
 72   // write_csr and read_csr are macros defined in kernel/riscv.h
 73   write_csr(mideleg, interrupts);
 74   write_csr(medeleg, exceptions);
 75   assert(read_csr(mideleg) == interrupts);
 76   assert(read_csr(medeleg) == exceptions);
 77 }

而在本实验的应用中产生缺页异常的本质还是应用往未被映射的内存空间“写”以及后续的访问所导致的所以CAUSE_STORE_PAGE_FAULT是我们应该关注的异常。通过阅读delegate_traps()函数我们看到该函数显然已将缺页异常CAUSE_STORE_PAGE_FAULT代理给了S模式所以接下来我们就应阅读kernel/strap.c文件中对于这类异常的处理

 52 void handle_user_page_fault(uint64 mcause, uint64 sepc, uint64 stval) {
 53   sprint("handle_page_fault: %lx\n", stval);
 54   switch (mcause) {
 55     case CAUSE_STORE_PAGE_FAULT:
 56       // TODO (lab2_3): implement the operations that solve the page fault to
 57       // dynamically increase application stack.
 58       // hint: first allocate a new physical page, and then, maps the new page to the
 59       // virtual address that causes the page fault.
 60       panic( "You need to implement the operations that actually handle the page fault in lab 2_3.\n" );
 61
 62       break;
 63     default:
 64       sprint("unknown page fault.\n");
 65       break;
 66   }
 67 }

这里,我们找到了之前运行./obj/app_sum_sequence出错的地方我们只需要改正这一错误实现缺页处理使得程序获得正确的输出就好。实现缺页处理的思路如下

  • 通过输入的参数stval存放的是发生缺页异常时程序想要访问的逻辑地址判断缺页的逻辑地址在用户进程逻辑地址空间中的位置看是不是比USER_STACK_TOP小且比我们预设的可能的用户栈的最小栈底指针要大这里我们可以给用户栈空间一个上限例如20个4KB的页面若满足则为合法的逻辑地址本例中不必实现此判断默认逻辑地址合法
  • 分配一个物理页将所分配的物理页面映射到stval所对应的虚拟地址上。

实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab2_3中所做的工作

$ git commit -a -m "my work on lab2_3 is done."

4.5 lab2_challenge1 复杂缺页异常(难度:★☆☆☆☆)

给定应用

  • user/app_sum_sequence.c

     1	/*
    2	 * The application of lab2_4.
    3	 * Based on application of lab2_3.
    4	 */
    5	
    6	#include "user_lib.h"
    7	#include "util/types.h"
    8	
    9	//
    10	// compute the summation of an arithmetic sequence. for a given "n", compute
    11	// result = n + (n-1) + (n-2) + ... + 0
    12	// sum_sequence() calls itself recursively till 0. The recursive call, however,
    13	// may consume more memory (from stack) than a physical 4KB page, leading to a page fault.
    14	// PKE kernel needs to improved to handle such page fault by expanding the stack.
    15	//
    16	uint64 sum_sequence(uint64 n, int *p) {
    17	  if (n == 0)
    18	    return 0;
    19	  else
    20	    return *p=sum_sequence( n-1, p+1 ) + n;
    21	}
    22	
    23	int main(void) {
    24	  // FIRST, we need a large enough "n" to trigger pagefaults in the user stack
    25	  uint64 n = 1024;
    26	
    27	  // alloc a page size array(int) to store the result of every step
    28	  // the max limit of the number is 4kB/4 = 1024
    29	
    30	  // SECOND, we use array out of bound to trigger pagefaults in an invalid address
    31	  int *ans = (int *)naive_malloc();
    32	
    33	  printu("Summation of an arithmetic sequence from 0 to %ld is: %ld \n", n, sum_sequence(n+1, ans) );
    34	
    35	  exit(0);
    36	}
    
    

    程序思路基本同lab2_3一致对给定n计算0到n的和但要求将每一步递归的结果保存在数组ans中。创建数组时我们使用了当前的malloc函数申请了一个页面4KB的大小对应可以存储的个数上限为1024。在函数调用时我们试图计算1025求和首先由于n足够大所以在函数递归执行时会触发用户栈的缺页你需要对其进行正确处理确保程序正确运行其次1025在最后一次计算时会访问数组越界地址由于该处虚拟地址尚未有对应的物理地址映射因此属于非法地址的访问这是不被允许的对于这种缺页异常应该提示用户并退出程序执行。如上的应用预期输出如下

    In m_start, hartid:0
    HTIF is available!
    (Emulated) memory size: 2048 MB
    Enter supervisor mode...
    PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
    free physical memory address: [0x000000008000e000, 0x0000000087ffffff] 
    kernel memory manager is initializing ...
    KERN_BASE 0x0000000080000000
    physical address of _etext is: 0x0000000080004000
    kernel page table is on 
    User application is loading.
    user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000 
    Application: ./obj/app_sum_sequence
    Application program entry point (virtual address): 0x00000000000100da
    Switching to user mode...
    handle_page_fault: 000000007fffdff8
    handle_page_fault: 000000007fffcff8
    handle_page_fault: 000000007fffbff8
    handle_page_fault: 000000007fffaff8
    handle_page_fault: 000000007fff9ff8
    handle_page_fault: 000000007fff8ff8
    handle_page_fault: 000000007fff7ff8
    handle_page_fault: 000000007fff6ff8
    handle_page_fault: 0000000000401000
    this address is not available!
    System is shutting down with exit code -1.
    

根据结果可以看出:前八个缺页是由于函数递归调用引起的,而最后一个缺页是对动态申请的数组进行越界访问造成的,访问非法地址,程序报错并退出。

实验内容

本实验为挑战实验基础代码将继承和使用lab2_3完成后的代码

  • 先提交lab2_3的答案然后切换到lab2_challenge1_pagefaults、继承lab2_3中所做修改
//切换到lab2_challenge1_pagefault
$ git checkout lab2_challenge1_pagefaults

//继承lab2_3以及之前的答案
$ git merge lab2_3_pagefault -m "continue to work on lab2_challenge1"

注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。**同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。

  • 本实验的具体要求为通过修改PKE内核包括machine文件夹下的代码使得对于不同情况的缺页异常进行不同的处理。
  • 文件名规范:需要包含路径,如果是用户源程序发生的错误,路径为相对路径,如果是调用的标准库内发生的错误,路径为绝对路径。

实验指导

  • 你对内核代码的修改可能包含以下内容:
    • 修改进程的数据结构以对虚拟地址空间进行监控。
    • 修改kernel/strap.c中的异常处理函数。对于合理的缺页异常扩大内核栈大小并为其映射物理块对于非法地址缺页报错并退出程序。

注意完成挑战任务对两种缺页进行实现后读者可对任意n验证由于目前的malloc函数是申请一个页面大小所以对于n<=1024只会产生第一种缺页并打印正确的计算结果对于n>=1025则会因为访问非法地址退出请读者验证自己的实现。

另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。

4.6 lab2_challenge2 堆空间管理 (难度:★★★★☆)

给定应用

  • user/app_singlepageheap.c
  1 /*
  2  * Below is the given application for lab2_challenge2_singlepageheap.
  3  * This app performs malloc memory.
  4  */
  5
  6 #include "user_lib.h"
  7 //#include "util/string.h"
  8
  9 typedef unsigned long long uint64;
 10
 11 char* strcpy(char* dest, const char* src) {
 12   char* d = dest;
 13   while ((*d++ = *src++))
 14     ;
 15   return dest;
 16 }
 17 int main(void) {
 18
 19   char str[20] = "hello, world!!!";
 20   char *m = (char *)better_malloc(100);
 21   char *p = (char *)better_malloc(50);
 22   if((uint64)p - (uint64)m > 512 ){
 23     printu("you need to manage the vm space precisely!");
 24     exit(-1);
 25   }
 26   better_free((void *)m);
 27
 28   strcpy(p,str);
 29   printu("%s\n",p);
 30   char *n = (char *)better_malloc(50);
 31
 32   if(m != n)
 33   {
 34     printu("your malloc is not complete.\n");
 35     exit(-1);
 36   }
 37 //  else{
 38 //    printu("0x%lx 0x%lx\n", m, n);
 39 //  }
 40   exit(0);
 41   return 0;
 42 }

以上程序先利用better_malloc分别申请100和50个字节的一个物理页的内存然后使用better_free释放掉100个字节向50个字节中复制一串字符串进行输出。原本的pke中malloc的实现是非常简化的一次直接分配一个页面你的挑战任务是修改内核(包括machine文件夹下)的代码使得应用程序的malloc能够在一个物理页中分配并对各申请块进行合理的管理,如上面的应用预期输出如下:

In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x0000000080008000, PKE kernel size: 0x0000000000008000 .
free physical memory address: [0x0000000080008000, 0x0000000087ffffff]
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080005000
kernel page table is on
User application is loading.
user frame 0x0000000087fbc000, user stack 0x000000007ffff000, user kstack 0x0000000087fbb000
Application: obj/app_singlepageheap
Application program entry point (virtual address): 0x000000000001008a
Switch to user mode...
hello, world!!!
User exit with code:0.
System is shutting down with exit code 0.

通过应用程序和对应的预期结果可以看出两次申请的空间在同一页面并且释放第一块时不会释放整个页面所以需要你设计合适的数据结构对各块进行管理使得better_malloc申请的空间更加“紧凑”。

实验内容

本实验为挑战实验基础代码将继承和使用lab2_3完成后的代码

  • 先提交lab2_3的答案然后切换到lab2_challenge2、继承lab2_3中所做修改
//切换到lab2_challenge2_singlepageheap
$ git checkout lab2_challenge2_singlepageheap

//继承lab2_challenge1以及之前的答案
$ git merge lab2_3_pagefault -m "continue to work on lab2_challenge2"

注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。**同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。

  • 本实验的具体要求为通过修改PKE内核包括machine文件下的代码实现优化后的malloc函数使得应用程序两次申请块在同一页面并且能够正常输出存入第二块中的字符串"hello world"。
  • 文件名规范:需要包含路径,如果是用户源程序发生的错误,路径为相对路径,如果是调用的标准库内发生的错误,路径为绝对路径。

实验指导

  • 为完成该挑战你需要对进程的虚拟地址空间进行管理建议参考Linux的内存分配策略从而实现malloc。

  • 你对内核代码的修改可能包含以下内容:

    • 增加内存控制块数据结构对分配的内存块进行管理。

    • 修改process的数据结构以扩展对虚拟地址空间的管理后续对于heap的扩展需要对新增的虚拟地址添加对应的物理地址映射。

    • 设计函数对进程的虚拟地址空间进行管理借助以上内容具体实现heap扩展。

    • 设计malloc函数和free函数对内存块进行管理。

注意本挑战的创新思路及难点就在于分配和回收策略的设计对应malloc和free读者应同时思考两个函数如何实现基于紧凑性和高效率的设计目标设计自己认为高效的分配策略。完成设计后请读者另外编写应用设计不同场景使用better_malloc和better_free函数验证挑战目标以及对自己实现进行检测。

另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。

4.7 lab2_challenge3 多核内存管理(难度:★★☆☆☆)

给定应用

  • user/app_alloc0.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
#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,4app_alloc1.c会依次写入并输出5,6,7,8,9

实验内容

本实验为挑战实验基础代码将继承和使用lab2_3完成后的代码

  • 先提交lab2_3的答案然后切换到lab2_challenge3_multicoremem继承lab2_3注意不是继承lab2_challenge1_pagefaults和lab2_challenge2_singlepageheapPKE的挑战实验之间无继承关联)中所做修改:
//切换到lab2_challenge3_multicoremem
$ git checkout lab2_challenge3_multicoremem

//继承lab2_3以及之前的答案
$ git merge lab2_3_pagefault -m "continue to work on lab2_challenge3"

特别注意本实验需要借助你在lab1_challenge3_multicore中实现的多核启动及运行机制因此在进行本实验之前你应该在本实验的基础代码上重做你在lab1_challenge3_multicore中的改动不要直接使用git merge lab1_challenge3_multicore

注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。**同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。

  • 在lab1_challenge3中你已经实现了一个不支持虚拟内存的简单的多核操作系统。现在在lab2中因为虚拟内存概念的引入需要你为这个简单操作系统添加额外的多核内存管理。
  • 如同lab1_challenge3kernel/config.h中的NCPU规定了操作系统内核支持的核数在本实验设置为2即要求你能够正确执行spike -p2 riscv-pke app_alloc0 app_alloc1,每个核分别执行一个进程,进行内存的分配与释放。
  • 注意,每个进程分配得到的虚拟地址应当是连续的,并且不同核上的进程不应该分配到同一个物理页
  • 最终你的输出应当如下所示:
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): 0x0000000000010078
hartid = 1: Application program entry point (virtual address): 0x0000000000010078
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
=== user 0: 0
>>> user alloc 1 @ vaddr 0x00404000
=== user 0: 1
>>> user 1: 5
=== user 0: 2
>>> user 1: 6
=== user 0: 3
>>> user 1: 7
=== user 0: 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,表示是用户进程在申请物理内存。但是这个代码目前不支持多核,你需要进行相应修改使其适配多核,并能够正确打印出每个核上用户进程申请的物理页地址。

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得到的虚拟内存页不连续。

在实验的基础代码中,有一些打印分配内存地址的代码,你需要将这部分代码改为支持多核执行的版本,然后便可以阅读执行后的输出来判断是否为每个进程分配得到的物理内存页和对应的虚拟内存。