Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
* [线程的创建](docs/lab-4/guide/part-2.md)
* [线程的切换](docs/lab-4/guide/part-3.md)
* [线程的结束](docs/lab-4/guide/part-4.md)
* [内核栈](docs/lab-4/guide/part-5.md)
* [中断栈](docs/lab-4/guide/part-5.md)
* [线程调度](docs/lab-4/guide/part-6.md)
* [小结](docs/lab-4/guide/summary.md)
* 实验指导五
Expand Down
4 changes: 2 additions & 2 deletions docs/lab-1/guide/part-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@

(这个寄存器的用处会在实现线程时起到作用,目前仅了解即可)

在用户态,`sscratch` 保存内核栈的地址;在内核态,`sscratch` 的值为 0。
在用户态,`sscratch` 保存中断栈的地址;在内核态,`sscratch` 的值为 0。

为了能够执行内核态的中断处理流程,仅有一个入口地址是不够的。中断处理流程很可能需要使用栈,而程序当前的用户栈是不安全的。因此,我们还需要一个预设的安全的栈空间,存放在这里。
为了能够执行内核态的中断处理流程,仅有一个入口地址是不够的。中断处理流程很可能需要使用栈,而程序当前的运行栈是不安全的。因此,我们还需要一个预设的安全的栈空间,存放在这里。

在内核态中,`sp` 可以认为是一个安全的栈空间,`sscratch` 便不需要保存任何值。此时将其设为 0,可以在遇到中断时通过 `sscratch` 中的值判断中断前程序是否处于内核态。

Expand Down
2 changes: 1 addition & 1 deletion docs/lab-4/guide/part-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- **运行栈**:每个线程都必须有一个独立的运行栈,保存运行时数据。
- **线程执行上下文**:当线程不在执行时,我们需要保存其上下文(其实就是一堆**寄存器**的值),这样之后才能够将其恢复,继续运行。和之前实现的中断一样,上下文由 `Context` 类型保存。(注:这里的**线程执行上下文**与前面提到的**中断上下文**是不同的概念)
- **所属进程的记号**:同一个进程中的多个线程,会共享页表、打开文件等信息。因此,我们将它们提取出来放到线程中。
- ***内核栈***:除了线程运行必须有的运行栈,中断处理也必须有一个单独的栈。之前,我们的中断处理是直接在原来的栈上进行(我们直接将 `Context` 压入栈)。但是在后面我们会引入用户线程,这时就只有上帝才知道发生了什么——栈指针、程序指针都可能在跨国(**国 == 特权态**)旅游。为了确保中断处理能够进行(让操作系统能够接管这样的线程),中断处理必须运行在一个准备好的、安全的栈上。这就是内核栈。不过,内核栈并没有存储在线程信息中。(注:**它的使用方法会有些复杂,我们会在后面讲解**。)
- ***中断栈***:除了线程运行必须有的运行栈,中断处理也必须有一个单独的栈。之前,我们的中断处理是直接在原来的栈上进行(我们直接将 `Context` 压入栈)。但是在后面我们会引入用户线程,这时就只有上帝才知道发生了什么——栈指针、程序指针都可能在跨国(**国 == 特权态**)旅游。为了确保中断处理能够进行(让操作系统能够接管这样的线程),中断处理必须运行在一个准备好的、安全的栈上。这就是中断栈。不过,中断栈并没有存储在线程信息中。(注:**它的使用方法会有些复杂,我们会在后面讲解**。)

{% label %}os/src/process/thread.rs{% endlabel %}
```rust
Expand Down
8 changes: 4 additions & 4 deletions docs/lab-4/guide/part-3.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

在线程切换时(即时钟中断时),`handle_interrupt` 函数需要将上一个线程的 `Context` 保存起来,然后将下一个线程的 `Context` 恢复并返回。

> 注 1:为什么不直接 in-place 修改 `Context` 呢?这是因为 `handle_interrupt` 函数返回的 `Context` 指针除了存储上下文以外,还提供了内核栈的地址。这个会在后面详细阐述。
> 注 1:为什么不直接 in-place 修改 `Context` 呢?这是因为 `handle_interrupt` 函数返回的 `Context` 指针除了存储上下文以外,还提供了中断栈的地址。这个会在后面详细阐述。
>
> 注 2:在 Rust 中,引用 `&mut` 和指针 `*mut` 只是编译器的理解不同,其本质都是一个存储对象地址的寄存器。这里返回值使用指针而不是引用,是因为其指向的位置十分特殊,其生命周期在这里没有意义。

Expand Down Expand Up @@ -96,16 +96,16 @@ pub fn prepare(&self) -> *mut Context {
self.process.inner().memory_set.activate();
// 取出 Context
let parked_frame = self.inner().context.take().unwrap();
// 将 Context 放至内核栈顶
// 将 Context 放至中断栈顶
unsafe { KERNEL_STACK.push_context(parked_frame) }
}
```

思考:在 `run` 函数中,我们在一开始就激活了页表,会不会导致后续流程无法正常执行?

#### 内核栈
#### 中断栈

现在,线程保存 `Context` 都是根据 `sp` 指针,在栈上压入一个 `Context` 来存储。但是,对于一个用户线程而言,它在用户态运行时用的是位于用户空间的用户栈。而它在用户态运行中如果触发中断,`sp` 指针指向的是用户空间的某地址,但此时 RISC-V CPU 会切换到内核态继续执行,就不能再用这个 `sp` 指针指向的用户空间地址了。这样,我们需要为 sp 指针准备好一个专门用于在内核态执行函数的内核栈。所以,为了不让一个线程的崩溃导致操作系统的崩溃,我们需要提前准备好内核栈,当线程发生中断时可用来存储线程的 `Context`。在下一节我们将具体讲解该如何做。
现在,线程保存 `Context` 都是根据 `sp` 指针,在栈上压入一个 `Context` 来存储。但是,对于一个用户线程而言,它在用户态运行时用的是位于用户空间的运行栈。而它在用户态运行中如果触发中断,`sp` 指针指向的是用户空间的某地址,但此时 RISC-V CPU 会切换到内核态继续执行,就不能再用这个 `sp` 指针指向的用户空间地址了。这样,我们需要为 sp 指针准备好一个专门用于在内核态执行函数的中断栈。所以,为了不让一个线程的崩溃导致操作系统的崩溃,我们需要提前准备好中断栈,当线程发生中断时可用来存储线程的 `Context`。在下一节我们将具体讲解该如何做。

### 小结

Expand Down
38 changes: 19 additions & 19 deletions docs/lab-4/guide/part-5.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
## 内核栈
## 中断栈

### 为什么 / 怎么做

在实现内核栈之前,让我们先检查一下需求和我们的解决办法。
在实现中断栈之前,让我们先检查一下需求和我们的解决办法。

- **不是每个线程都需要一个独立的内核栈**,因为内核栈只会在中断时使用,而中断结束后就不再使用。在只有一个 CPU 的情况下,不会有两个线程同时出现中断,**所以我们只需要实现一个共用的内核栈就可以了**。
- **每个线程都需要能够在中断时第一时间找到内核栈的地址**。这时,所有通用寄存器的值都无法预知,也无法从某个变量来加载地址。为此,**我们将内核栈的地址存放到内核态使用的特权寄存器 `sscratch` 中**。这个寄存器只能在内核态访问,这样在中断发生时,就可以安全地找到内核栈了
- **不是每个线程都需要一个独立的中断栈**,因为中断栈只会在中断时使用,而中断结束后就不再使用。在只有一个 CPU 的情况下,不会有两个线程同时出现中断,**所以我们只需要实现一个共用的中断栈就可以了**。
- **每个线程都需要能够在中断时第一时间找到中断栈的地址**。这时,所有通用寄存器的值都无法预知,也无法从某个变量来加载地址。为此,**我们将中断栈的地址存放到内核态使用的特权寄存器 `sscratch` 中**。这个寄存器只能在内核态访问,这样在中断发生时,就可以安全地找到中断栈了

因此,我们的做法就是:

- 预留一段空间作为内核栈
- 运行线程时,在 `sscratch` 寄存器中保存内核栈指针
- 预留一段空间作为中断栈
- 运行线程时,在 `sscratch` 寄存器中保存中断栈指针
- 如果线程遇到中断,则从将 `Context` 压入 `sscratch` 指向的栈中(`Context` 的地址为 `sscratch - size_of::<Context>()`),同时用新的栈地址来替换 `sp`(此时 `sp` 也会被复制到 `a0` 作为 `handle_interrupt` 的参数)
- 从中断中返回时(`__restore` 时),`a0` 应指向**被压在内核栈中的 `Context`**。此时出栈 `Context` 并且将栈顶保存到 `sscratch` 中
- 从中断中返回时(`__restore` 时),`a0` 应指向**被压在中断栈中的 `Context`**。此时出栈 `Context` 并且将栈顶保存到 `sscratch` 中

### 实现

#### 为内核栈预留空间
#### 为中断栈预留空间

我们直接使用一个 `static mut` 来指定一段空间作为栈。

{% label %}os/src/process/kernel_stack.rs{% endlabel %}
```rust
/// 内核栈
/// 中断栈
#[repr(align(16))]
#[repr(C)]
pub struct KernelStack([u8; KERNEL_STACK_SIZE]);

/// 公用的内核栈
/// 公用的中断栈
pub static mut KERNEL_STACK: KernelStack = KernelStack([0; STACK_SIZE]);
```

在我们创建线程时,需要使用的操作就是在内核栈顶压入一个初始状态 `Context`:
在我们创建线程时,需要使用的操作就是在中断栈顶压入一个初始状态 `Context`:

{% label %}os/src/process/kernel_stack.rs{% endlabel %}
```rust
Expand All @@ -57,13 +57,13 @@ impl KernelStack {
{% label %}os/src/interrput/interrupt.asm{% endlabel %}
```asm
__interrupt:
# 因为线程当前的栈不一定可用,必须切换到内核栈来保存 Context 并进行中断流程
# 因此,我们使用 sscratch 寄存器保存内核栈地址
# 因为线程当前的栈不一定可用,必须切换到中断栈来保存 Context 并进行中断流程
# 因此,我们使用 sscratch 寄存器保存中断栈地址
# 思考:sscratch 的值最初是在什么地方写入的?

# 交换 sp 和 sscratch(切换到内核栈
# 交换 sp 和 sscratch(切换到中断栈
csrrw sp, sscratch, sp
# 在内核栈开辟 Context 的空间
# 在中断栈开辟 Context 的空间
addi sp, sp, -36*8

# 保存通用寄存器,除了 x0(固定为 0)
Expand All @@ -80,8 +80,8 @@ __interrupt:
{% label %}os/src/interrupt/interrupt.asm{% endlabel %}
```asm
# 离开中断
# 此时内核栈顶被推入了一个 Context,而 a0 指向它
# 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录内核栈地址
# 此时中断栈顶被推入了一个 Context,而 a0 指向它
# 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录中断栈地址
# 最后跳转至恢复的 sepc 的位置
__restore:
# 从 a0 中读取 sp
Expand All @@ -92,7 +92,7 @@ __restore:
LOAD t1, 33
csrw sstatus, t0
csrw sepc, t1
# 将内核栈地址写入 sscratch
# 将中断栈地址写入 sscratch
addi t0, sp, 36*8
csrw sscratch, t0

Expand All @@ -102,7 +102,7 @@ __restore:

### 小结

为了能够鲁棒地处理用户线程产生的异常,我们为线程准备好一个内核栈,发生中断时会切换到这里继续处理。
为了能够鲁棒地处理用户线程产生的异常,我们为线程准备好一个中断栈,发生中断时会切换到这里继续处理。

#### 思考

Expand Down
2 changes: 1 addition & 1 deletion docs/lab-4/guide/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- 通过设置 `Context`,可以构造一个线程的初始状态
- 通过 `__restore` 标签,直接进入第一个线程之中
- 用 `Context` 来保存进程的状态,从而实现在时钟中断时切换线程
- 实现内核栈,提供安全的中断处理空间
- 实现中断栈,提供安全的中断处理空间
- 实现调度器,完成线程的调度

同时,可以发现我们这一章的内容集中在内核线程上面,对用户进程还没有过多的提及。而为了让用户进程可以在我们的系统上运行起来,一个优美的做法将会是隔开用户程序和内核。需要注意到现在的内核还直接放在了内存上,在下一个章节,我们暂时跳过用户进程,实现可以放置用户数据的文件系统。
Expand Down
2 changes: 1 addition & 1 deletion docs/lab-4/practice-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
{% endreveal %}

<br>
2. 设计:如果不使用 `sscratch` 提供内核栈,而是像原来一样,遇到中断就直接将上下文压栈,请举出(思路即可,无需代码):
2. 设计:如果不使用 `sscratch` 提供中断栈,而是像原来一样,遇到中断就直接将上下文压栈,请举出(思路即可,无需代码):
- 一种情况不会出现问题
- 一种情况导致异常无法处理(指无法进入 `handle_interrupt`)
- 一种情况导致产生嵌套异常(指第二个异常能够进行到调用 `handle_interrupt`,不考虑后续执行情况)
Expand Down
2 changes: 1 addition & 1 deletion docs/lab-5/guide/part-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ extern "C" fn virtio_virt_to_phys(va: VirtualAddress) -> PhysicalAddress {
{% reveal %}
> 我们拿到地址的究极目的是访问地址上的内容,需要注意到在 0x80000000 到 0x88000000 的区间的物理页有可能对应着两个虚拟页,我们在启动或是新建内核线程的时候都包含诸如 0xffffffff80000000 到 0x80000000 这样的线性映射,这意味着,在内核线程里面,只要一个物理地址加上偏移得到的虚拟地址肯定是可以访问对应的物理地址的。所以,把物理地址转为虚拟地址加个偏移既可。
>
> 也需要注意到,内核线程虽然代码和数据都是线性映射的,但是内核栈是以 Frame 为单位分配的(除了 boot 线程是直接放在 .bss 中),而以 Frame 为单位分配意味着,虚拟地址可能从 0 开始,这个时候要转为物理地址,显然不是减去偏移量的线性映射,而必须查当前的表。
> 也需要注意到,内核线程虽然代码和数据都是线性映射的,但是中断栈是以 Frame 为单位分配的(除了 boot 线程是直接放在 .bss 中),而以 Frame 为单位分配意味着,虚拟地址可能从 0 开始,这个时候要转为物理地址,显然不是减去偏移量的线性映射,而必须查当前的表。
>
> 这个时候,你可能问了:为什么 RISC-V 处理器可以通过虚拟地址来访问,但是我还要手写查表来做这件事呢?这是因为 RISC-V 还真没有直接用 MMU 得到地址的指令,我们只能手写。
{% endreveal %}
2 changes: 1 addition & 1 deletion os/src/interrupt/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) ->
return processor.prepare_next_thread();
}
}
// 根据中断类型来处理,返回的 Context 必须位于放在内核栈顶
// 根据中断类型来处理,返回的 Context 必须位于放在中断栈顶
match scause.cause() {
// 断点中断(ebreak)
Trap::Exception(Exception::Breakpoint) => breakpoint(context),
Expand Down
14 changes: 7 additions & 7 deletions os/src/interrupt/interrupt.asm
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
# 进入中断
# 保存 Context 并且进入 Rust 中的中断处理函数 interrupt::handler::handle_interrupt()
__interrupt:
# 因为线程当前的栈不一定可用,必须切换到内核栈来保存 Context 并进行中断流程
# 因此,我们使用 sscratch 寄存器保存内核栈地址
# 因为线程当前的栈不一定可用,必须切换到中断栈来保存 Context 并进行中断流程
# 因此,我们使用 sscratch 寄存器保存中断栈地址
# 思考:sscratch 的值最初是在什么地方写入的?

# 交换 sp 和 sscratch(切换到内核栈
# 交换 sp 和 sscratch(切换到中断栈
csrrw sp, sscratch, sp
# 在内核栈开辟 Context 的空间
# 在中断栈开辟 Context 的空间
addi sp, sp, -CONTEXT_SIZE * REG_SIZE

# 保存通用寄存器,除了 x0(固定为 0)
Expand Down Expand Up @@ -67,8 +67,8 @@ __interrupt:

.globl __restore
# 离开中断
# 此时内核栈顶被推入了一个 Context,而 a0 指向它
# 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录内核栈地址
# 此时中断栈顶被推入了一个 Context,而 a0 指向它
# 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录中断栈地址
# 最后跳转至恢复的 sepc 的位置
__restore:
# 从 a0 中读取 sp
Expand All @@ -79,7 +79,7 @@ __restore:
LOAD t1, 33
csrw sstatus, t0
csrw sepc, t1
# 将内核栈地址写入 sscratch
# 将中断栈地址写入 sscratch
addi t0, sp, CONTEXT_SIZE * REG_SIZE
csrw sscratch, t0

Expand Down
2 changes: 1 addition & 1 deletion os/src/process/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
/// 每个线程的运行栈大小 512 KB
pub const STACK_SIZE: usize = 0x8_0000;

/// 共用的内核栈大小 512 KB
/// 共用的中断栈大小 512 KB
pub const KERNEL_STACK_SIZE: usize = 0x8_0000;
18 changes: 9 additions & 9 deletions os/src/process/kernel_stack.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
//! 内核栈 [`KernelStack`]
//! 中断栈 [`KernelStack`]
//!
//! 用户态的线程出现中断时,因为用户栈无法保证可用性,中断处理流程必须在内核栈上进行
//! 所以我们创建一个公用的内核栈,即当发生中断时,会将 Context 写到内核栈顶
//! 用户态的线程出现中断时,因为运行栈无法保证可用性,中断处理流程必须在中断栈上进行
//! 所以我们创建一个公用的中断栈,即当发生中断时,会将 Context 写到中断栈顶
//!
//! ### 线程 [`Context`] 的存放
//! > 1. 线程初始化时,一个 `Context` 放置在内核栈顶,`sp` 指向 `Context` 的位置
//! > 1. 线程初始化时,一个 `Context` 放置在中断栈顶,`sp` 指向 `Context` 的位置
//! > (即栈顶 - `size_of::<Context>()`)
//! > 2. 切换到线程,执行 `__restore` 时,将 `Context` 的数据恢复到寄存器中后,
//! > 会将 `Context` 出栈(即 `sp += size_of::<Context>()`),
//! > 然后保存 `sp` 至 `sscratch`(此时 `sscratch` 即为内核栈顶
//! > 然后保存 `sp` 至 `sscratch`(此时 `sscratch` 即为中断栈顶
//! > 3. 发生中断时,将 `sscratch` 和 `sp` 互换,入栈一个 `Context` 并保存数据
//!
//! 容易发现,线程的 `Context` 一定保存在内核栈顶。因此,当线程需要运行时,
//! 从 [`Thread`] 中取出 `Context` 然后置于内核栈顶即可
//! 容易发现,线程的 `Context` 一定保存在中断栈顶。因此,当线程需要运行时,
//! 从 [`Thread`] 中取出 `Context` 然后置于中断栈顶即可

use super::*;
use core::mem::size_of;

/// 内核栈
/// 中断栈
#[repr(align(16))]
#[repr(C)]
pub struct KernelStack([u8; KERNEL_STACK_SIZE]);

/// 公用的内核栈
/// 公用的中断栈
pub static mut KERNEL_STACK: KernelStack = KernelStack([0; KERNEL_STACK_SIZE]);

impl KernelStack {
Expand Down
2 changes: 1 addition & 1 deletion os/src/process/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ impl Thread {
self.process.inner().memory_set.activate();
// 取出 Context
let parked_frame = self.inner().context.take().unwrap();
// 将 Context 放至内核栈顶
// 将 Context 放至中断栈顶
unsafe { KERNEL_STACK.push_context(parked_frame) }
}

Expand Down