# 虚拟内存

​		虚拟内存是现代主流操作系统支持的管理方案，它需要硬件提供MMU进行分页机制，不过K210的MMU使用有些要注意的，XV6-K210等项目均提到了，我们这里采用了RustSBI作为我们的SBI，因此可以一定程度上屏蔽K210和qemu的差异。

​		 虚拟内存部分由主要由三个类/结构体进行控制，分别为虚拟内存空间（Virtual Memory Space，简称VMS），虚拟内存区域（Virtual Memory Region，简称VMR），页表（PageTable）。



#### 虚拟内存空间VirtualMemorySpace

​		VMS用于管理概念上的一个逻辑地址空间，拥有一个根页表（PDT）和若干VMR串成的链表。其主要成员和说明如下：

###### 内部保护成员

* VirtualMemorySpace* CurrentVMS：类静态成员，表示当前的VMS
* VirtualMemorySpace* BootVMS：类静态成员，表示启动时使用的VMS
* VirtualMemorySpace* KernelVMS：类静态成员，表示内核所属的VMS，目前与BootVMS相同，后期计划改成单独的页表控制
* LinkTableT \<VirtualMemoryRegion\> VmrHead：VMR区域按地址顺序链接成的链表
* Uint32 VmrCount：管理的VMR的个数
* VirtualMemoryRegion *VmrCache：上次的VMR的缓存，用于提高查找效率
* PageTable *PDT：对应的根页表指针，是虚拟内存空间真正的支撑者
* Uint32 SharedCount：共享计数，当共享计数达到0时可以自动销毁自身空间

###### 主要成员函数：

* EnableAccessUser：默认情况下，内核态是不可以访问用户态地址空间的，当用户态传递了某个指针必须由内核访问时，可以 暂时开启访问允许，不需要后立刻关闭，以最大程度避免误操作
* DisableAccessUser：与上一个类似，为关闭允许访问用户空间使用。特别注意，在qemu和K210开发板上它们的Riscv标准不同，因此需要操作的寄存器方法不同，对应位的含义两者时相反的。
* FindVMR：查找某个地址所属的VMR，如果不存在则返回nullptr
* InsertVMR：插入一个构造好的VMR到当前VMS管理的链表中，表示启用地址空间中的某一区域
* RemoveVMR：从地址空间中移除某个VMR
* GetUsableVMR：在给定的[start,end)区间内获取第一个能分配length长度的VMR的首地址
* Unref：进程解除对VMS的引用，使得引用计数-1
* Ref：进程引用VMS，使得引用计数+1
* TryDeleteSelf：检查引用计数是否为0，如是则销毁自己，返回是否销毁
* Enter：VMS的核心功能之一，即进入到当前虚拟地址空间，具体操作时是对页表进行更换，并刷新TLB缓存
* Leave：概念上与Enter相反的功能，由于进程必须呆在地址空间中，因此Leave实际上是Enter到了KernelVMS中
* SolvePageFault：VMS的核心功能之一，处理缺页中断，处理缺页中断时会检查是否有合法的VMR，然后再补全页表和页表项
* Create：创建一个指定类型的虚拟内存空间
* CreateFrom：从给定的虚拟内存空间创建一个新的VMS，即拷贝VMS
* ……



#### 虚拟内存区域VirtualMemoryRegion

​		虚拟内存区域VMR表示的是VMS中的一个合法区间，通过VMR我们可以对进程的不同段进行划分，区分内存映射文件等。其主要成员和说明如下：

###### 公开内部结构：

* 权限VM_XXX：表示这个区域上的属性的标志位，如Read、Write、Exec等，VMR通过自身的属性可以转成页表所需要的标志位

###### 内部保护成员：

* LinkTableT \<VirtualMemoryRegion\>：继承于的基类，用于将当前VMR变成一种链表结构进行管理

* VirtualMemorySpace *VMS：所属虚拟内存空间
* PtrInt Start：区域起始地址
* PtrInt End：区域终止地址
* Uint32 Flags：区域属性

###### 主要成员函数：

* CopyMemory：从给定页表拷贝一片虚拟内存区域
* ToPageEntryFlags：将自身的属性转换成页表对应的属性格式
* Intersect：判断某个区间是否和自身有交集
* In：判断某个区间、地址是否包含于自身
* ……

当前设计中，VMR以链式结构由VMS统一管理，因此一个VMR只能属于一个VMS，不便于从VMR一级进行共享，未来计划可能设计成可以由多个VMS共享的形式。此外，原先的设计中并未打算作为基类使用，但后续有了这样的需求，因此未来还需要改进。



#### 内存区域拷贝

​		为了实现Clone的功能，必须要支持对VMS进行拷贝，而VMS的拷贝实际上就是对所属的VMR一个一个进行拷贝，因此我们设计将VMR作为拷贝的单元。

​		具体的实现方案为逐层遍历目标与自身区间有交集的页表，如果是页表，则创建新的页表，如果是页表项，则创建一个页表项并拷贝目标的数据。特别注意的是，对于特定的页表，如内核和共享内存页，不需要拷贝，而是增加这些页的引用计数。



#### 堆内存区域

​		原先设计的VMR并不支持动态地调整大小，为了提供动态调整大小的功能，从VMR派生出HeapMemoryRegion（HMR），用来支持用户程序的brk调用，即调整堆段大小。当进行调整时，函数会判断这次调用是否能成功，保证不与其他VMR发生重叠。

​		HMR一般在载入用户进程时创建，根据ELF文件段表的描述，我们在最后一个段出现的位置后面用GetUsableVMR来获取一个可用区域作为堆段。



#### 文件内存区域

​		将文件的一部分映射到内存中的某个区域，也是通过继承VirtualMemoryRegion实现的，当区域创建和销毁时，会自动与文件进行同步，此外，用户也可以主动同步。具体的描述详见0x55特殊文件文档。



#### 页表PageTable

​		页表是我们根据SV39页表的规范构造的结构体，在目前的虚拟内存机制下它是由512个页表项组成的，大小为一个4k页的结构体，特别注意，创建该对象时不能用一般的Kmalloc或new，而需要用PMM中提供的分配物理页方法，以保证4KB对齐。

​		页表结构体的成员如下：

###### 公开内部结构：即下方会提到的PageEntry，详见下文

###### 内部保护成员：512个PageEntry组成的对齐到4KB的数组

###### 主要成员函数：

* VPN：获取虚拟地址对应的某级虚拟页号，该函数有两个重载版本，其中一个用C++模板实现，能尽可能将所需数据编译器计算出来，减少函数调用开销，提高效率
* Boot：获取Boot页表，即启动汇编代码里构造的boot_page_table_sv39
* KAddr：根据该页表获取内核页的地址，事实上就是this指针
* PAddr：根据该页表获取物理页的地址，即KAddr转换到物理地址后的结果
* InitAsPDT：初始化为根页表
* Init：初始化空页表
* ……



#### 页表项PageEntry

​		页表项是页表的成员，可以作为次级页表索引，也可以作为物理页索引，此外其上还标记了一些标志位，用于权限控制等。为了提高效率，读写标志位的部分均使用模板实现，最大限度地提高效率，同时保留易用性。由于设计较多C++模板的特性，这里就不多做赘述了。

​		页表项的XWR三个位用来标识当前页表项的属性，当全为0时表示次级页表，索引时需要拿到次级页表的物理页号，转换成内核虚拟页号，一级一级向下找。



#### 缺页中断的处理

​		缺页中断实现起来还是比较简单的，首先是在Trap分发程序处捕获到缺页异常，并转发到SolvePageFault函数处理。该函数首先用FindVMR判断是否存在合法的虚拟内存区域，如果不存在则返回错误。Trap分发程序会根据进程类型进行处理，对于用户进程是强制终止，对于内核是进入KernelMonitor等待用户处理。

​		对于合法的VMR，缺页处理程序从根页表开始，一级一级访问，如果某一级发生页表或页表项缺失，则对其进行补全。最后缺页处理完成后刷新TLB即可。







--------

By：qianpinyi

2022.06.05