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.

14 KiB

第四章实验3物理内存管理

4.1 实验内容

实验要求:了解物理内存,管理物理内存。

4.1.1 练习一OS内存的初始化过程

"pk/mmap.c"内有 pk_vm_init()函数阅读该函数了解OS内存初始化的过程。

364  uintptr_t pk_vm_init()
365  {
366      // HTIF address signedness and va2pa macro both cap memory size to 2 GiB
         //设置物理内存大小
367      mem_size = MIN(mem_size, 1U << 31);
              //计算物理页的数量
368      size_t mem_pages = mem_size >> RISCV_PGSHIFT;
369      free_pages = MAX(8, mem_pages >> (RISCV_PGLEVEL_BITS-1));
370          
              //_end为内核结束地址
371      extern char _end;
372      first_free_page = ROUNDUP((uintptr_t)&_end, RISCV_PGSIZE);
373      first_free_paddr = first_free_page + free_pages * RISCV_PGSIZE;
374              
       //映射内核的物理空间
375      root_page_table = (void*)__page_alloc();
376      __map_kernel_range(DRAM_BASE, DRAM_BASE, first_free_paddr - DRAM_BASE, PROT_READ|PROT_WRITE|PROT_EXEC);
377              
       //crrent.mmap_max: 0x000000007f7ea000
378      current.mmap_max = current.brk_max =
379       MIN(DRAM_BASE, mem_size - (first_free_paddr - DRAM_BASE));
380          
              //映射用户栈
381      size_t stack_size = MIN(mem_pages >> 5, 2048) * RISCV_PGSIZE;
382      size_t stack_bottom = __do_mmap(current.mmap_max - stack_size, stack_size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED , 0, 0);
383      kassert(stack_bottom != (uintptr_t)-1);
384      current.stack_top = stack_bottom + stack_size;
385
              //开启分页
386      flush_tlb();
387      write_csr(sptbr, ((uintptr_t)root_page_table >> RISCV_PGSHIFT) | SATP_MODE_CHOICE);
388
              //分配内核栈空间,
389      uintptr_t kernel_stack_top = __page_alloc() + RISCV_PGSIZE;
390      return kernel_stack_top;
391  }

以上代码中我们给出了大体的注释请根据以上代码尝试画出RISCV的物理内存结构图。

4.1.2 练习二first_fit内存页分配算法需要编程

在"pk/pmm.c" 中,我们实现了对物理内存的管理。

构建了物理内存页管理器框架struct pmm_manager结构如下

135 const struct pmm_manager default_pmm_manager = {

136 .name = "default_pmm_manager",

137 .init = default_init,

138 .init_memmap = default_init_memmap,

139 .alloc_pages = default_alloc_pages,

140 .free_pages = default_free_pages,

141 .nr_free_pages = default_nr_free_pages,

142 .pmm_check = basic_check,

143 };

默认的内存管理器有如下属性:

l name:内存管理器的名字

l init:对内存管理算法所使用的数据结构进行初始化

l init_ memmap:根据物理内存设置内存管理算法的数据结构

l alloc_pages分配物理页

l free_pages释放物理页

l nr_free_pages空闲物理页的数量

l pmm_check :检查校验函数

参考已经实现的函数完成default_alloc_pages()和default_free_pages(),实现first_fit内存页分配算法。

first_fit分配算法需要维护一个查找有序地址按从小到大排列空闲块以页为最小单位的连续地址空间的数据结构而双向链表是一个很好的选择。pk/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作所以需要了解如何使用这个文件提供的各种函数从而可以完成对双向链表的初始化/插入/删除等。

你可以使用python脚本检查你的输出

./pke-lab3

若得到如下输出,那么恭喜你,你已经成功完成了实验三!!!

build pk : OK

running app3 m2048 : OK

test3_m2048 : OK

running app3 m1024 : OK

test3_m1024 : OK

Score: 20/20

4.2 基础知识

4.2.1 物理内存空间与空间编址

计算机的存储结构可以抽象的看做由N个连续的字节组成的数组。想一想在数组中我们如何找到一个元素对了是下标那么我们如何在内存中如何找打一个元素呢自然也是下标。这个下标的起始位置和位数由机器本身决定我们称之为“物理地址”。

在riscv中内存地址是从0x80000000 开始的。在pke的连接文件pke.lds中我们可以看到这样两行

14 /* Begining of code and text segment */

15 . = 0x80000000;

至于内存的大小,还记得实验二中的-m选项吗spike模拟器可以通过-m选项配置物理内存的大小。

现在思考一下问题首先为什么需要物理内存的管理这个问题可以用另一个问题回答当程序如malloc申请一段内存空间的时候你如何准确的给出一片符合大小要求的且安全可用的内存空间。

其次,我们现在管理的可用内存是那一部分? 代理内核的本质也是一段程序他本身是需要内存空间的而这一段空间自然不能再被分配。除去内核本身占的空间内核可支配的物理空间从0x80016000开始大小在PKE设定为8M2048个页面故而供内核支配的的内存的范围为first_free_page~first_free_paddr。如下图所示。

KERNTOP------->+---------------------------------+ 0x80816000

(first_free_paddr) | |

| Kern Physical Memory |

| | 8M 2048pages

(first_free_page) | |

KERNBSE -----> +---------------------------------+ 0x80016000

| Kern Text/Data/BBS |

KERN ------>+---------------------------------+ 0x80000000

再往上便是我们可以支配的空间了KERNTOP~Top Memory

kernel/user

4G ------------> +-------------------------------------------------+

| |

| Empty Memory (*) |

| |

Top Memmory ---> +----------------------------------------------+ 随物理内存大小移动

| |

| User Remapped Memory |

| |

| |

first_free_paddr->+------------------------------------------------------+ 0x80816000

最后我们来看物理内存分配的单位操作系统中物理页是物理内存分配的基本单位。一个物理页的大小是4KB我们使用结构体Page来表示其结构如图

struct Page {

sint_t ref;

uint_t flags;

uint_t property;

list_entry_t page_link;

};

l ref表示这样页被页表的引用记数

l flags表示此物理页的状态标记

l property用来记录某连续内存空闲块的大小即地址连续的空闲页的个数

l page_link是维持空闲物理页链表的重要结构。

Page结构体对应着物理页我们来看Page结构体同物理地址之间是如何转换的。首先我们需要先了解一下物理地址。

图4.1 RISCV64 物理地址

总的来说物理地址分为两部分页号PPN+offset

页号可以理解为物理页的编码而offset则为页内偏移量。现在考虑一下12位的offset对应的内存大小是多少呢

2<<12=4096也就是4KB还记得我们讲过的物PA理页大小是多少吗没错是4KB。12位的offset设计便是由此而来。

有了物理地址PA这一概念那PA和Pages结构体又是如何转换

实际上在初始化空闲页链表之前系统会定义一个Page结构体的数组而链表的节点也正是来自于这些数组这个数组的每一项代表着一个物理页而且它们的数组下标就代表着每一项具体代表的是哪一个物理页就如下图所示

3.2.2 中断的处理过程

考虑一下当程序执行到中断之前程序是有自己的运行状态的例如寄存器里保持的上下文数据。当中断发生硬件在自动设置完中断原因和中断地址后就会调转到中断处理程序而中断处理程序同样会使用寄存器于是当程序从中断处理程序返回时需要保存需要被调用者保存的寄存器我们称之为callee-saved寄存器。

在PK的machine/minit.c中间中便通过delegate_traps()将部分中断及同步异常委托给S模式。同学们可以查看具体是哪些中断及同步异常

43 // send S-mode interrupts and most exceptions straight to S-mode

44 static void delegate_traps()

45 {

46 if (!supports_extension('S'))

47 return;

48

49 uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;

50 uintptr_t exceptions =

51 (1U << CAUSE_MISALIGNED_FETCH) |

52 (1U << CAUSE_FETCH_PAGE_FAULT) |

53 (1U << CAUSE_BREAKPOINT) |

54 (1U << CAUSE_LOAD_PAGE_FAULT) |

55 (1U << CAUSE_STORE_PAGE_FAULT) |

56 (1U << CAUSE_USER_ECALL);

57

58 write_csr(mideleg, interrupts);

59 write_csr(medeleg, exceptions);

60 assert(read_csr(mideleg) == interrupts);

61 assert(read_csr(medeleg) == exceptions);

62 }

这里介绍一下RISCV的中断委托机制在默认的情况下所有的异常都会被交由机器模式处理。但正如我们知道的那样大部分的系统调用都是在S模式下处理的因此RISCV提供了这一委托机制可以选择性的将中断交由S模式处理从而完全绕过M模式。

接下我们继续看S模式下的中断处理。在pk目录下的pk.c文件中的boot_loader函数中将&trap_entry写入了stvec寄存器中stvec保存着发生异常时处理器需要跳转到的地址也就是说当中断发生我们将跳转至trap_entry现在我们继续跟踪trap_entry。trap_entry在pk目录下的entry.S中其代码如下

60 trap_entry:

61 csrrw sp, sscratch, sp

62 bnez sp, 1f

63 csrr sp, sscratch

64 1:addi sp,sp,-320

65 save_tf

66 move a0,sp

67 jal handle_trap

在61行交换了sp与sscratch的值这里是为了根据sscratch的值判断该中断是来源于U模式还是S模式。

如果sp也就是传入的sscratch值不为零则跳转至64行若sscratch的值为零则恢复原sp中的值。这是因为当中断来源于S模式是sscratch的值为0sp中存储的就是内核的堆栈地址。而当中断来源于U模式时sp中存储的是用户的堆栈地址sscratch中存储的则是内核的堆栈地址需要交换二者是sp指向内核的堆栈地址。

接着在64,65行保存上下文最后跳转至67行处理trap。handle_trap在pk目录下的handlers.c文件中代码如下

112 void handle_trap(trapframe_t* tf)

113 {

114 if ((intptr_t)tf->cause < 0)

115 return handle_interrupt(tf);

116

117 typedef void (trap_handler)(trapframe_t);

118

119 const static trap_handler trap_handlers[] = {

120 [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch,

121 [CAUSE_FETCH_ACCESS] = handle_instruction_access_fault,

122 [CAUSE_LOAD_ACCESS] = handle_load_access_fault,

123 [CAUSE_STORE_ACCESS] = handle_store_access_fault,

124 [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch,

125 [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction,

126 [CAUSE_USER_ECALL] = handle_syscall,

127 [CAUSE_BREAKPOINT] = handle_breakpoint,

128 [CAUSE_MISALIGNED_LOAD] = handle_misaligned_load,

129 [CAUSE_MISALIGNED_STORE] = handle_misaligned_store,

130 [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load,

131 [CAUSE_STORE_PAGE_FAULT] = handle_fault_store,

132 };

handle_trap函数中实现了S模式下各类中断的处理。可以看到代码的126行就对应着系统调用的处理handle_syscall的实现如下

100 static void handle_syscall(trapframe_t* tf)

101 {

102 tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13],

103 tf->gpr[14], tf->gpr[15], tf->gpr[17]);

104 tf->epc += 4;

105 }

还记得我们在例3.1中是将中断号写入x17寄存器嘛其对应的就是这里do_syscall的最后一个参数我们跟踪进入do_syscall函数其代码如下

313 long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n)

314 {

315 const static void* syscall_table[] = {

316 // your code here:

317 // add get_init_memsize syscall

318 [SYS_init_memsize ] = sys_get_init_memsize,

319 [SYS_exit] = sys_exit,

320 [SYS_exit_group] = sys_exit,

321 [SYS_read] = sys_read,

322 [SYS_pread] = sys_pread,

323 [SYS_write] = sys_write,

324 [SYS_openat] = sys_openat,

325 [SYS_close] = sys_close,

326 [SYS_fstat] = sys_fstat,

327 [SYS_lseek] = sys_lseek,

328 [SYS_renameat] = sys_renameat,

329 [SYS_mkdirat] = sys_mkdirat,

330 [SYS_getcwd] = sys_getcwd,

331 [SYS_brk] = sys_brk,

332 [SYS_uname] = sys_uname,

333 [SYS_prlimit64] = sys_stub_nosys,

334 [SYS_rt_sigaction] = sys_rt_sigaction,

335 [SYS_times] = sys_times,

336 [SYS_writev] = sys_writev,

337 [SYS_readlinkat] = sys_stub_nosys,

338 [SYS_rt_sigprocmask] = sys_stub_success,

339 [SYS_ioctl] = sys_stub_nosys,

340 [SYS_getrusage] = sys_stub_nosys,

341 [SYS_getrlimit] = sys_stub_nosys,

342 [SYS_setrlimit] = sys_stub_nosys,

343 [SYS_set_tid_address] = sys_stub_nosys,

344 [SYS_set_robust_list] = sys_stub_nosys,

345 };

346

347 syscall_t f = 0;

348

349 if (n < ARRAY_SIZE(syscall_table))

350 f = syscall_table[n];

351 if (!f)

352 panic("bad syscall #%ld!",n);

353

354 return f(a0, a1, a2, a3, a4, a5, n);

355 }

do_syscall中通过传入的系统调用号n查询syscall_table得到对应的函数并最终执行系统调用。