rCore 学习笔记 - 第四章

分页内存管理

为了避免应用的恶意行为,以及方便应用的地址管理,内核要负责物理内存的管理,并提供一层抽象的接口,也就是地址空间。每个应用的视角下,它们各自拥有一大块独占的地址空间。应用使用虚拟地址来索引地址空间的数据,经过 MMU 映射到物理地址。

现在主流的机制是分页内存管理,将用户程序分成若干页面 (Page), 将物理地址分为若干个页帧 (Frame),每个页面和页帧都有一个编号,通过页表 (Page Table) 建立映射关系。

分页机制的启用需要修改 satp 这个寄存器。对于 RISC-V 64,前四位是 MODE,用于指定页表实现方式,设为 0 的时候所有访存为物理地址,设为 8 的时候启用 Sv39 模式,这会启用三级页表,使用 39 位的虚拟地址,前 27 位分为 3 段,分别是各级页表的索引,最后 12 位是页内偏移,这个字段还可以设为别的值,启用别的模式,不过 rCore 的实现中似乎没用上;中间 16 位是 ASID,现在还用不上;后 44 位 PPN 是页表所在的物理页帧,由于每个页面大小是 4 KiB,每级页表占一个页面,所以大小也是 4 KiB,每项 8 B,总共可以存 512 个项,正好对应虚拟地址的其中 9 位。在采用 Sv39 分页模式的时候,实际上只有 39 位的地址是有效的。

FrameTracker 可以看作对于裸页帧的 RAII 包装,而 FRAME_ALLOCATOR 负责管理页帧,当前的实现是 StackFrameAllocator,优先会从回收的页帧里进行重分配,不够了再利用预留的,注意这里返回的直接是物理地址。PageTable 是页表的具体实现,提供了将虚拟地址和物理地址映射 / 解除映射的功能。

当启用分页机制之后,连内核自己访问的地址,也变成了虚拟地址,如果不建立好映射关系,就会出现问题。教程中给出了一种方法——恒等映射:在开启分页之前先把一块区域的虚拟地址和物理地址建立恒等映射,这样在启用分页之后,对于内核来说,访问的虚拟地址就是物理地址了,不会受到分页的影响。

MapArea 代表逻辑段,是一段连续地址的虚拟内存,一个程序的地址空间用 MemorySet 管理,MemorySet 包含了若干 MapArea,用 RAII 思想做内存管理,让 MemorySet 生命周期结束后,自动回收物理页帧。

有个地方让我感到奇怪,就是实际上 MapArea 在进行 new 时,只是对一些参数进行了设置,并没有实际建立虚拟地址和物理地址的映射关系,而是在 MemorySet 把它 push 进去的时候,再调用 MapAreamap 方法进行配置。我不太理解为什么要把这两个东西进行耦合。我觉得,MapAreamapunmap 名字有误导性,实际上应当是应用设置好的操作。我的理解是,在这里,MapArea 只是一种区域信息的描述,实际上 MemorySet 是真正的管理者,而 PageTable 的细节被抽象掉了。而在硬件访问的时候,就无视高级语言做的这些设计了,直接根据页表的数据来做访问。不知道还有没有其他的考量。

之前的代码也要发生变化,比如 trap.S 里面。关键点在于换地址空间。进入 __alltraps 之后,先是把寄存器保存在 TrapContext 里,然后把应用初始化时期就设置好的,内核地址空间 token、trap handler 虚拟地址、内核栈顶地址等加载进寄存器,在 U 模式这些只是地址,没权限访问,__alltraps 把这些加载进寄存器之后,就可以调用 trap handler 来处理异常了。这个跳板页的设计很有意思,由于内核地址空间和用户地址空间不互通,切换地址空间后的那一刻,pc 简单地指向下一条指令,它是个虚拟地址,如果地址空间不能连续,那么这个地址就不知道指到哪里去了。为此,在用户地址空间的顶部,有一个跳板页,在内核地址空间的同样位置也有这样的跳板页,trap.S 就放在这页里面,它们都是一样映射的。这样,在跳板页中,可以确保指令的执行是连续的。

最后看一下内存结构。由于恒等映射,内核的虚拟地址就是物理地址。最上方是跳板页。接下来大片的空闲区域,在 TASK_MANAGER 初始化的时候,在这片区域一次性为所有用户程序创建好了内核栈。在创建每个内核栈的时候会将栈的区域进行映射,页帧的分配是交由 FRAME_ALLOCATOR 管理的,后续用户需要内存的时候,也是走的 FRAME_ALLOCATOR,而它已经把物理地址较高的区域分配给了程序的内核栈,所以不会出现重叠。FRAME_ALLOCATOR 管理的区域,从标记的内核区域截止位置 ekernelMEMORY_ENDekernel 往下,就是运行前就决定的内核区域了。内核在 .bss 段里创建了内核堆和启动栈,.bss 段下方是 .data.rodata.text 等。用户程序的虚拟地址空间中,最上面的跳板页比较特殊,因为是和内核跳板页一样恒等映射的。它下面是 TrapContext,在创建任务的时候会记录下它的物理页号,在内核中就可以直接访问这个页号,对它的值进行更改。再往下就是用户程序自己定义的了,没什么需要解释的了,只需要知道,这些东西的物理地址,实际上位于从 ekernel 到最后一个内核栈最下方为止的这一片区域,由 FRAME_ALLOCATOR 进行统一管理。

作业实现


rCore 学习笔记 - 第四章
http://xiao-h.com/2025/04/09/rCore-0x04/
作者
小H
发布于
2025年4月9日
许可协议