[rCore学习笔记 027]地址空间

写在前面

本随笔是非常菜的菜鸡写的。如有问题请及时提出。

可以联系:[email protected]

GitHhub:https://github.com/WindDevil (目前啥也没有

引言

兜兜转转又是新的一章的开始,还是首先要看官方手册里的理论介绍和内容.

这里主要还是提纲挈领地摘抄里面的部分内容,在下面用更小的标题表现.

本章目的

本章展现了操作系统为实现“理想”而要扩展的一系列功能:

  • 通过动态内存分配,提高了应用程序对内存的动态使用效率
  • 通过页表的虚实内存映射机制,简化了编译器对应用的地址空间设置
  • 通过页表的虚实内存映射机制,加强了应用之间,应用与内核之间的内存隔离,增强了系统安全
  • 通过页表的虚实内存映射机制,可以实现空分复用 (提出,但没有实现)

需求

在大多数应用(也就是应用开发者)的视角中,它们会独占一整个 CPU 和特定(连续或不连续)的内存空间。当然,通过上一章的学习,我们知道在现代操作系统中,出于公平性的考虑,我们极少会让独占 CPU 这种情况发生。所以应用自认为的独占 CPU 只是内核想让应用看到的一种 幻象 (Illusion) ,而 CPU 计算资源被 时分复用 (TDM, Time-Division Multiplexing) 的实质被内核通过恰当的抽象隐藏了起来,对应用不可见。

与之相对,我们目前还没有对内存管理功能进行进一步拓展,仅仅是把程序放到某处的物理内存中。在内存访问方面,所有的应用都直接通过物理地址访问物理内存,这使得应用开发者需要了解繁琐的物理地址空间布局,访问内存也很不方便。在上一章中,出于任务切换的需要,所有的应用都在初始化阶段被加载到内存中并同时驻留下去直到它们全部运行结束。而且,所有的应用都直接通过物理地址访问物理内存。

这会带来以下问题:

  • 首先,内核提供给应用的内存访问接口不够透明,也不好用。由于应用直接访问物理内存,这需要它在构建的时候就清楚所运行计算机的物理内存空间布局,还需规划自己需要被加载到哪个地址运行。为了避免冲突可能还需要应用的开发者们对此进行协商,这显然是一件在今天看来不够通用且极端麻烦的事情。
  • 其次,内核并没有对应用的访存行为进行任何保护措施,每个应用都有计算机系统中整个物理内存的读写权力。即使应用被限制在 U 特权级下运行,它还是能够造成很多麻烦:比如它可以读写其他应用的数据来窃取信息或者破坏其它应用的正常运行;甚至它还可以修改内核的代码段来替换掉原本的 trap_handler 函数,来挟持内核执行恶意代码。总之,这造成系统既不安全、也不稳定。
  • 再次,目前应用的内存使用空间在其运行前已经限定死了,内核不能灵活地给应用程序提供的运行时动态可用内存空间。比如一个应用结束后,这个应用所占的空间就被释放了,但这块空间无法动态地给其它还在运行的应用使用。

解决方案

为了简化应用开发,防止应用胡作非为,本章将更好地管理物理内存,并提供给应用一个抽象出来的更加透明易用、也更加安全的访存接口,这就是基于分页机制的虚拟内存。站在应用程序运行的角度看,就是存在一个从“0”地址开始的非常大的可读/可写/可执行的地址空间(Address Space),而站在操作系统的角度看,每个应用被局限在分配给它的物理内存空间中运行,无法读写其它应用和操作系统所在的内存空间。

硬件支持

实现地址空间的第一步就是实现分页机制,建立好虚拟内存和物理内存的页映射关系。此过程需要硬件支持,硬件细节与具体CPU相关,涉及地址映射机制等,相对比较复杂。

需要思考的问题

总体而言,我们需要思考如下问题:

  • 硬件中物理内存的范围是什么?
  • 哪些物理内存空间需要建立页映射关系?
  • 如何建立页表使能分页机制?
  • 如何确保 OS 能够在分页机制使能前后的不同时间段中都能正常寻址和执行代码?
  • 页目录表(一级)的起始地址设置在哪里?
  • 二级/三级等页表的起始地址设置在哪里,需要多大空间?
  • 如何设置页目录表项/页表项的内容?
  • 如果要让每个任务有自己的地址空间,那每个任务是否要有自己的页表?
  • 代表应用程序的任务和操作系统需要有各自的页表吗?
  • 在有了页表之后,任务和操作系统之间应该如何传递数据?

体验环节

经典切换代码到第四章分支,然后一举运行:

cd ~/App/rCore-Tutorial-v3
git checkout ch4
cd os
make run

这里注意如果不能切换到ch4很有可能是因为没丢弃或者保存分支,尝试使用git checkout .指令.

运行结果:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
.text [0x80200000, 0x8020c000)
.rodata [0x8020c000, 0x80210000)
.data [0x80210000, 0x80266000)
.bss [0x80266000, 0x80577000)
mapping .text section
mapping .rodata section
mapping .data section
mapping .bss section
mapping physical memory
mapping memory-mapped registers
[kernel] back to world!
remap_test passed!
init TASK_MANAGER
num_app = 7
power_3 [10000/300000]
power_3 [20000/300000]
power_3 [30000/300000]
power_3 [40000/300000]
power_3 [50000/300000]
power_3 [60000/300000]
power_3 [70000/300000]
power_3 [80000/300000]
power_3 [90000/300000]
power_3 [100000/300000]
power_3 [110000/300000]
power_3 [120000/300000]
power_3 [130000/300000]
power_3 [140000/300000]
power_3 [150000/300000]
power_3 [160000/300000]
power_3 [170000/300000]
power_3 [180000/300000]
power_3 [190000/300000]
power_3 [200000/300000]
power_3 [210000/300000]
power_3 [220000/300000]
power_3 [230000/300000]
power_3 [240000/300000]
power_3 [250000/300000]
power_3 [260000/300000]
power_3 [270000/300000]
power_3 [280000/300000]
power_3 [290000/300000]
power_3 [300000/300000]
3^300000 = 612461288(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
power_5 [10000/210000]
power_5 [20000/210000]
power_5 [30000/210000]
power_5 [40000/210000]
power_5 [50000/210000]
power_5 [60000/210000]
power_5 [70000/210000]
power_5 [80000/210000]
power_5 [90000/210000]
power_5 [100000/210000]
power_5 [110000/210000]
power_5 [120000/210000]
power_5 [130000/210000]
power_5 [140000/210000]
power_5 [150000/210000]
power_7 [10000/240000]
power_7 [20000/240000]
power_7 [30000/240000]
power_7 [40000/240000]
power_7 [50000/240000]
power_7 [60000/240000]
power_7 [70000/240000]
power_7 [80000/240000]
power_7 [90000/240000]
power_7 [100000/240000]
power_7 [110000/240000]
power_7 [120000/240000]
power_7 [130000/240000]
power_7 [140000/240000]
power_7 [150000/240000]
power_7 [160000/240000]
power_7 [170000/240000]
power_7 [180000/240000]
power_7 [190000/240000]
power_7 [200000/240000]
power_7 [210000/240000]
power_7 [220000/240000]
power_7 [230000/240000]
power_7 [240000/240000]
7^240000 = 304164893(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0

load_fault APP running...

Into Test load_fault, we will insert an invalid load operation...
Kernel should kill this application!
[kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x100c0, kernel killed it.

store_fault APP running...

Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x100c0, kernel killed it.
Test sbrk start.
origin break point = 17000
one page allocated,  break point = 18000
try write to allocated page
write ok
10 page allocated,  break point = 22000
11 page DEALLOCATED,  break point = 17000
try DEALLOCATED more one page, should be failed.
Test sbrk almost OK!
now write to deallocated page, should cause page fault.
[kernel] PageFault in application, bad addr = 0x17000, bad instruction = 0x1124a, kernel killed it.
power_5 [160000/210000]
power_5 [170000/210000]
power_5 [180000/210000]
power_5 [190000/210000]
power_5 [200000/210000]
power_5 [210000/210000]
5^210000 = 527227302(MOD 998244353)
Test power_5 OK!
[kernel] Application exited with code 0
Test sleep OK!
[kernel] Application exited with code 0
All applications completed!

本章框图

这里需要和上一章的框图进行对比才能知道本章做了什么改进.

这是本章框图:

这是上章框图:

可以看到的不同分为两个部分:

  1. 硬件需求不同
  2. 系统架构不同

硬件需求

观察框图,可以看到本章的框图中要求:

  1. CPU with MMU 要求 CPU 含有MMU,也即内存管理单元(Memory Management Unit)是计算机硬件的一部分,主要负责处理内存管理和虚拟地址到物理地址的转换。
  2. MEM with PageTable,要求内存支持页表,Page Table 是操作系统用于管理虚拟内存的一种数据结构,它记录了虚拟地址与物理地址之间的映射关系。Page Table 需要硬件支持,特别是 MMU(Memory Management Unit)来实现虚拟地址到物理地址的快速转换。

总而言之就是需要MMU.

系统架构

本章框图中增加了:

  1. APP层和OS层的跳板Trampline
  2. 每个APP的地址空间
  3. 内核的地址空间
  4. 物理页帧分配

等功能.

这里摘自官方手册:

在具体实现上,扩展了 TaskManager 的管理范围,每个 Task 的上下文 Task Context 还包括该任务的地址空间,在切换任务时,也要切换任务的地址空间。新增的内存管理模块主要包括与内核中动态内存分配相关的页帧分配、堆分配,以及表示应用地址空间的 Apps MemSets 类型和内核自身地址空间的 Kernel MemSet类型。 MemSet 类型所包含的页表 PageTable 建立了虚实地址映射关系,而另外一个 MemArea 表示任务的合法空间范围。

建议

代码的阅读顺序参考官方手册.

这里代码繁多,尤其是涉及到内存的变换之后就会面临对前面所有的数据结构的大范围的重写,对trap的处理和对__switch的重构.

建议多看代码,多理清问题,这样才能顺利度过这最难的一章.