引言 ============================== 本章导读 ------------------------------- 本章中内核将实现虚拟内存机制,这注定是一趟艰难的旅程。 实践体验 ----------------------- 本章应用运行起来效果与上一章基本一致。 获取本章代码: .. code-block:: console $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023A.git $ cd rCore-Tutorial-Code-2023A $ git checkout ch4 $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023A.git user 或许你之前已经克隆过了仓库,只希望从远程仓库更新,而非再克隆一次: .. code-block:: console $ cd rCore-Tutorial-Code-2023A # 你可以将 upstream 改为你喜欢的名字 $ git remote add upstream https://github.com/LearningOS/rCore-Tutorial-Code-2023A.git # 更新仓库信息 $ git fetch upstream # 查看已添加的远程仓库;应该能看到已有一个 origin 和新添加的 upstream 仓库 $ git remote -v # 根据需求选择以下一种操作即可 # 在本地新建一个与远程仓库对应的分支: $ git checkout -b ch4 upstream/ch4 # 本地已有分支,从远程仓库更新: $ git checkout ch4 $ git merge upstream/ch4 # 将更新推送到自己的远程仓库 $ git push origin ch4 在 qemu 模拟器上运行本章代码: .. code-block:: console $ cd os $ make run 本章代码树 ----------------------------------------------------- .. code-block:: :linenos: ├── os │   ├── ... │   └── src │   ├── ... │   ├── config.rs(修改:新增一些内存管理的相关配置) │   ├── linker.ld(修改:将跳板页引入内存布局) │   ├── loader.rs(修改:仅保留获取应用数量和数据的功能) │   ├── main.rs(修改) │   ├── mm(新增:内存管理的 mm 子模块) │   │   ├── address.rs(物理/虚拟 地址/页号的 Rust 抽象) │   │   ├── frame_allocator.rs(物理页帧分配器) │   │   ├── heap_allocator.rs(内核动态内存分配器) │   │   ├── memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等) │   │   ├── mod.rs(定义了 mm 模块初始化方法 init) │   │   └── page_table.rs(多级页表抽象 PageTable 以及其他内容) │   ├── syscall │   │   ├── fs.rs(修改:基于地址空间的 sys_write 实现) │   │   ├── mod.rs │   │   └── process.rs │   ├── task │   │   ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文) │   │   ├── mod.rs(修改,详见文档) │   │   ├── switch.rs │   │   ├── switch.S │   │   └── task.rs(修改,详见文档) │   └── trap │   ├── context.rs(修改:在 Trap 上下文中加入了更多内容) │   ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档) │   └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码) └── user ├── build.py(编译时不再使用) ├── ... └── src ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置) └── ... cloc os ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Rust 26 138 56 1526 Assembly 3 3 26 86 make 1 11 4 36 TOML 1 2 1 13 ------------------------------------------------------------------------------- SUM: 31 154 87 1661 ------------------------------------------------------------------------------- .. 本章代码导读 .. ----------------------------------------------------- .. 本章涉及的代码量相对多了起来,也许同学们不知如何从哪里看起或从哪里开始尝试实验。这里简要介绍一下“头甲龙”操作系统的大致开发过程。 .. 我们先从简单的地方入手,那当然就是先改进应用程序了。具体而言,主要就是把 ``linker.ld`` 中应用程序的起始地址都改为 ``0x0`` ,这是假定我们操作系统能够通过分页机制把不同应用的相同虚地址映射到不同的物理地址中。这样我们写应用就不用考虑物理地址布局的问题,能够以一种更加统一的方式编写应用程序,可以忽略掉一些不必要的细节。 .. 为了能够在内核中动态分配内存,我们的第二步需要在内核增加连续内存分配的功能,具体实现主要集中在 ``os/src/mm/heap_allocator.rs`` 中。完成这一步后,我们就可以在内核中用到Rust的堆数据结构了,如 ``Vec`` 、 ``Box`` 等,这样内核编程就更加灵活了。 .. 操作系统如果要建立页表,首先要能管理整个系统的物理内存,这就需要知道物理内存哪些区域放置内核的代码、数据,哪些区域则是空闲的等信息。所以需要了解整个系统的物理内存空间的范围,并以物理页帧为单位分配和回收物理内存,具体实现主要集中在 ``os/src/mm/frame_allocator.rs`` 中。 .. 页表中的页表项的索引其实是虚拟地址中的虚拟页号,页表项的重要内容是物理地址的物理页帧号。为了能够灵活地在虚拟地址、物理地址、虚拟页号、物理页号之间进行各种转换,在 ``os/src/mm/address.rs`` 中实现了各种转换函数。 .. 完成上述工作后,基本上就做好了建立页表的前期准备。我们就可以开始建立页表,这主要涉及到页表项的数据结构表示,以及多级页表的起始物理页帧位置和整个所占用的物理页帧的记录。具体实现主要集中在 ``os/src/mm/page_table.rs`` 中。 .. 一旦使能分页机制,那么内核中也将基于虚地址进行虚存访问,所以在给应用添加虚拟地址空间前,内核自己也会建立一个页表,把整个物理地址空间通过简单的恒等映射对应到一个虚拟地址空间中。后续的应用在执行前,也需要建立一个虚拟地址空间,这意味着第三章的 ``task`` 将进化到第五章的拥有独立页表的进程 。虚拟地址空间需要有一个数据结构管理起来,这就是 ``MemorySet`` ,即地址空间这个抽象概念所对应的具象体现。在一个虚拟地址空间中,有代码段,数据段等不同属性且不一定连续的子空间,它们通过一个重要的数据结构 ``MapArea`` 来表示和管理。围绕 ``MemorySet`` 等一系列的数据结构和相关操作的实现,主要集中在 ``os/src/mm/memory_set.rs`` 中。比如内核的页表和虚拟空间的建立在如下代码中: .. .. code-block:: rust .. :linenos: .. // os/src/mm/memory_set.rs .. lazy_static! { .. pub static ref KERNEL_SPACE: Arc> = Arc::new(Mutex::new( .. MemorySet::new_kernel() .. )); .. } .. 完成到这里,我们就可以使能分页机制了。且我们应该有更加方便的机制来给支持应用运行。在本章之前,都是把应用程序的所有元数据丢弃从而转换成二进制格式来执行,这其实把编译器生成的 ELF 执行文件中大量有用的信息给去掉了,比如代码段、数据段的各种属性,程序的入口地址等。既然有了给应用运行提供虚拟地址空间的能力,我们就可以利用 ELF 执行文件中的各种信息来灵活构建应用运行所需要的虚拟地址空间。在 ``os/src/loader.rs`` 中可以看到如何获取一个应用的 ELF 执行文件数据,而在 ``os/src/mm/memory_set`` 中的 ``MemorySet::from_elf`` 可以看到如何通过解析 ELF 来创建一个应用地址空间。 .. 对于有了虚拟地址空间的 *任务* ,我们可以把它叫做 *进程* 了。操作系统为此需要扩展任务控制块 ``TaskControlBlock`` 的管理范围,使得操作系统能管理拥有独立页表和虚拟地址空间的应用程序的运行。相关主要的改动集中在 ``os/src/task/task.rs`` 中。 .. 由于代表应用程序运行的进程和管理应用的操作系统各自有独立的页表和虚拟地址空间,所以这就出现了两个比较挑战的事情。一个是由于系统调用、中断或异常导致的应用程序和操作系统之间的 Trap 上下文切换不像以前那么简单了,因为需要切换页表,这需要看看 ``os/src/trap/trap.S`` ;还有就是需要对来自用户态和内核态的 Trap 分别进行处理,这需要看看 ``os/src/trap/mod.rs`` 和 :ref:`跳板的实现 ` 中的讲解。 .. 另外一个挑战是,在内核地址空间中执行的内核代码常常需要读写应用地址空间的数据,这无法简单的通过一次访存交给 MMU 来解决,而是需要手动查应用地址空间的页表。在访问应用地址空间中的一块跨多个页数据的时候还需要注意处理边界条件。可以参考 ``os/src/syscall/fs.rs``、 ``os/src/mm/page_table.rs`` 中的 ``translated_byte_buffer`` 函数的实现。 .. 实现到这,应该就可以给应用程序运行提供一个方便且安全的虚拟地址空间了。