Initial commit

This commit is contained in:
github-classroom[bot]
2022-06-29 12:24:12 +00:00
commit 4dfe17a1b1
867 changed files with 57359 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
引言
================================
本章导读
---------------------------------
**批处理系统** (Batch System) 出现于计算资源匮乏的年代,其核心思想是:
将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会 *自动* 执行下一个程序。
应用程序难免会出错,如果一个程序的错误导致整个操作系统都无法运行,那就太糟糕了。
*保护* 操作系统不受出错程序破坏的机制被称为 **特权级** (Privilege) 机制,
它实现了用户态和内核态的隔离。
本章在上一章的基础上,让我们的 OS 内核能以批处理的形式一次运行多个应用程序,同时利用特权级机制,
令 OS 不因出错的用户态程序而崩溃。
本章首先为批处理操作系统设计用户程序,再阐述如何将这些应用程序链接到内核中,最后介绍如何利用特权级机制处理 Trap.
实践体验
---------------------------
.. note::
基于github classroom的开发方式
基于github classroom可方便建立开发用的git repository并可基于github的 codespace 在线版ubuntu +vscode在线开发使用。整个开发环境仅仅需要一个网络浏览器。
1. 在网络浏览器中用自己的 github id 登录 github.com
2. 接收 `第二个实验练习的github classroom在线邀请 <https://classroom.github.com/a/UEOvz4qO>`_ 根据提示一路选择OK即可。
3. 完成第二步后,你的第二个实验练习的 github repository 会被自动建立好点击此github repository的链接就可看到你要完成的第一个实验了。
4. 在你的第二个实验练习的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮就可以进入到在线的ubuntu +vscode环境中
5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境rustcqemu等工具。
6. 在vscode的 `console` 中执行 `make setupclassroom_test2` 该命令仅执行一次配置githubclassroom 自动评分功能。
7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。
上述的345步不是必须的你也可以线下本地开发。
本章我们引入了用户程序。
.. code-block:: console
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
$ cd rust-based-os-comp2022
$ make setupclassroom_test2 //注意这一步很重要是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次以后一般就不用执行了除非 .github/workflows/classroom.yml发生了变化。
在 qemu 模拟器上运行本章代码:
.. code-block:: console
$ cd os2
$ make run LOG=INFO
批处理系统自动加载并运行了所有的用户程序,尽管某些程序出错了:
.. code-block::
[rustsbi] RustSBI version 0.2.0-alpha.4
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
[rustsbi-dtb] Hart count: cluster0 with 1 cores
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: ssoft, stimer, sext (0x222)
[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
[rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
[rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
[rustsbi] enter supervisor 0x80200000
[kernel] Hello, world!
[ INFO] [kernel] num_app = 6
[ INFO] [kernel] app_0 [0x8020b040, 0x8020f868)
[ INFO] [kernel] app_1 [0x8020f868, 0x80214090)
[ INFO] [kernel] app_2 [0x80214090, 0x80218988)
[ INFO] [kernel] app_3 [0x80218988, 0x8021d160)
[ INFO] [kernel] app_4 [0x8021d160, 0x80221a68)
[ INFO] [kernel] app_5 [0x80221a68, 0x80226538)
[ INFO] [kernel] Loading app_0
[ERROR] [kernel] PageFault in application, core dumped.
[ INFO] [kernel] Loading app_1
[ERROR] [kernel] IllegalInstruction in application, core dumped.
[ INFO] [kernel] Loading app_2
[ERROR] [kernel] IllegalInstruction in application, core dumped.
[ INFO] [kernel] Loading app_3
[ INFO] [kernel] Application exited with code 1234
[ INFO] [kernel] Loading app_4
Hello, world from user mode program!
[ INFO] [kernel] Application exited with code 0
[ INFO] [kernel] Loading app_5
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[ INFO] [kernel] Application exited with code 0
Panicked at src/batch.rs:68 All applications completed!
本章代码树
-------------------------------------------------
.. code-block::
── os2
│   ├── Cargo.toml
│   ├── Makefile (修改:构建内核之前先构建应用)
│   ├── build.rs (新增:生成 link_app.S 将应用作为一个数据段链接到内核)
│   └── src
│   ├── batch.rs(新增:实现了一个简单的批处理系统)
│   ├── console.rs
│   ├── entry.asm
│   ├── lang_items.rs
│   ├── link_app.S(构建产物,由 os/build.rs 输出)
│   ├── linker.ld
│   ├── logging.rs
│   ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用)
│   ├── sbi.rs
│   ├── sync(新增包装了RefCell暂时不用关心)
│   │   ├── mod.rs
│   │   └── up.rs
│   ├── syscall(新增:系统调用子模块 syscall)
│   │   ├── fs.rs(包含文件 I/O 相关的 syscall)
│   │   ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理)
│   │   └── process.rs(包含任务处理相关的 syscall)
│   └── trap(新增Trap 相关子模块 trap)
│   ├── context.rs(包含 Trap 上下文 TrapContext)
│   ├── mod.rs(包含 Trap 处理入口 trap_handler)
│   └── trap.S(包含 Trap 上下文保存与恢复的汇编代码)
└── user(新增:应用测例保存在 user 目录下)
├── Cargo.toml
├── Makefile
└── src
├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中)
│   ├── ...
├── console.rs
├── lang_items.rs
├── lib.rs(用户库 user_lib)
├── linker.ld(应用的链接脚本)
└── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令,
各个具体的 syscall 都是通过 syscall 来实现的)
cloc os
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Rust 14 62 21 435
Assembly 3 9 16 106
make 1 12 4 36
TOML 1 2 1 9
-------------------------------------------------------------------------------
SUM: 19 85 42 586
-------------------------------------------------------------------------------

View File

@@ -0,0 +1,214 @@
实现应用程序
===========================
.. toctree::
:hidden:
:maxdepth: 5
.. note::
拓展阅读:`RISC-V 特权级机制 <https://rcore-os.github.io/rCore-Tutorial-Book-v3/chapter2/1rv-privilege.html>`_
应用程序设计
-----------------------------
.. attention::
用户库看起来很复杂,它预留了直到 ch7 内核才能实现的系统调用接口console 模块还实现了输出缓存区。它们不是为本章准备的,你只需关注本节提到的部分即可。
应用程序、用户库包括入口函数、初始化函数、I/O函数和系统调用接口等多个rs文件组成放在项目根目录的 ``user`` 目录下:
- user/src/bin/*.rs各个应用程序
- user/src/*.rs用户库包括入口函数、初始化函数、I/O函数和系统调用接口等
- user/src/linker.ld应用程序的内存布局说明
项目结构
^^^^^^^^^^^^^^^^^^^^^^
``user/src/bin`` 里面有多个文件,其中三个是:
- ``hello_world``:在屏幕上打印一行 ``Hello, world!``
- ``bad_address``:访问一个非法的物理地址,测试批处理系统是否会被该错误影响
- ``power``:不断在计算操作和打印字符串操作之间切换
批处理系统会按照文件名顺序加载并运行它们。
每个应用程序的实现都在对应的单个文件中。打开 ``hello_world.rs``,能看到一个 ``main`` 函数,还有外部库引用:
.. code-block:: rust
#[macro_use]
extern crate user_lib;
这个外部库其实就是 ``user`` 目录下的 ``lib.rs`` 以及它引用的若干子模块。
``user/Cargo.toml`` 中我们对于库的名字进行了设置: ``name = "user_lib"``
它作为 ``bin`` 目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。
``lib.rs`` 中我们定义了用户库的入口点 ``_start``
.. code-block:: rust
:linenos:
#[no_mangle]
#[link_section = ".text.entry"]
pub extern "C" fn _start() -> ! {
clear_bss();
exit(main());
}
第 2 行使用 ``link_section`` 宏将 ``_start`` 函数编译后的汇编代码放在名为 ``.text.entry`` 的代码段中,
方便用户库链接脚本将它作为用户程序的入口。
而从第 4 行开始,我们手动清零 ``.bss`` 段,然后调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值,
最后,调用用户库提供的 ``exit`` 接口退出,并将返回值告知批处理系统。
我们在 ``lib.rs`` 中看到了另一个 ``main``
.. code-block:: rust
:linenos:
#![feature(linkage)] // 启用弱链接特性
#[linkage = "weak"]
#[no_mangle]
fn main() -> i32 {
panic!("Cannot find main!");
}
我们使用 Rust 宏将其标志为弱链接。这样在最后链接的时候,
虽然 ``lib.rs````bin`` 目录下的某个应用程序中都有 ``main`` 符号,
但由于 ``lib.rs`` 中的 ``main`` 符号是弱链接,
链接器会使用 ``bin`` 目录下的函数作为 ``main``
如果在 ``bin`` 目录下找不到任何 ``main`` ,那么编译也能通过,但会在运行时报错。
内存布局
^^^^^^^^^^^^^^^^^^^^^^
我们使用链接脚本 ``user/src/linker.ld`` 规定用户程序的内存布局:
- 将程序的起始物理地址调整为 ``0x80400000`` ,三个应用程序都会被加载到这个物理地址上运行;
-``_start`` 所在的 ``.text.entry`` 放在整个程序的开头 ``0x80400000``
批处理系统在加载应用后,跳转到 ``0x80400000``,就进入了用户库的 ``_start`` 函数;
- 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。
其余的部分和第一章基本相同。
系统调用
^^^^^^^^^^^^^^^^^^^^^^
在子模块 ``syscall`` 中我们来通过 ``ecall`` 调用批处理系统提供的接口,
由于应用程序运行在用户态(即 U 模式), ``ecall`` 指令会触发名为 ``Environment call from U-mode`` 的异常,
并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务程序。
这个接口被称为 ABI 或者系统调用。
现在我们不关心 S 态的批处理系统如何提供应用程序所需的功能,只考虑如何使用它。
在本章中,应用程序和批处理系统约定如下两个系统调用:
.. code-block:: rust
:caption: 第二章新增系统调用
/// 功能:将内存中缓冲区中的数据写入文件。
/// 参数:`fd` 表示待写入文件的文件描述符;
/// `buf` 表示内存中缓冲区的起始地址;
/// `len` 表示内存中缓冲区的长度。
/// 返回值:返回成功写入的长度。
/// syscall ID64
fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
/// 功能:退出应用程序并将返回值告知批处理系统。
/// 参数:`xstate` 表示应用程序的返回值。
/// 返回值:该系统调用不应该返回。
/// syscall ID93
fn sys_exit(xstate: usize) -> !;
实际调用时,我们要按照 RISC-V 调用规范,在合适的寄存器中放置参数,
然后执行 ``ecall`` 指令触发 Trap。当 Trap 结束,回到 U 模式后,
用户程序会从 ``ecall`` 的下一条指令继续执行,同时在合适的寄存器中读取返回值。
.. note::
RISC-V 寄存器编号从 ``0~31`` ,表示为 ``x0~x31`` 。 其中:
- ``x10~x17`` : 对应 ``a0~a7``
- ``x1`` :对应 ``ra``
约定寄存器 ``a0~a6`` 保存系统调用的参数, ``a0`` 保存系统调用的返回值,
寄存器 ``a7`` 用来传递 syscall ID。
这超出了 Rust 语言的表达能力,我们需要内嵌汇编来完成参数/返回值绑定和 ``ecall`` 指令的插入:
.. code-block:: rust
:linenos:
// user/src/syscall.rs
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
core::arch::asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id
);
}
ret
}
第 3 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。
第 6 行开始,我们使用 Rust 提供的 ``asm!`` 宏在代码中内嵌汇编。
Rust 编译器无法判定汇编代码的安全性,所以我们需要将其包裹在 unsafe 块中。
简而言之,这条汇编代码的执行结果是以寄存器 ``a0~a2`` 来保存系统调用的参数,以及寄存器 ``a7`` 保存 syscall ID
返回值通过寄存器 ``a0`` 传递给局部变量 ``ret``
这段汇编代码与第一章中出现过的内嵌汇编很像,读者可以查看 ``os/src/sbi.rs``
.. note::
可以查看 `Inline assembly <https://doc.rust-lang.org/nightly/reference/inline-assembly.html>`_ 了解 ``asm`` 宏。
于是 ``sys_write````sys_exit`` 只需将 ``syscall`` 进行包装:
.. code-block:: rust
:linenos:
// user/src/syscall.rs
const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
我们将上述两个系统调用在用户库 ``user_lib`` 中进一步封装,像标准库一样:
.. code-block:: rust
:linenos:
// user/src/lib.rs
use syscall::*;
pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) }
pub fn exit(exit_code: i32) -> isize { sys_exit(exit_code) }
``console`` 子模块中,借助 ``write``,我们为应用程序实现了 ``println!`` 宏。
传入到 ``write````fd`` 参数设置为 1代表标准输出 STDOUT暂时不用考虑其他的 ``fd`` 选取情况。
编译生成应用程序二进制码
-------------------------------
简要介绍一下应用程序的构建,在 ``user`` 目录下 ``make build``
1. 对于 ``src/bin`` 下的每个应用程序,
``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件;
2. 使用 objcopy 二进制工具删除所有 ELF header 和符号,得到 ``.bin`` 后缀的纯二进制镜像文件。
它们将被链接进内核,并由内核在合适的时机加载到内存。

View File

@@ -0,0 +1,168 @@
.. _term-batchos:
实现批处理操作系统
==============================
.. toctree::
:hidden:
:maxdepth: 5
将应用程序链接到内核
--------------------------------------------
在本章中,我们要把应用程序的二进制镜像文件作为数据段链接到内核里,
内核需要知道应用程序的数量和它们的位置。
``os/src/main.rs`` 中能够找到这样一行:
.. code-block:: rust
core::arch::global_asm!(include_str!("link_app.S"));
这里我们引入了一段汇编代码 ``link_app.S`` ,它是在 ``make run`` 构建操作系统时自动生成的,里面的内容大致如下:
.. code-block:: asm
:linenos:
# os/src/link_app.S
.align 3
.section .data
.global _num_app
_num_app:
.quad 3
.quad app_0_start
.quad app_1_start
.quad app_2_start
.quad app_2_end
.section .data
.global app_0_start
.global app_0_end
app_0_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/hello_world.bin"
app_0_end:
.section .data
.global app_1_start
.global app_1_end
app_1_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/bad_address.bin"
app_1_end:
.section .data
.global app_2_start
.global app_2_end
app_2_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/power.bin"
app_2_end:
第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,
并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的开始和结束位置。
而第 3 行开始的另一个数据段相当于一个 64 位整数数组。
数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用程序的起始地址,
最后一个元素放置最后一个应用程序的结束位置。这样数组中相邻两个元素记录了每个应用程序的始末位置,
这个数组所在的位置由全局符号 ``_num_app`` 所指示。
这个文件是在 ``cargo build`` 时,由脚本 ``os/build.rs`` 控制生成的。
找到并加载应用程序二进制码
-----------------------------------------------
我们在 ``os````batch`` 子模块中实现一个应用管理器 ``AppManager`` ,结构体定义如下:
.. code-block:: rust
struct AppManager {
num_app: usize,
current_app: usize,
app_start: [usize; MAX_APP_NUM + 1],
}
初始化 ``AppManager`` 的全局实例:
.. code-block:: rust
lazy_static! {
static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe {
UPSafeCell::new({
extern "C" {
fn _num_app();
}
let num_app_ptr = _num_app as usize as *const usize;
let num_app = num_app_ptr.read_volatile();
let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
let app_start_raw: &[usize] =
core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1);
app_start[..=num_app].copy_from_slice(app_start_raw);
AppManager {
num_app,
current_app: 0,
app_start,
}
})
};
}
初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。
用容器 ``UPSafeCell`` 包裹 ``AppManager`` 是为了防止全局对象 ``APP_MANAGER`` 被重复获取。
.. note::
``UPSafeCell`` 实现在 ``sync`` 模块中,调用 ``exclusive_access`` 方法能获取其内部对象的可变引用,
如果程序运行中同时存在多个这样的引用,会触发 ``already borrowed: BorrowMutError``
``UPSafeCell`` 既提供了内部可变性,又在单核情境下防止了内部对象被重复借用,我们将在后文中多次见到它。
这里使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。
``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置初始值,
但是有些全局变量的初始化依赖于运行期间才能得到的数据。
如这里我们借助 ``lazy_static!`` 声明了一个 ``AppManager`` 结构的名为 ``APP_MANAGER`` 的全局实例,
只有在它第一次被使用到的时候才会进行实际的初始化工作。
``AppManager`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``
.. code-block:: rust
:linenos:
unsafe fn load_app(&self, app_id: usize) {
if app_id >= self.num_app {
panic!("All applications completed!");
}
info!("[kernel] Loading app_{}", app_id);
// clear icache
core::arch::asm!("fence.i");
// clear app area
core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
let app_src = core::slice::from_raw_parts(
self.app_start[app_id] as *const u8,
self.app_start[app_id + 1] - self.app_start[app_id],
);
let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
app_dst.copy_from_slice(app_src);
}
这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80400000`` 起始的位置,
这个位置是批处理操作系统和应用程序之间约定的常数地址。
我们将从这里开始的一块内存清空,然后找到待加载应用二进制镜像的位置,并将它复制到正确的位置。
清空内存前,我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。
我们知道, 缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。
通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。
但在这里,我们会修改会被 CPU 取指的内存区域,使得 i-cache 中含有与内存不一致的内容,
必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效,
才能够保证程序执行正确性。
.. warning::
**模拟器与真机的不同之处**
在 Qemu 模拟器上,即使不加刷新 i-cache 的指令,大概率也能正常运行,但在物理计算机上不是这样。
``batch`` 子模块对外暴露出如下接口:
- ``init`` :调用 ``print_app_info`` 的时第一次用到了全局变量 ``APP_MANAGER`` ,它在这时完成初始化;
- ``run_next_app`` :批处理操作系统的核心操作,即加载并运行下一个应用程序。
批处理操作系统完成初始化,或者应用程序运行结束/出错后会调用该函数。下节再介绍其具体实现。

View File

@@ -0,0 +1,519 @@
.. _term-trap-handle:
实现特权级的切换
===========================
.. toctree::
:hidden:
:maxdepth: 5
RISC-V特权级切换
---------------------------------------
特权级切换的起因
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序前进行一些初始化工作,
并监控应用程序的执行,具体体现在:
- 启动应用程序时,需要初始化应用程序的用户态上下文,并能切换到用户态执行应用程序;
- 应用程序发起系统调用后,需要切换到批处理操作系统中进行处理;
- 应用程序执行出错时,批处理操作系统要杀死该应用并加载运行下一个应用;
- 应用程序执行结束时,批处理操作系统要加载运行下一个应用。
这些处理都涉及到特权级切换,因此都需要硬件和操作系统协同提供的特权级切换机制。
特权级切换相关的控制状态寄存器
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
本章中我们仅考虑当 CPU 在 U 特权级运行用户程序的时候触发 Trap
并切换到 S 特权级的批处理操作系统进行处理。
.. list-table:: 进入 S 特权级 Trap 的相关 CSR
:header-rows: 1
:align: center
:widths: 30 100
* - CSR 名
- 该 CSR 与 Trap 相关的功能
* - sstatus
- ``SPP`` 等字段给出 Trap 发生之前 CPU 处在哪个特权级S/U等信息
* - sepc
- 当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址
* - scause
- 描述 Trap 的原因
* - stval
- 给出 Trap 附加信息
* - stvec
- 控制 Trap 处理代码的入口地址
特权级切换的具体过程一部分由硬件直接完成,另一部分则需要由操作系统来实现。
.. _trap-hw-mechanism:
特权级切换的硬件控制机制
-------------------------------------
当 CPU 执行完一条指令并准备从用户特权级 陷入( ``Trap`` )到 S 特权级的时候,硬件会自动完成如下这些事情:
- ``sstatus````SPP`` 字段会被修改为 CPU 当前的特权级U/S
- ``sepc`` 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。
- ``scause/stval`` 分别会被修改成这次 Trap 的原因以及相关的附加信息。
- CPU 会跳转到 ``stvec`` 所设置的 Trap 处理入口地址,并将当前特权级设置为 S 然后从Trap 处理入口地址处开始执行。
.. note::
**stvec 相关细节**
在 RV64 中, ``stvec`` 是一个 64 位的 CSR在中断使能的情况下保存了中断处理的入口地址。它有两个字段
- MODE 位于 [1:0],长度为 2 bits
- BASE 位于 [63:2],长度为 62 bits。
当 MODE 字段为 0 的时候, ``stvec`` 被设置为 Direct 模式,此时进入 S 模式的 Trap 无论原因如何,处理 Trap 的入口地址都是 ``BASE<<2``
CPU 会跳转到这个地方进行异常处理。本书中我们只会将 ``stvec`` 设置为 Direct 模式。而 ``stvec`` 还可以被设置为 Vectored 模式,
而当 CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 ``sret`` 来完成,这一条指令具体完成以下功能:
- CPU 会将当前的特权级按照 ``sstatus````SPP`` 字段设置为 U 或者 S
- CPU 会跳转到 ``sepc`` 寄存器指向的那条指令,然后继续执行。
用户栈与内核栈
--------------------------------
在 Trap 触发的一瞬间, CPU 会切换到 S 特权级并跳转到 ``stvec`` 所指示的位置。
但是在正式进入 S 特权级的 Trap 处理之前,我们必须保存原控制流的寄存器状态,这一般通过栈来完成。
但我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。
我们声明两个类型 ``KernelStack````UserStack`` 分别表示用户栈和内核栈,它们都只是字节数组的简单包装:
.. code-block:: rust
:linenos:
// os/src/batch.rs
#[repr(align(4096))]
struct KernelStack {
data: [u8; KERNEL_STACK_SIZE],
}
#[repr(align(4096))]
struct UserStack {
data: [u8; USER_STACK_SIZE],
}
static KERNEL_STACK: KernelStack = KernelStack {
data: [0; KERNEL_STACK_SIZE],
};
static USER_STACK: UserStack = UserStack {
data: [0; USER_STACK_SIZE],
};
两个栈以全局变量的形式实例化在批处理操作系统的 ``.bss`` 段中。
我们为两个类型实现了 ``get_sp`` 方法来获取栈顶地址。由于在 RISC-V 中栈是向下增长的,
我们只需返回包裹的数组的结尾地址,以用户栈类型 ``UserStack`` 为例:
.. code-block:: rust
:linenos:
impl UserStack {
fn get_sp(&self) -> usize {
self.data.as_ptr() as usize + USER_STACK_SIZE
}
}
换栈是非常简单的,只需将 ``sp`` 寄存器的值修改为 ``get_sp`` 的返回值即可。
.. _term-trap-context:
接下来是 Trap 上下文,即在 Trap 发生时需要保存的物理资源内容,定义如下:
.. code-block:: rust
:linenos:
// os/src/trap/context.rs
#[repr(C)]
pub struct TrapContext {
pub x: [usize; 32],
pub sstatus: Sstatus,
pub sepc: usize,
}
可以看到里面包含所有的通用寄存器 ``x0~x31`` ,还有 ``sstatus````sepc``
- 对于通用寄存器而言,两条控制流(应用程序控制流和内核控制流)运行在不同的特权级,所属的软件也可能由不同的编程语言编写,虽然在 Trap 控制流中只是会执行 Trap 处理
相关的代码,但依然可能直接或间接调用很多模块,因此很难甚至不可能找出哪些寄存器无需保存。既然如此我们就只能全部保存了。但这里也有一些例外,
``x0`` 被硬编码为 0 ,它自然不会有变化;还有 ``tp(x4)`` 寄存器,除非我们手动出于一些特殊用途使用它,否则一般也不会被用到。虽然它们无需保存,
但我们仍然在 ``TrapContext`` 中为它们预留空间,主要是为了后续的实现方便。
- 对于 CSR 而言,我们知道进入 Trap 的时候,硬件会立即覆盖掉 ``scause/stval/sstatus/sepc`` 的全部或是其中一部分。``scause/stval``
的情况是:它总是在 Trap 处理的第一时间就被使用或者是在其他地方保存下来了,因此它没有被修改并造成不良影响的风险。
而对于 ``sstatus/sepc`` 而言,它们会在 Trap 处理的全程有意义(在 Trap 控制流最后 ``sret`` 的时候还用到了它们),而且确实会出现
Trap 嵌套的情况使得它们的值被覆盖掉。所以我们需要将它们也一起保存下来,并在 ``sret`` 之前恢复原样。
Trap 管理
-------------------------------
Trap 上下文的保存与恢复
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
首先是具体实现 Trap 上下文保存和恢复的汇编代码。
.. _trap-context-save-restore:
在批处理操作系统初始化时,我们需要修改 ``stvec`` 寄存器来指向正确的 Trap 处理入口点。
.. code-block:: rust
:linenos:
// os/src/trap/mod.rs
core::arch::global_asm!(include_str!("trap.S"));
pub fn init() {
extern "C" { fn __alltraps(); }
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}
这里我们引入了一个外部符号 ``__alltraps`` ,并将 ``stvec`` 设置为 Direct 模式指向它的地址。我们在 ``os/src/trap/trap.S``
中实现 Trap 上下文保存/恢复的汇编代码,分别用外部符号 ``__alltraps````__restore`` 标记为函数,并通过 ``global_asm!`` 宏将 ``trap.S`` 这段汇编代码插入进来。
Trap 处理的总体流程如下:首先通过 ``__alltraps`` 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 ``trap_handler`` 函数
完成 Trap 分发及处理。当 ``trap_handler`` 返回之后,使用 ``__restore`` 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条
``sret`` 指令回到应用程序执行。
首先是保存 Trap 上下文的 ``__alltraps`` 的实现:
.. code-block:: riscv
:linenos:
# os/src/trap/trap.S
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
.align 2
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
# allocate a TrapContext on kernel stack
addi sp, sp, -34*8
# save general-purpose registers
sd x1, 1*8(sp)
# skip sp(x2), we will save it later
sd x3, 3*8(sp)
# skip tp(x4), application does not use it
# save x5~x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# we can use t0/t1/t2 freely, because they were saved on kernel stack
csrr t0, sstatus
csrr t1, sepc
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# read user stack from sscratch and save it on the kernel stack
csrr t2, sscratch
sd t2, 2*8(sp)
# set input argument of trap_handler(cx: &mut TrapContext)
mv a0, sp
call trap_handler
- 第 7 行我们使用 ``.align````__alltraps`` 的地址 4 字节对齐,这是 RISC-V 特权级规范的要求;
- 第 9 行的 ``csrrw`` 原型是 :math:`\text{csrrw rd, csr, rs}` 可以将 CSR 当前的值读到通用寄存器 :math:`\text{rd}` 中,然后将
通用寄存器 :math:`\text{rs}` 的值写入该 CSR 。因此这里起到的是交换 sscratch 和 sp 的效果。在这一行之前 sp 指向用户栈, sscratch
指向内核栈(原因稍后说明),现在 sp 指向内核栈, sscratch 指向用户栈。
- 第 12 行,我们准备在内核栈上保存 Trap 上下文,于是预先分配 :math:`34\times 8` 字节的栈帧,这里改动的是 sp ,说明确实是在内核栈上。
- 第 13~24 行,保存 Trap 上下文的通用寄存器 x0~x31跳过 x0 和 tp(x4),原因之前已经说明。我们在这里也不保存 sp(x2),因为它在第 9 行
后指向的是内核栈。用户栈的栈指针保存在 sscratch 中,必须通过 ``csrr`` 指令读到通用寄存器中后才能使用,因此我们先考虑保存其它通用寄存器,腾出空间。
我们要基于 sp 来找到每个寄存器应该被保存到的正确的位置。实际上,在栈帧分配之后,我们可用于保存 Trap 上下文的地址区间为 :math:`[\text{sp},\text{sp}+8\times34)`
按照 ``TrapContext`` 结构体的内存布局基于内核栈的位置sp所指地址来从低地址到高地址分别按顺序放置 x0~x31这些通用寄存器最后是 sstatus 和 sepc 。因此通用寄存器 xn
应该被保存在地址区间 :math:`[\text{sp}+8n,\text{sp}+8(n+1))`
为了简化代码x5~x31 这 27 个通用寄存器我们通过类似循环的 ``.rept`` 每次使用 ``SAVE_GP`` 宏来保存,其实质是相同的。注意我们需要在
``trap.S`` 开头加上 ``.altmacro`` 才能正常使用 ``.rept`` 命令。
- 第 25~28 行,我们将 CSR sstatus 和 sepc 的值分别读到寄存器 t0 和 t1 中然后保存到内核栈对应的位置上。指令
:math:`\text{csrr rd, csr}` 的功能就是将 CSR 的值读到寄存器 :math:`\text{rd}` 中。这里我们不用担心 t0 和 t1 被覆盖,
因为它们刚刚已经被保存了。
- 第 30~31 行专门处理 sp 的问题。首先将 sscratch 的值读到寄存器 t2 并保存到内核栈上,注意: sscratch 的值是进入 Trap 之前的 sp 的值,指向
用户栈。而现在的 sp 则指向内核栈。
- 第 33 行令 :math:`\text{a}_0\leftarrow\text{sp}`,让寄存器 a0 指向内核栈的栈指针也就是我们刚刚保存的 Trap 上下文的地址,
这是由于我们接下来要调用 ``trap_handler`` 进行 Trap 处理,它的第一个参数 ``cx`` 由调用规范要从 a0 中获取。而 Trap 处理函数
``trap_handler`` 需要 Trap 上下文的原因在于:它需要知道其中某些寄存器的值,比如在系统调用的时候应用程序传过来的 syscall ID 和
对应参数。我们不能直接使用这些寄存器现在的值,因为它们可能已经被修改了,因此要去内核栈上找已经被保存下来的值。
.. _term-atomic-instruction:
.. note::
**CSR 相关原子指令**
RISC-V 中读写 CSR 的指令是一类能不会被打断地完成多个读写操作的指令。这种不会被打断地完成多个操作的指令被称为 **原子指令** (Atomic Instruction)。这里的 **原子** 的含义是“不可分割的最小个体”,也就是说指令的多个操作要么都不完成,要么全部完成,而不会处于某种中间状态。
``trap_handler`` 返回之后会从调用 ``trap_handler`` 的下一条指令开始执行,也就是从栈上的 Trap 上下文恢复的 ``__restore``
.. _code-restore:
.. code-block:: riscv
:linenos:
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
__restore:
# case1: start running app by __restore
# case2: back to U after handling trap
mv sp, a0
# now sp->kernel stack(after allocated), sscratch->user stack
# restore sstatus/sepc
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
# restore general-purpuse registers except sp/tp
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
# release TrapContext on kernel stack
addi sp, sp, 34*8
# now sp->kernel stack, sscratch->user stack
csrrw sp, sscratch, sp
sret
- 第 8 行比较奇怪,我们暂且不管,假设它从未发生,那么 sp 仍然指向内核栈的栈顶。
- 第 11~24 行负责从内核栈顶的 Trap 上下文恢复通用寄存器和 CSR 。注意我们要先恢复 CSR 再恢复通用寄存器,这样我们使用的三个临时寄存器
才能被正确恢复。
- 在第 26 行之前sp 指向保存了 Trap 上下文之后的内核栈栈顶, sscratch 指向用户栈栈顶。我们在第 26 行在内核栈上回收 Trap 上下文所
占用的内存,回归进入 Trap 之前的内核栈栈顶。第 27 行,再次交换 sscratch 和 sp现在 sp 重新指向用户栈栈顶sscratch 也依然保存
进入 Trap 之前的状态并指向内核栈栈顶。
- 在应用程序控制流状态被还原之后,第 28 行我们使用 ``sret`` 指令回到 U 特权级继续运行应用程序控制流。
Trap 分发与处理
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Trap 在使用 Rust 实现的 ``trap_handler`` 函数中完成分发和处理:
.. code-block:: rust
:linenos:
// os/src/trap/mod.rs
#[no_mangle]
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
let scause = scause::read();
let stval = stval::read();
match scause.cause() {
Trap::Exception(Exception::UserEnvCall) => {
cx.sepc += 4;
cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
}
Trap::Exception(Exception::StoreFault) |
Trap::Exception(Exception::StorePageFault) => {
println!("[kernel] PageFault in application, core dumped.");
run_next_app();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!("[kernel] IllegalInstruction in application, core dumped.");
run_next_app();
}
_ => {
panic!("Unsupported trap {:?}, stval = {:#x}!", scause.cause(), stval);
}
}
cx
}
- 第 4 行声明返回值为 ``&mut TrapContext`` 并在第 25 行实际将传入的 ``cx`` 原样返回,因此在 ``__restore`` 的时候 ``a0`` 寄存器在调用
``trap_handler`` 前后并没有发生变化,仍然指向分配 Trap 上下文之后的内核栈栈顶,和此时 ``sp`` 的值相同,我们 :math:`\text{sp}\leftarrow\text{a}_0`
并不会有问题;
- 第 7 行根据 ``scause`` 寄存器所保存的 Trap 的原因进行分发处理。这里我们无需手动操作这些 CSR ,而是使用 Rust 第三方库 riscv 。
- 第 8~11 行,发现触发 Trap 的原因是来自 U 特权级的 Environment Call也就是系统调用。这里我们首先修改保存在内核栈上的 Trap 上下文里面
sepc让其增加 4。这是因为我们知道这是一个由 ``ecall`` 指令触发的系统调用,在进入 Trap 的时候,硬件会将 sepc 设置为这条 ``ecall``
指令所在的地址(因为它是进入 Trap 之前最后一条执行的指令)。而在 Trap 返回之后,我们希望应用程序控制流从 ``ecall`` 的下一条指令
开始执行。因此我们只需修改 Trap 上下文里面的 sepc让它增加 ``ecall`` 指令的码长,也即 4 字节。这样在 ``__restore`` 的时候 sepc
在恢复之后就会指向 ``ecall`` 的下一条指令,并在 ``sret`` 之后从那里开始执行。
用来保存系统调用返回值的 a0 寄存器也会同样发生变化。我们从 Trap 上下文取出作为 syscall ID 的 a7 和系统调用的三个参数 a0~a2 传给
``syscall`` 函数并获取返回值。 ``syscall`` 函数是在 ``syscall`` 子模块中实现的。 这段代码是处理正常系统调用的控制逻辑。
- 第 12~20 行,分别处理应用程序出现访存错误和非法指令错误的情形。此时需要打印错误信息并调用 ``run_next_app`` 直接切换并运行下一个
应用程序。
- 第 21 行开始,当遇到目前还不支持的 Trap 类型的时候,批处理操作系统整个 panic 报错退出。
对于系统调用而言, ``syscall`` 函数并不会实际处理系统调用,而只是根据 syscall ID 分发到具体的处理函数:
.. code-block:: rust
:linenos:
// os/src/syscall/mod.rs
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
这里我们会将传进来的参数 ``args`` 转化成能够被具体的系统调用处理函数接受的类型。它们的实现都非常简单:
.. code-block:: rust
:linenos:
// os/src/syscall/fs.rs
const FD_STDOUT: usize = 1;
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
match fd {
FD_STDOUT => {
let slice = unsafe { core::slice::from_raw_parts(buf, len) };
let str = core::str::from_utf8(slice).unwrap();
print!("{}", str);
len as isize
},
_ => {
panic!("Unsupported fd in sys_write!");
}
}
}
// os/src/syscall/process.rs
pub fn sys_exit(xstate: i32) -> ! {
println!("[kernel] Application exited with code {}", xstate);
run_next_app()
}
- ``sys_write`` 我们将传入的位于应用程序内的缓冲区的开始地址和长度转化为一个字符串 ``&str`` ,然后使用批处理操作系统已经实现的 ``print!``
宏打印出来。这里我们并没有检查传入参数的安全性,存在安全隐患。
- ``sys_exit`` 打印退出的应用程序的返回值并同样调用 ``run_next_app`` 切换到下一个应用程序。
.. _ch2-app-execution:
执行应用程序
-------------------------------------
当批处理操作系统初始化完成,或者是某个应用程序运行结束或出错的时候,我们要调用 ``run_next_app`` 函数切换到下一个应用程序。此时 CPU 运行在
S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,唯一一种能够使得 CPU 特权级下降的方法就是通过 Trap 返回系列指令,比如
``sret`` 。事实上,在运行应用程序之前要完成如下这些工作:
- 跳转到应用程序入口点 ``0x80400000``
- 将使用的栈切换到用户栈;
-``__alltraps`` 时我们要求 ``sscratch`` 指向内核栈,这个也需要在此时完成;
- 从 S 特权级切换到 U 特权级。
它们可以通过复用 ``__restore`` 的代码来更容易的实现上述工作。我们只需要在内核栈上压入一个为启动应用程序而特殊构造的 Trap 上下文,再通过 ``__restore`` 函数,就能
让这些寄存器到达启动应用程序所需要的上下文状态。
.. code-block:: rust
:linenos:
// os/src/trap/context.rs
impl TrapContext {
pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; }
pub fn app_init_context(entry: usize, sp: usize) -> Self {
let mut sstatus = sstatus::read();
sstatus.set_spp(SPP::User);
let mut cx = Self {
x: [0; 32],
sstatus,
sepc: entry,
};
cx.set_sp(sp);
cx
}
}
``TrapContext`` 实现 ``app_init_context`` 方法,修改其中的 sepc 寄存器为应用程序入口点 ``entry`` sp 寄存器为我们设定的
一个栈指针,并将 sstatus 寄存器的 ``SPP`` 字段设置为 User 。
``run_next_app`` 函数中我们能够看到:
.. code-block:: rust
:linenos:
// os/src/batch.rs
pub fn run_next_app() -> ! {
let mut app_manager = APP_MANAGER.exclusive_access();
let current_app = app_manager.get_current_app();
unsafe {
app_manager.load_app(current_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(TrapContext::app_init_context(
APP_BASE_ADDRESS,
USER_STACK.get_sp(),
)) as *const _ as usize);
}
panic!("Unreachable in batch::run_current_app!");
}
``__restore`` 所做的事情是在内核栈上压入一个 Trap 上下文,其 ``sepc`` 是应用程序入口地址 ``0x80400000`` ,其 ``sp`` 寄存器指向用户栈,其 ``sstatus``
``SPP`` 字段被设置为 User 。
``push_context`` 的返回值是内核栈压入 Trap 上下文之后的栈顶,它会被作为 ``__restore`` 的参数(
回看 :ref:`__restore 代码 <code-restore>` ,这时我们可以理解为何 ``__restore`` 函数的起始部分会完成
:math:`\text{sp}\leftarrow\text{a}_0` ),这使得在 ``__restore`` 函数中 ``sp`` 仍然可以指向内核栈的栈顶。这之后,就和执行一次普通的
``__restore`` 函数调用一样了。
.. note::
有兴趣的读者可以思考: sscratch 是何时被设置为内核栈顶的?
..
马老师发生甚么事了?
--
这里要说明目前只考虑从 U Trap 到 S ,而实际上 Trap 的要素就有Trap 之前在哪个特权级Trap 在哪个特权级处理。这个对于中断和异常
都是如此,只不过中断可能跟特权级的关系稍微更紧密一点。毕竟中断的类型都是跟特权级挂钩的。但是对于 Trap 而言有一点是共同的,也就是触发
Trap 不会导致优先级下降。从中断/异常的代理就可以看出从定义上就不允许代理到更低的优先级。而且代理只能逐级代理,目前我们能操作的只有从
M 代理到 S其他代理都基本只出现在指令集拓展或者硬件还不支持。中断的情况是如果是属于某个特权级的中断不能在更低的优先级处理。事实上
这个中断只可能在 CPU 处于不会更高的优先级上收到(否则会被屏蔽),而 Trap 之后优先级不会下降Trap 代理机制决定),这样就自洽了。
--
之前提到异常是说需要执行环境功能的原因与某条指令的执行有关。而 Trap 的定义更加广泛一些,就是在执行某条指令之后发现需要执行环境的功能,
如果是中断的话 Trap 回来之后默认直接执行下一条指令,如果是异常的话硬件会将 sepc 设置为 Trap 发生之前最后执行的那条指令,而异常发生
的原因不一定和这条指令的执行有关。应该指出的是,在大多数情况下都是和最后这条指令的执行有关。但在缓存的作用下也会出现那种特别极端的情况。
--
然后是 Trap 到 S就有 S 模式的一些相关 CSR以及从 U Trap 到 S硬件会做哪些事情包括触发异常的一瞬间以及处理完成调用 sret
之后)。然后指出从用户的视角来看,如果是 ecall 的话, Trap 回来之后应该从 ecall 的下一条指令开始执行,且执行现场不能发生变化。
所以就需要将应用执行环境保存在内核栈上(还需要换栈!)。栈存在的原因可能是 Trap handler 是一条新的运行在 S 特权级的执行流,所以
这个可以理解成跨特权级的执行流切换,确实就复杂一点,要保存的内容也相对多一点。而下一章多任务的任务切换是全程发生在 S 特权级的执行流
切换,所以会简单一点,保存的通用寄存器大概率更少(少在调用者保存寄存器),从各种意义上都很像函数调用。从不同特权级的角度来解释换栈
是出于安全性,应用不应该看到 Trap 执行流的栈,这样做完之后,虽然理论上可以访问,但应用不知道内核栈的位置应该也有点麻烦。
--
然后是 rust_trap 的处理,尤其是奇妙的参数传递,内部处理逻辑倒是非常简单。
--
最后是如何利用 __restore 初始化应用的执行环境,包括如何设置入口点、用户栈以及保证在 U 特权级执行。

View File

@@ -0,0 +1,139 @@
chapter2练习已废弃
=====================================================
.. toctree::
:hidden:
:maxdepth: 4
编程练习
-------------------------------
简单安全检查
+++++++++++++++++++++++++++++++
.. lab2 中,我们实现了第一个系统调用 ``sys_write``,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。
.. 由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查:
.. - sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。
实验要求
+++++++++++++++++++++++++++++++
.. - 实现分支: ch2。
.. - 完成实验指导书中的内容,能运行用户态程序并执行 sys_writesys_exit 系统调用。
.. - 为 sys_write 增加安全性检查,并通过 `Rust测例 <https://github.com/DeathWish5/rCore_tutorial_tests>`_ 中 chapter2 对应的所有测例,测例详情见对应仓库。
.. challenge: 支持多核,实现多个核运行用户程序。
实验约定
++++++++++++++++++++++++++++++
.. 在第二章的测试中,我们对于内核有如下仅仅为了测试方便的要求,请调整你的内核代码来符合这些要求。
.. - 用户栈大小必须为 4096且按照 4096 字节对齐。这一规定可以在实验4开始删除仅仅为通过 lab2/3 测例设置。
.. .. _inherit-last-ch-changes:
.. .. note::
.. **如何快速继承上一章练习题的修改**
.. 从这一章开始,在完成本章习题之前,首先要做的就是将上一章框架的修改继承到本章的框架代码。出于各种原因,实际上通过 ``git merge`` 并不是很方便,这里给出一种打 patch 的方法,希望能够有所帮助。
.. 1. 切换到上一章的分支,通过 ``git log`` 找到你在此分支上的第一次 commit 的前一个 commit 的 ID ,复制其前 8 位,记作 ``base-commit`` 。假设分支上最新的一次 commit ID 是 ``last-commit`` 。
.. 2. 确保你位于项目根目录 ``rCore-Tutorial-v3`` 下。通过 ``git diff <base-commit> <last-commit> > <patch-path>`` 即可在 ``patch-path`` 路径位置(比如 ``~/Desktop/chx.patch`` )生成一个描述你对于上一章分支进行的全部修改的一个补丁文件。打开看一下,它给出了每个被修改的文件中涉及了哪些块的修改,还附加了块前后的若干行代码。如果想更加灵活进行合并的话,可以通过 ``git format-patch <base-commit>`` 命令在当前目录下生成一组补丁,它会对于 ``base-commit`` 后面的每一次 commit 均按照顺序生成一个补丁。
.. 3. 切换到本章分支,通过 ``git apply --reject <patch-path>`` 来将一个补丁打到当前章节上。它的大概原理是对于补丁中的每个被修改文件中的每个修改块,尝试通过块的前后若干行代码来定位它在当前分支上的位置并进行替换。有一些块可能无法匹配,此时会生成与这些块所在的文件同名的 ``*.rej`` 文件,描述了哪些块替换失败了。在项目根目录 ``rCore-Tutorial-v3`` 下,可以通过 ``find . -name *.rej`` 来找到所有相关的 ``*.rej`` 文件并手动完成替换。
.. 4. 在处理完所有 ``*.rej`` 之后,将它们删除并 commit 一下。现在就可以开始本章的实验了。
实验检查
++++++++++++++++++++++++++++++
.. - 实验目录要求(Rust)
.. .. code-block::
.. ├── os(内核实现)
.. │   ├── build.rs (在这里实现用户程序的打包)
.. │   ├── Cargo.toml(配置文件)
.. │   ├── Makefile (要求 make run 可以正确执行,尽量不输出调试信息)
.. │   ├── src(所有内核的源代码放在 os/src 目录下)
.. │   ├── main.rs(内核主函数)
.. │   ├── ...
.. ├── reports
.. │   ├── lab2.md/pdf
.. │   └── ...
.. ├── README.md其他必要的说明
.. ├── ...
.. 参考示例目录结构。目标用户目录 ``../user/build/bin``。
.. - 检查
.. .. code-block:: console
.. $ git checkout ch2
.. $ cd os
.. $ make run
.. 可以正确执行正确执行目标用户测例,并得到预期输出(详见测例注释)。
简答题
-------------------------------
.. 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。目前由于一些其他原因,这些问题不太好测试,请同学们可以自行测试这些内容(参考 `前三个测例 <https://github.com/DeathWish5/rCore_tutorial_tests/tree/master/user/src/bin>`_ ),描述程序出错行为,同时注意注明你使用的 sbi 及其版本。
.. 2. 请结合用例理解 `trap.S <https://github.com/rcore-os/rCore-Tutorial-v3/blob/ch2/os/src/trap/trap.S>`_ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下几个问题:
.. 1. L40刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。
.. 2. L46-L51这几行汇编代码特殊处理了哪些寄存器这些寄存器的的值对于进入用户态有何意义请分别解释。
.. .. code-block:: riscv
.. ld t0, 32*8(sp)
.. ld t1, 33*8(sp)
.. ld t2, 2*8(sp)
.. csrw sstatus, t0
.. csrw sepc, t1
.. csrw sscratch, t2
.. 3. L53-L59为何跳过了 ``x2`` 和 ``x4``
.. .. code-block:: riscv
.. ld x1, 1*8(sp)
.. ld x3, 3*8(sp)
.. .set n, 5
.. .rept 27
.. LOAD_GP %n
.. .set n, n+1
.. .endr
.. 4. L63该指令之后``sp`` 和 ``sscratch`` 中的值分别有什么意义?
.. .. code-block:: riscv
.. csrrw sp, sscratch, sp
.. 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?
.. 6. L13该指令之后``sp`` 和 ``sscratch`` 中的值分别有什么意义?
.. .. code-block:: riscv
.. csrrw sp, sscratch, sp
.. 7. 从 U 态进入 S 态是哪一条指令发生的?
.. 3. 程序陷入内核的原因有中断和异常(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。
.. 4. 对于任何中断,``__alltraps`` 中都需要保存所有寄存器吗?你有没有想到一些加速 ``__alltraps`` 的方法?简单描述你的想法。
报告要求
-------------------------------
- 简单总结你实现的功能200字以内不要贴代码
- 完成问答题。
- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

View File

@@ -0,0 +1,13 @@
.. _link-chapter2:
第二章:批处理系统
==============================================
.. toctree::
:maxdepth: 4
0intro
2application
3batch-system
4trap-handling