提交 0bea9d45 编写于 作者: chyyuu1972's avatar chyyuu1972

update ch4/sec6, upate some words in ch4/5

上级 01ec3302
......@@ -282,7 +282,7 @@ usize 的一种简单包装。我们刻意将它们各自抽象出来而不是
- 第 15 行中,我们也可以通过 ``empty`` 方法生成一个全零的页表项,注意这隐含着该页表项的 V 标志位为 0 ,
因此它是不合法的。
后面我们还为 ``PageTableEntry`` 实现了一些工具函数,可以快速判断一个页表项的 V/R/W/X 标志位是否为 1,以 V
后面我们还为 ``PageTableEntry`` 实现了一些辅助函数(Helper Function),可以快速判断一个页表项的 V/R/W/X 标志位是否为 1,以 V
标志位的判断为例:
.. code-block:: rust
......
......@@ -197,8 +197,8 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
之前设计方式的优点在于: Trap 的时候无需切换地址空间,而在任务切换的时候才需要切换地址空间。由于后者比前者更容易
实现,这降低了实现的复杂度。而且在应用高频进行系统调用的时候能够避免地址空间切换的开销,这通常源于快表或 cache
的失效问题。但是这种设计方式也有缺点:即内核的逻辑段需要在每个应用的地址空间内都映射一次,这会带来一些无法忽略的
内存占用开销,并显著限制了嵌入式平台(如我们所采用的 K210 )的任务并发数。此外,这种做法无法应对处理器的 **熔断**
(Meltdown) 漏洞,使得恶意应用能够以某种方式看到它本来无权访问的地址空间中内核部分的数据。将内核与地址空间隔离
内存占用开销,并显著限制了嵌入式平台(如我们所采用的 K210 )的任务并发数。此外,这种做法无法应对处理器的 `熔断
(Meltdown) 漏洞 <https://cacm.acm.org/magazines/2020/6/245161-meltdown/fulltext>`_ ,使得恶意应用能够以某种方式看到它本来无权访问的地址空间中内核部分的数据。将内核与地址空间隔离
便是修复此漏洞的一种方法。
经过权衡,在本教程中我们参考 MIT 的教学 OS `xv6 <https://github.com/mit-pdos/xv6-riscv>`_ ,
......@@ -313,7 +313,7 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
随后,就像之前一样,我们 ``csrrw`` 交换 sp 和 ``sscratch`` ,并基于指向 Trap 上下文位置的 sp 开始保存通用
寄存器和一些 CSR ,这个过程在第 28 行结束。到这里,我们就全程在内核地址空间中完成了保存 Trap 上下文的工作。
接下来该考虑切换到内核地址空间并跳转到 trap handler 了。第 30 行我们将内核地址空间的 token 载入到 t0 寄存器中,
- 接下来该考虑切换到内核地址空间并跳转到 trap handler 了。第 30 行我们将内核地址空间的 token 载入到 t0 寄存器中,
第 32 行我们将 trap handler 入口点的虚拟地址载入到 t1 寄存器中,第 34 行我们直接将 sp 修改为应用内核栈顶的地址。
这三条信息均是内核在初始化该应用的时候就已经设置好的。第 36~37 行我们将 satp 修改为内核地址空间的 token 并使用
``sfence.vma`` 刷新快表,这就切换到了内核地址空间。最后在第 39 行我们通过 ``jr`` 指令跳转到 t1 寄存器所保存的
......@@ -344,11 +344,12 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
}
这样,这段汇编代码放在一个物理页帧中,且 ``__alltraps`` 恰好位于这个物理页帧的开头,其物理地址被外部符号
``strampoline`` 标记。在开启分页模式之后,内核和应用代码都只能看到地址空间,而在它们的视角中,这段汇编代码
被放在它们地址空间的最高页面上,由于这段汇编代码在执行的时候涉及到地址空间切换,故而被称为跳板页面。那么指令
能够被连续执行呢?注意无论是内核还是应用的地址空间,跳板页面均位于同样位置,且它们也将会映射到同一个实际存放这段
汇编代码的物理页帧。也就是说,无论在执行 ``__alltraps`` 还是 ``__restore`` 切换地址空间的时候,两个地址空间
在切换地址空间的指令附近的映射方式均是相同的,这就说明了指令仍是连续执行的。
``strampoline`` 标记。在开启分页模式之后,内核和应用代码都只能看到各自的虚拟地址空间,而在它们的视角中,这段汇编代码
被放在它们地址空间的最高虚拟页面上,由于这段汇编代码在执行的时候涉及到地址空间切换,故而被称为跳板页面。
那么在产生trap前后的一小段时间内会有一个比较 **极端** 的情况,即刚产生trap时,CPU已经进入了内核态(即Supervisor Mode),但此时执行代码和访问数据还是在应用程序所处的用户态虚拟地址空间中,而不是我们通常理解的内核虚拟地址空间。在这段特殊的时间内,CPU指令
为什么能够被连续执行呢?这里需要注意:无论是内核还是应用的地址空间,跳板的虚拟页均位于同样位置,且它们也将会映射到同一个实际存放这段
汇编代码的物理页帧。也就是说,在执行 ``__alltraps`` 或 ``__restore`` 函数进行地址空间切换的时候,应用的用户态虚拟地址空间和操作系统内核的内核态虚拟地址空间对切换地址空间的指令所在页的映射方式均是相同的,这就说明了这段切换地址空间的指令控制流仍是可以连续执行的。
现在可以说明我们在创建用户/内核地址空间中用到的 ``map_trampoline`` 是如何实现的了:
......@@ -376,10 +377,11 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
跳板汇编代码所在的物理页帧的键值对,访问方式限制与代码段相同,即 RX 。
最后可以解释为何我们在 ``__alltraps`` 中需要借助寄存器 ``jr`` 而不能直接 ``call trap_handler`` 了。因为在
内存布局中,这条 ``.text.trampoline`` 段中的跳转指令和 ``trap_handler`` 都在代码段之内,汇编器计算二者地址偏移量
内存布局中,这条 ``.text.trampoline`` 段中的跳转指令和 ``trap_handler`` 都在代码段之内,汇编器(Assembler)和链接器(Linker)会根据 ``linker-qemu/k210.ld`` 的地址布局描述,设定电子指令的地址,并计算二者地址偏移量
并让跳转指令的实际效果为当前 pc 自增这个偏移量。但实际上我们知道由于我们设计的缘故,这条跳转指令在被执行的时候,
它的虚拟地址是在地址空间中的最高页面之内,加上这个偏移量并不能正确的得到 ``trap_handler`` 的入口地址。问题的本质可以
概括为:跳转指令实际被执行时的虚拟地址和在编译器进行链接时看到的它的地址不同。
它的虚拟地址被操作系统内核设置在地址空间中的最高页面之内,加上这个偏移量并不能正确的得到 ``trap_handler`` 的入口地址。
** 问题的本质可以概括为:跳转指令实际被执行时的虚拟地址和在编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码是设置此指令的地址是不同。**
加载和执行应用程序
------------------------------------
......@@ -387,7 +389,7 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
扩展任务控制块
^^^^^^^^^^^^^^^^^^^^^^^^^^^
任务控制块相比第三章包含了更多内容:
为了让应用在运行时有一个安全隔离且符合编译器给应用设定的地址空间布局的虚拟地址空间,操作系统需要对任务进行更多的管理,所以任务控制块相比第三章也包含了更多内容:
.. code-block:: rust
:linenos:
......@@ -478,8 +480,7 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
.. _trap-return-intro:
- 第 30~32 行,我们在应用的内核栈顶压入一个跳转到 ``trap_return`` 而不是 ``__restore`` 的任务上下文使得可以第一次
执行该应用。在构造方式上,只是将 ra 寄存器的值设置为 ``trap_return`` 的地址。 ``trap_return`` 是我们后面要介绍的
- 第 30~32 行,我们在应用的内核栈顶压入一个跳转到 ``trap_return`` 而不是 ``__restore`` 的任务上下文,这主要是为了能够支持对该应用的启动并顺利切换到用户地址空间执行。在构造方式上,只是将 ra 寄存器的值设置为 ``trap_return`` 的地址。 ``trap_return`` 是我们后面要介绍的
新版的 Trap 处理的一部分。
这里我们对裸指针解引用成立的原因在于:我们之前已经进入了内核地址空间,而我们要操作的内核栈也是在内核地址空间中的;
......@@ -499,9 +500,9 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
}
此处需要说明的是,返回 ``'static`` 的可变引用和之前一样可以看成一个绕过 unsafe 的裸指针;而 ``PhysPageNum::get_mut``
是一个泛型函数,由于我们已经声明了总体返回 ``TrapContext`` 的可变引用,则编译器会给 ``get_mut`` 针对 ``T=TrapContext``
的情况生成一个版本的实现,在 ``get_trap_cx`` 中则会静态调用该实现。
- 第 42 行我们正式通过 Trap 上下文的可变引用来进行初始化:
是一个泛型函数,由于我们已经声明了总体返回 ``TrapContext`` 的可变引用,则Rust编译器会给 ``get_mut`` 泛型函数针对具体类型 ``TrapContext``
的情况生成一个特定版本的 ``get_mut`` 函数实现。在 ``get_trap_cx`` 函数中则会静态调用``get_mut`` 泛型函数的特定版本实现。
- 第 42 行我们正式通过 Trap 上下文的可变引用来对其进行初始化:
.. code-block:: rust
:linenos:
......@@ -572,10 +573,10 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
可以看到,在 ``TaskManagerInner`` 中我们使用向量 ``Vec`` 来保存任务控制块。在全局任务管理器 ``TASK_MANAGER``
初始化的时候,只需使用 ``loader`` 子模块提供的 ``get_num_app`` 和 ``get_app_data`` 分别获取链接到内核的应用
数量和每个应用的 ELF 格式数据,然后依次给每个应用创建任务控制块并加入到向量中即可。我们还将 ``current_task`` 设置
数量和每个应用的 ELF 文件格式的数据,然后依次给每个应用创建任务控制块并加入到向量中即可。我们还将 ``current_task`` 设置
为 0 ,于是将从第 0 个应用开始执行。
回过头来介绍一下应用链接器 ``os/build.rs`` 的改动:
回过头来介绍一下应用构建器 ``os/build.rs`` 的改动:
- 首先,我们在 ``.incbin`` 中不再插入清除全部符号的应用二进制镜像 ``*.bin`` ,而是将构建得到的 ELF 格式文件直接链接进来;
- 其次,在链接每个 ELF 格式文件之前我们都加入一行 ``.align 3`` 来确保它们对齐到 8 字节,这是由于如果不这样做, ``xmas-elf`` crate 可能会在解析 ELF 的时候进行不对齐的内存读写,例如使用 ``ld`` 指令从内存的一个没有对齐到 8 字节的地址加载一个 64 位的值到一个通用寄存器。而在 k210 平台上,由于其硬件限制,这会触发一个内存读写不对齐的异常,导致解析无法正常完成。
......@@ -612,10 +613,10 @@ MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页
通过 ``current_user_token`` 和 ``current_trap_cx`` 分别可以获得当前正在执行的应用的地址空间的 token 和可以在
内核地址空间中修改位于该应用地址空间中的 Trap 上下文的可变引用。
Trap 处理
改进 Trap 处理的实现
------------------------------------
让我们来看现在 ``trap_handler`` 的实现:
为了能够支持地址空间,让我们来看现在 ``trap_handler`` 的改进实现:
.. code-block:: rust
:linenos:
......@@ -720,13 +721,13 @@ Trap 处理
在 ``__switch`` 切换到它的时候,这将会跳转到 ``trap_return`` 并第一次返回用户态。
sys_write 的改动
改进 sys_write 的实现
------------------------------------
同样由于内核和应用地址空间的隔离, ``sys_write`` 不再能够直接访问位于应用空间中的数据,而需要手动查页表才能知道那些
数据被放置在哪些物理页帧上并进行访问。
为此,页表模块 ``page_table`` 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的工具函数:
为此,页表模块 ``page_table`` 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的辅助函数:
.. code-block:: rust
:linenos:
......@@ -787,7 +788,9 @@ sys_write 的改动
侏罗纪“头甲龙”操作系统
小结
-------------------------------------
添加侏罗纪“头甲龙”操作系统的相关内容。
这一章内容很多,讲解了 **地址空间** 这一抽象概念是如何在一个具体的“头甲龙”操作系统中实现的。这里面的核心内容是如何建立基于页表机制的虚拟地址空间。为此,操作系统需要知道并管理整个系统中的物理内存;需要建立虚拟地址到物理地址映射关系的页表;并基于页表给操作系统自身和每个应用提供一个虚拟地址空间;并需要对管理应用的任务控制块进行扩展,确保能对应用的地址空间进行管理;由于应用和内核的地址空间是隔离的,需要有一个跳板来帮助完成应用与内核之间的切换执行;并导致了对异常、中断、系统调用的相应更改。这一系列的改进,最终的效果是编写应用更加简单了,且应用的执行或错误不会影响到内核和其他应用的正常工作。为了得到这些好处,我们需要比较费劲地进化我们的操作系统。如果同学结合阅读代码,编译并运行应用+内核,读懂了上面的文档,那完成本章的实验就有了一个坚实的基础。
如果同学能想明白如何插入/删除页表;如何在 ``trap_handler`` 下处理 ``LoadPageFault`` ;以及 ``sys_get_time`` 在使能页机制下如何实现,那就会发现下一节的实验练习 **就和lab1一样** 。
......@@ -138,7 +138,7 @@
│   └── rustsbi-qemu.bin
├── LICENSE
├── os
│   ├── build.rs(修改:基于应用名的应用链接器)
│   ├── build.rs(修改:基于应用名的应用构建器)
│   ├── Cargo.toml
│   ├── Makefile
│   └── src
......
......@@ -636,7 +636,7 @@ exec 系统调用的实现
// ---- release current PCB lock automatically
}
``sys_waitpid`` 是一个立即返回的系统调用,它的返回值语义是:如果当前的进程不存在一个符合要求的子进程,则返回 -1;如果至少存在一个,但是其中没有僵尸进程(也即仍未退出)则返回 -2;如果都不是的话则可以正常回收并返回回收子进程的 pid 。但在编写应用的开发者看来, ``wait/waitpid`` 两个工具函数都必定能够返回一个有意义的结果,要么是 -1,要么是一个正数 PID ,是不存在 -2 这种通过等待即可消除的中间结果的。这等待的过程正是在用户库 ``user_lib`` 中完成。
``sys_waitpid`` 是一个立即返回的系统调用,它的返回值语义是:如果当前的进程不存在一个符合要求的子进程,则返回 -1;如果至少存在一个,但是其中没有僵尸进程(也即仍未退出)则返回 -2;如果都不是的话则可以正常回收并返回回收子进程的 pid 。但在编写应用的开发者看来, ``wait/waitpid`` 两个辅助函数都必定能够返回一个有意义的结果,要么是 -1,要么是一个正数 PID ,是不存在 -2 这种通过等待即可消除的中间结果的。这等待的过程正是在用户库 ``user_lib`` 中完成。
第 11~17 行判断 ``sys_waitpid`` 是否会返回 -1 ,这取决于当前进程是否有一个符合要求的子进程。当传入的 ``pid`` 为 -1 的时候,任何一个子进程都算是符合要求;但 ``pid`` 不为 -1 的时候,则只有 PID 恰好与 ``pid`` 相同的子进程才算符合条件。我们简单通过迭代器即可完成判断。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册