Skip to content

Commit

Permalink
udpate ch2:0/4
Browse files Browse the repository at this point in the history
  • Loading branch information
chyyuu committed Nov 6, 2021
1 parent 59ee800 commit 3c52911
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 24 deletions.
5 changes: 4 additions & 1 deletion source/chapter2/0intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
..
chyyuu:有一个ascii图,画出我们做的OS。
本章展现了操作系统一系列功能
本章从一个更底层的角度来思考如何设计应用程序,并进一步展现了操作系统相关的一系列新功能

- 构造包含操作系统内核和多个应用程序的单一执行程序
- 通过批处理支持多个程序的自动加载和运行
- 操作系统利用硬件特权级机制,实现对操作系统自身的保护
- 实现特权级的穿越
- 支持跨特权级的系统调用功能

上一章,我们在 RISC-V 64 裸机平台上成功运行起来了 ``Hello, world!`` 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。

Expand Down
60 changes: 37 additions & 23 deletions source/chapter2/4trap-handling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,21 @@
本节导读
-------------------------------

由于处理器具有硬件级的特权级机制,应用程序在用户态特权级运行时,是无法直接通过函数调用访问处于内核态特权级的批处理操作系统内核中的函数。但应用程序又需要得到操作系统提供的服务,所以应用程序需要通过某种机制进行特权级之间的切换,使得用户态应用程序可以得到内核态操作系统函数的服务。本节将讲解在RISC-V 64处理器提供的U/S特权级下,批处理操作系统和应用程序如何相互配合,完成特权级切换的。
由于处理器具有硬件级的特权级机制,应用程序在用户态特权级运行时,是无法直接通过函数调用访问处于内核态特权级的批处理操作系统内核中的函数。但应用程序又需要得到操作系统提供的服务,所以应用程序与操作系统需要通过某种合作机制完成特权级之间的切换,使得用户态应用程序可以得到内核态操作系统函数的服务。本节将讲解在RISC-V 64处理器提供的U/S特权级下,批处理操作系统和应用程序如何相互配合,完成特权级切换的。

RISC-V特权级切换
---------------------------------------

特权级切换的起因
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
我们知道,批处理操作系统被设计为运行在 S 模式,这是由RustSBI提供的 SEE(Supervisor Execution Environment) 所保证的;而应用程序被设计为运行在 U 模式,这个则是由“邓式鱼” 批处理操作系统提供的 AEE(Application Execution Environment)
所保证的。批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序之前进行一些初始化工作,并监控应用程序的执行,具体体现在:
我们知道,批处理操作系统被设计为运行在内核态特权级( RISC-V的S 模式),这是由RustSBI提供的 SEE(Supervisor Execution Environment) 所保证的。而应用程序被设计为运行在用户态特权级( RISC-V的U 模式),被操作系统为核心的执行环境监管起来。在本章中,这个应用程序的执行环境即是由“邓式鱼” 批处理操作系统提供的 AEE(Application Execution Environment) 。批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序之前进行一些初始化工作,并监控应用程序的执行,具体体现在:

- 当启动应用程序的时候,需要初始化应用程序的用户态上下文,并能切换到用户态执行应用程序;
- 当应用程序发起系统调用(即发出Trap )之后,需要到批处理操作系统中进行处理;
- 当应用程序执行出错的时候,需要到批处理操作系统中杀死该应用并加载运行下一个应用;
- 当应用程序执行结束的时候,需要到批处理操作系统中加载运行下一个应用(实际上也是通过系统调用 ``sys_exit`` 来实现的)。

这些处理都涉及到特权级切换,因此都需要硬件和操作系统协同提供的特权级切换机制
这些处理都涉及到特权级切换,因此需要应用程序、操作系统和硬件一起协同,完成特权级切换机制


特权级切换相关的控制状态寄存器
Expand All @@ -34,9 +33,9 @@ RISC-V特权级切换
当从一般意义上讨论 RISC-V 架构的 Trap 机制时,通常需要注意两点:

- 在触发 Trap 之前 CPU 运行在哪个特权级;
- 以及 CPU 需要切换到哪个特权级来处理该 Trap 并在处理完成之后返回原特权级。
- CPU 需要切换到哪个特权级来处理该 Trap 并在处理完成之后返回原特权级。

但本章中我们仅考虑当 CPU 在 U 特权级运行用户程序的时候触发 Trap,并切换到 S 特权级的批处理操作系统的对应服务代码来进行处理
但本章中我们仅考虑如下流程:当 CPU 在用户态特权级( RISC-V的U 模式)运行应用程序,执行到 Trap,切换到内核态特权级( RISC-V的S 模式),批处理操作系统的对应代码响应 Trap,并执行系统调用服务,处理完毕后,从内核态返回到用户态应用程序继续执行后续指令


.. _term-s-mod-csr:
Expand Down Expand Up @@ -75,7 +74,7 @@ RISC-V特权级切换
特权级切换
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

大多数的 Trap (陷入) 发生的场景都是在执行某条特殊指令(如 ``ecall`` )之后,CPU 发现触发了一个 Trap并需要进行特殊处理,并涉及到 :ref:`执行环境切换 <term-ee-switch>` 。具体而言,用户态执行环境中的应用程序通过 ``ecall`` 指令
大多数的 Trap (陷入) 发生的场景都是在执行某条特殊指令(如 ``ecall`` )之后,CPU 发现触发了一个 Trap并需要进行特殊处理,这涉及到 :ref:`执行环境切换 <term-ee-switch>` 。具体而言,用户态执行环境中的应用程序通过 ``ecall`` 指令
向内核态执行环境中的操作系统请求某项服务功能,那么处理器和操作系统会完成到内核态执行环境的切换,并在操作系统完成服务后,再次切换到用户态执行环境,然后应用程序会紧接着``ecall`` 指令的后一条指令位置处继续执行,参考 :ref:`图示 <environment-call-flow>` 。

.. chy ???: 这条触发 Trap 的指令和进入 Trap 之前执行的最后一条指令不一定是同一条。
Expand Down Expand Up @@ -112,7 +111,7 @@ RISC-V特权级切换
由调用规范它知道 Trap 之后 ``a0~a1`` 两个寄存器会被用来保存返回值,所以会发生变化。这个信息是应用程序明确知晓的,但某种程度上
确实也体现了执行流的变化。
应用程序被切换回来之后需要从暂停的位置恢复并继续执行,这需要在切换前后维持应用程序的上下文保持不变。应用程序的上下文包括通用寄存器和栈两个主要部分。
应用程序被切换回来之后需要从发出系统调用请求的执行位置恢复应用程序上下文并继续执行,这需要在切换前后维持应用程序的上下文保持不变。应用程序的上下文包括通用寄存器和栈两个主要部分。
由于CPU 在不同特权级下共享一套通用寄存器,所以在运行操作系统的 Trap
处理过程中,操作系统也会用到这些寄存器,这会改变应用程序的上下文。因此,与函数调用需要保存函数调用上下文/活动记录一样,在执行操作系统的 Trap 处理过程(会修改通用寄存器)之前,我们需要在某个地方(某内存块或内核的栈)保存这些寄存器并在Trap 处理结束后恢复这些寄存器。

Expand All @@ -128,7 +127,7 @@ CSR,比如 CPU 所在的特权级。我们要保证它们的变化在我们的
特权级切换的硬件控制机制
-------------------------------------

当 CPU 执行完一条指令并准备从用户特权级 陷入( ``Trap`` )到 S 特权级的时候,硬件会自动完成如下这些事情:
当 CPU 执行完一条指令(如 ``ecall`` )并准备从用户特权级 陷入( ``Trap`` )到 S 特权级的时候,硬件会自动完成如下这些事情:

- ``sstatus`` 的 ``SPP`` 字段会被修改为 CPU 当前的特权级(U/S)。
- ``sepc`` 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。
Expand Down Expand Up @@ -160,12 +159,12 @@ CSR,比如 CPU 所在的特权级。我们要保证它们的变化在我们的
--------------------------------

在 Trap 触发的一瞬间, CPU 就会切换到 S 特权级并跳转到 ``stvec`` 所指示的位置。但是在正式进入 S 特权级的 Trap 处理之前,上面
提到过我们必须保存原控制流的寄存器状态,这一般通过栈来完成。但我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。
提到过我们必须保存原控制流的寄存器状态,这一般通过内核栈来保存。注意,我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。

..
chy:我们在一个作为用户栈的特别留出的内存区域上保存应用程序的栈信息,而 Trap 执行流则使用另一个内核栈。
使用两个不同的栈是为了安全性:如果两个控制流(即应用程序的控制流和内核的控制流)使用同一个栈,在返回之后应用程序就能读到 Trap 控制流的
使用两个不同的栈主要是为了安全性:如果两个控制流(即应用程序的控制流和内核的控制流)使用同一个栈,在返回之后应用程序就能读到 Trap 控制流的
历史信息,比如内核一些函数的地址,这样会带来安全隐患。于是,我们要做的是,在批处理操作系统中添加一段汇编代码,实现从用户栈切换到内核栈,
并在内核栈上保存应用程序控制流的寄存器状态。

Expand Down Expand Up @@ -359,6 +358,8 @@ Trap 处理的总体流程如下:首先通过 ``__alltraps`` 将 Trap 上下
.. code-block:: riscv
:linenos:
# os/src/trap/trap.S
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
Expand Down Expand Up @@ -443,8 +444,8 @@ Trap 在使用 Rust 实现的 ``trap_handler`` 函数中完成分发和处理:
cx
}
- 第 4 行声明返回值为 ``&mut TrapContext`` 并在第 25 行实际将传入的 ``cx`` 原样返回,因此在 ``__restore`` 的时候 ``a0`` 寄存器在调用
``trap_handler`` 前后并没有发生变化,仍然指向分配 Trap 上下文之后的内核栈栈顶,和此时 ``sp`` 的值相同,我们 :math:`\text{sp}\leftarrow\text{a}_0`
- 第 4 行声明返回值为 ``&mut TrapContext`` 并在第 25 行实际将传入的Trap 上下文 ``cx`` 原样返回,因此在 ``__restore`` 的时候 ``a0`` 寄存器在调用
``trap_handler`` 前后并没有发生变化,仍然指向分配 Trap 上下文之后的内核栈栈顶,和此时 ``sp`` 的值相同,这里的 :math:`\text{sp}\leftarrow\text{a}_0`
并不会有问题;
- 第 7 行根据 ``scause`` 寄存器所保存的 Trap 的原因进行分发处理。这里我们无需手动操作这些 CSR ,而是使用 Rust 的 riscv 库来更加方便的
做这些事情。要引入 riscv 库,我们需要:
Expand All @@ -468,6 +469,11 @@ Trap 在使用 Rust 实现的 ``trap_handler`` 函数中完成分发和处理:
应用程序。
- 第 21 行开始,当遇到目前还不支持的 Trap 类型的时候,“邓式鱼” 批处理操作系统整个 panic 报错退出。



实现系统调用功能
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

对于系统调用而言, ``syscall`` 函数并不会实际处理系统调用,而只是根据 syscall ID 分发到具体的处理函数:

.. code-block:: rust
Expand Down Expand Up @@ -523,13 +529,17 @@ Trap 在使用 Rust 实现的 ``trap_handler`` 函数中完成分发和处理:
-------------------------------------

当批处理操作系统初始化完成,或者是某个应用程序运行结束或出错的时候,我们要调用 ``run_next_app`` 函数切换到下一个应用程序。此时 CPU 运行在
S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,唯一一种能够使得 CPU 特权级下降的方法就是通过 Trap 返回系列指令,比如
``sret`` 。事实上,在运行应用程序之前要完成如下这些工作:
S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,唯一一种能够使得 CPU 特权级下降的方法就是执行 Trap 返回的特权指令,如
``sret`` 、
``mret`` 等。事实上,在从操作系统内核返回到运行应用程序之前,要完成如下这些工作:


- 构造应用程序开始执行所需的 Trap 上下文;
- 通过 ``__restore`` 函数,从刚构造的 Trap 上下文中,恢复应用程序执行的部分寄存器;
- 设置 ``sepc`` CSR的内容为应用程序入口点 ``0x80400000``;
- 切换 ``scratch`` 和 ``sp`` 寄存器,设置 ``sp`` 指向应用程序用户栈;
- 执行 ``sret`` 从 S 特权级切换到 U 特权级。

- 跳转到应用程序入口点 ``0x80400000``;
- 将使用的栈切换到用户栈;
- 在 ``__alltraps`` 时我们要求 ``sscratch`` 指向内核栈,这个也需要在此时完成;
- 从 S 特权级切换到 U 特权级。

它们可以通过复用 ``__restore`` 的代码来更容易的实现上述工作。我们只需要在内核栈上压入一个为启动应用程序而特殊构造的 Trap 上下文,再通过 ``__restore`` 函数,就能
让这些寄存器到达启动应用程序所需要的上下文状态。
Expand Down Expand Up @@ -561,16 +571,20 @@ S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,

.. code-block:: rust
:linenos:
:emphasize-lines: 10,11,12,13,14
:emphasize-lines: 14,15,16,17,18
// os/src/batch.rs
pub fn run_next_app() -> ! {
let current_app = APP_MANAGER.inner.borrow().get_current_app();
let mut app_manager = APP_MANAGER.exclusive_access();
let current_app = app_manager.get_current_app();
unsafe {
APP_MANAGER.inner.borrow().load_app(current_app);
app_manager.load_app(current_app);
}
APP_MANAGER.inner.borrow_mut().move_to_next_app();
app_manager.move_to_next_app();
drop(app_manager);
// before this we have to drop local variables related to resources manually
// and release the resources
extern "C" { fn __restore(cx_addr: usize); }
unsafe {
__restore(KERNEL_STACK.push_context(
Expand Down

0 comments on commit 3c52911

Please sign in to comment.