本文是参加2025春夏季开源操作系统训练营时对第二阶段文档第4章做的笔记。
文档:https://learningos.cn/rCore-Camp-Guide-2025S
完整版文档:https://rcore-os.cn/rCore-Tutorial-Book-v3
本章将进一步实现虚拟内存机制,主要是为了隔离性。
目前为止我们的操作系统中只有静态分配的功能,缺少灵活性,需要进一步实现动态内存分配的能力。这需要操作系统提供堆和与堆相关的一些功能:分配和释放内存的接口、空闲空间管理的算法、建立在堆上的数据结构。
Rust的标准库中有许多堆数据结构,主要是基于智能指针实现的,智能指针是对堆上数据进行管理的,除了地址以外还有一些其它信息。在智能指针的基础上实现了例如 Vec
, BTreeMap
等数据结构。
要在操作系统中实现上述的功能,需要利用 alloc
库定义的接口来实现基本的动态内存分配器。实现类似C语言中的 malloc
和 free
接口,然后根据 alloc
留好的接口提供全局动态内存分配器。
Lec28-31 Virtual Memory, IO#Virtual Memory, 和OSTEP的13-16章
SV39中,通过修改 satp
寄存器启用分页功能,satp
寄存器分为3个字段,MODE
, ASID
, PPN
。
MODE
控制CPU使用哪种页表实现,包括直接访问物理地址和启用分页机制。ASID
表示地址空间标识符,与进程有关。PPN
存的是根页表所在的物理页号。SV39的地址格式中,虚拟地址39位,物理地址56位;虚拟地址中包含27位的虚拟页号和12位的页内偏移;物理地址中包含44位的物理页号和12位的页内偏移。这是因为单个页面的大小为4kb,需要用12位字节地址表示。地址转换是按页为单位进行的,页内偏移部分相同。其中,虚拟地址会被按符号位扩展到64位,只有最低的256GiB(当第 38 位为0时)以及最高的256GiB(当第38位为1时)是可能通过MMU检查的。
在一个4kb大小的页表中,每8个字节(64位)是一个页表项,用到了54位,包括物理页号、和一些权限位。
页表可以使用线性表简单实现,但是这样整段虚拟地址对应的页表都要放到内存中,会造成严重的浪费。优化可以采用按需分配,有多少已分配的虚拟页号,就维护多大的映射。可以使用多级页表,类似于一棵多叉树。SV39中采用的是三级页表,具体的查找过程如下:
PPN用于查找物理页,可能是下一级页表或最终的物理页号;VPN分为3部分,每部分长9位,用于在一个页内查找页表项。最后的PPN和虚拟地址中的偏移段拼在一起就是最终的56位物理地址。
TLB,即地址转换过程中的缓存Lec28-31 Virtual Memory, IO#Translation Lookaside Buffers,在更换根页表后要使用 sfence.vma
指令刷新。
在整块地址空间中,从起始地址到 ekernel
中是恒等映射,因此从 ekernel
到终止物理地址之间的空间,是用于需要存放应用数据或扩展应用的多级页表时分配空闲的物理页帧。
rcore
使用了一种比较简单的栈式物理页帧管理策略:
1 | // os/src/mm/frame_allocator.rs |
最终公开的 frame_alloc
接口的返回值类型并不是 FrameAllocator
要求的物理页号 PhysPageNum
,而是将其进一步包装为一个 FrameTracker
,将生命周期绑定到这个变量上。当一个 FrameTracker
生命周期结束被编译器回收的时候,将这个物理页帧会回收到分配器中。
对页表的管理,建立了一个 PageTable
结构体,其中包含了根页表页的 PPN
和页表所有的节点的物理页帧,然后为它实现了 map
, unmap
, find_pte_create
, find_pte
等函数,原文档写的非常详细。目前的实现为了简单起见,在物理页帧耗尽时只会退出,不会进行换页等复杂操作。
刚才的 PageTable
数据结构只能维护一个地址转换关系,内核还创建了一个 MapArea
数据结构,用于描述一段连续地址的虚拟内存:
1 | // os/src/mm/memory_set.rs |
记录了虚拟页号的范围、物理页帧的集合、映射的类型和这个段的权限,这里的物理页帧是作为数据页的页帧,与上面的页表页的页帧不同,即作为页表页的物理页帧的生命周期绑定在 PageTable
中,作为数据页的物理页帧的生命周期绑定在 MapArea
中。
然后在这个 MapArea
的基础上,建立了一个 MemorySet
数据结构作为地址空间:
1 | // os/src/mm/memory_set.rs |
存储了整个页表和这个地址空间中的所有逻辑段,在这个结构体的基础上建立了内核的地址空间和每个应用的地址空间,每个应用的地址空间保存在它的TCB中。
内核地址空间的高256GB是跳板页和每个应用的内核栈,内核栈之间还有一个保护页防止溢出;低256GB是内核自己的四个逻辑段 .text/.rodata/.data/.bss
,还有上面说的空闲空间。
用户地址空间的高256GB是跳板页和Trap上下文页;低256GB是自己的逻辑段和用户栈。
在操作系统刚刚启动时,还是使用 lazy_staic!
宏包裹一个全局变量 Arc<UPSafeCell<MemorySet>>
,用作内核的地址空间,然后填 satp
寄存器,填入当前页表的根节点的PPN,从这一刻开始 SV39 分页模式就被启用了。由于内核的地址空间采用的是恒等映射,因此启用分页模式前后的指令地址是连续的,不会发生问题。
内核和应用的地址空间,最高的虚拟页面都是一个跳板 Trampoline
,跳板的作用是存放 trap.S
的代码,这是因为trap会切换用户态到内核态或相反,这需要更换地址空间,如果后面的指令的虚拟地址对应的物理地址不同,会打断 trap.S
的代码的执行,因此 trap.S
的代码必须在内核地址空间中和应用地址空间中的映射相同,这就是跳板页。trap.S
中的__alltraps
和 __restore
函数也做了改变,增加了修改 satp
寄存器的部分。
对TCB块做了扩展,把地址空间、trap上下文的PPN,总的字节数也存进去了。