Skip to content

Commit

Permalink
Lab 6: 硬盘驱动与文件系统 (#19)
Browse files Browse the repository at this point in the history
Co-authored-by: Zhe Tang <tangzh6101@gmail.com>
Co-authored-by: Linloir <30849181+Linloir@users.noreply.github.com>
Co-authored-by: Smallorange666 <141314494+Smallorange666@users.noreply.github.com>
Co-authored-by: Harvey Mo <108322627+Master7Sword@users.noreply.github.com>
Co-authored-by: Aether Chen <15167799+chenjunyu19@users.noreply.github.com>
  • Loading branch information
6 people authored Mar 24, 2024
1 parent 875d7b1 commit f815f81
Show file tree
Hide file tree
Showing 55 changed files with 3,073 additions and 38 deletions.
4 changes: 4 additions & 0 deletions docs/css/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ table.wikitable {
.wikitable caption {
font-weight: bold;
}

[data-md-color-scheme="default"] .transparent-img {
filter: invert(1) hue-rotate(180deg)
}
37 changes: 25 additions & 12 deletions docs/general/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@ no_comments: true

# 常见问题及解答

## 为什么选择基于 UEFI 而不是自己使用汇编编写 bootloader?
## 实验设计问题

汇编是一种非常底层的语言,对于大部分同学的学习经历来说,只在组成原理课程上接触过简单的 MIPS / RISC-V,而 x86 / ARM 基本接触很少。根据以往的经验,汇编的调试、编写、理解对于同学们都是很大的挑战。
### 相较于 CPP,本实验设计为什么选择 Rust 语言?

因此在设计层面希望能够减少汇编的使用,利用现有的运行环境框架如 UEFI,结合可以直接编译成 UEFI 可执行文件的 Rust 语言,达成这一目的
这部分内容的重点是“重复造轮子”的问题,其所指的并不是指操作系统的设计,否则有 Linux 了就没必要开发其他 OS Kernel 了,而更多指对于底层的封装和抽象、语言的支持等

同时,相对于以往面向于 x86 的方案首先需要手动处理从 16 位到 32 位的切换这一历史遗留问题,从指令集和操作系统层面,这都造成了对于过时设计的非必要的学习成本,在本就不长的实验周期中无形减少学生学到的知识份额。
1. 不需要为架构无关的数据结构“造轮子”

## 相较于 CPP,本实验设计为什么选择 Rust 语言?
CPP 的实现中,std 库与系统底层架构强耦合,在裸操作系统中无法使用,因此就连 `vector` 这类**架构无关的数据结构都需要自己实现**;在 rust 中,这些架构无关的代码被放在了 `core``alloc` 两个底层包中,因此可以直接使用诸如 `Vec``String` 这些数据结构和相关功能。

这部分内容的重点是“重复造轮子”的问题,其所指的并不是指操作系统的设计,否则有 Linux 了就没必要开发其他 OS Kernel 了,而更多指对于底层的封装和抽象、语言的支持等。
2. 不需要为数据格式化等杂项“造轮子”

1. 在 CPP 的实现中,std 库与系统底层架构强耦合,在裸操作系统中无法使用,因此就连 `vector` 这类**架构无关的数据结构都需要自己实现** rust 中,这些架构无关的代码被放在了 `core``alloc` 两个底层包中,因此可以直接使用诸如 `Vec``String` 这些数据结构和相关功能
在 CPP 的实现中,比如 `printf` 的格式化实现是不透明的,想要重写的话就需要修改其源代码,而**阅读 std 的头文件、源代码对于大部分人来说都是比较灾难性的体验**而在 rust 中,当我们为一个结构体实现了 `Write` triat (类似于接口)后,就天然的实现了 `write_fmt` 这些功能,可以**直接利用现有的语言体系进行格式化输出**

2. 在 CPP 的实现中,比如 `printf` 的格式化实现是不透明的,想要重写的话就需要修改其源代码,而**阅读 std 的头文件、源代码对于大部分人来说都是比较灾难性的体验**;而在 rust 中,当我们为一个结构体实现了 `Write` triat (类似于接口)后,就天然的实现了 `write_fmt` 这些功能,可以**直接利用现有的语言体系进行格式化输出**
3. 不需要为堆内存分配的兼容性“造轮子”

3. 在 CPP 的实现中,`vector` 底层使用的 `malloc` 函数**无法直接替换和修改**,这也是无法直接使用的核心原因;在 rust 中,内存分配的底层操作是被抽象的,只需要借助 `#[global_allocator]` 声明堆分配器,就可以为**裸操作系统带来动态内存分配的能力**,这一内存分配可以直接使用现有的实现,实际代码不到十行,带来的效果是开发基本与非裸机一致,可以使用全部诸如 `Box` 等特性,也为同学们未来可能的项目开发累积使用经验。
在 CPP 的实现中,`vector` 底层使用的 `malloc` 函数**无法直接替换和修改**,这也是无法直接使用的核心原因;在 rust 中,内存分配的底层操作是被抽象的,只需要借助 `#[global_allocator]` 声明堆分配器,就可以为**裸操作系统带来动态内存分配的能力**,这一内存分配可以直接使用现有的实现,实际代码不到十行,带来的效果是开发基本与非裸机一致,可以使用全部诸如 `Box` 等特性,也为同学们未来可能的项目开发累积使用经验。

虽然从语言方面,Rust 带来的内存安全和新颖的语言设计概念都是值得学习的,对后续无论是编译原理还是现代特性理解都有帮助。但**客观来说,Rust 学习也存在很高的门槛**,虽然在实验设计中准备了很多的缓冲和学习机会,但还是需同学们自己进行权衡。

## 本实验设计与 rCore 实验设计的差异?
### 本实验设计与 rCore 实验设计的差异?

THU 的 rCore 课程实验是一个非常优秀的操作系统课程设计,它的设计目标是让学生能够从零开始,了解操作系统的各个部分,包括内核、驱动、文件系统、网络等等。

Expand All @@ -36,6 +36,19 @@ THU 的 rCore 课程实验是一个非常优秀的操作系统课程设计,它

> 如果需要进行多架构支持,需要对页表、寄存器、控制等内容进行抽象和统一,这部分的工程化抽象开销会很大的增加学生的学习成本。
## 面对多种实验方案,我都想先进行尝试,切换实验的成本如何?

在 Lab 3(第四次实验)开始前,如果想要切换还有机会;如果实现到进程相关的内容,由于没有前期的铺垫和学习,再切换实验带来的成本就比较大了。
### 为什么选择基于 UEFI 而不是自己使用汇编编写 bootloader?

汇编是一种非常底层的语言,对于大部分同学的学习经历来说,只在组成原理课程上接触过简单的 MIPS / RISC-V,而 x86 / ARM 基本接触很少。根据以往的经验,汇编的调试、编写、理解对于同学们都是很大的挑战。

因此在设计层面希望能够减少汇编的使用,利用现有的运行环境框架如 UEFI,结合可以直接编译成 UEFI 可执行文件的 Rust 语言,达成这一目的。

同时,相对于以往面向于 x86 的方案首先需要手动处理从 16 位到 32 位的切换这一历史遗留问题,从指令集和操作系统层面,这都造成了对于过时设计的非必要的学习成本,在本就不长的实验周期中无形减少学生学到的知识份额。

## 实验实践问题

### 面对多种实验方案,该如何选择?

对于学校提供的多种实验方案,笔者秉持鼓励尝试,自行选择的态度。

但是在选择实验方案时,需要考虑到教师的要求和切换实验的成本:**在 Lab 3(第四次实验)开始前**,如果想要切换还有机会;如果实现到进程相关的内容,由于没有前期的铺垫和学习,再切换实验带来的成本就比较大了。
10 changes: 10 additions & 0 deletions docs/labs/0x00/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ targets = [ "x86_64-unknown-uefi" ]

在配置好的工作区中执行编译时,Rust 会自动下载并安装对应的工具链。

!!! note "rust-analyzer 在非 x86 平台上总是提示错误?"

可以通过 VSCode 配置项:`rust-analyzer.cargo.target` 来指定目标平台:

```json
{
"rust-analyzer.cargo.target": "x86_64-unknown-uefi"
}
```

### 运行第一个 UEFI 程序

编译一个 UEFI 程序时,我们没有操作系统所提供的标准库,也没有操作系统提供的 Interpreter,因此我们需要使用 `#![no_std]` 来声明我们的程序不依赖标准库,使用 `#![no_main]` 来声明我们的程序不依赖操作系统的入口函数。
Expand Down
34 changes: 31 additions & 3 deletions docs/labs/0x01/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ pub fn get_serial_for_sure<'a>() -> spin::MutexGuard<'a, SerialPort> {

`spin::Mutex` 是一个基于自旋锁实现的互斥锁,它的 `try_lock` 方法尝试获取互斥锁的所有权,如果获取成功,则返回一个 `MutexGuard`,这个 `MutexGuard` 将会在离开作用域时自动释放互斥锁。

部分有关于“自选锁”和“互斥锁”的实现代码如下所示:
部分有关于“自旋锁”和“互斥锁”的实现代码如下所示:

```rust
pub fn lock(&self) -> SpinMutexGuard<T> {
Expand Down Expand Up @@ -447,9 +447,37 @@ char read_serial() {

#### 串口驱动的测试

`pkg/kernel/src/utils/macros.rs` 中,你可以找到 `print!``println!` 宏面向串口输出的实现
`pkg/kernel/src/utils/macros.rs` 中,你可以找到 `print!``println!` 宏面向串口输出的实现

在调用 `drivers::serial::init()` 后,如果能够正常看到 `[+] Serial Initialized.` 的输出,说明串口驱动已经成功初始化。
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => (
$crate::utils::print_internal(format_args!($($arg)*))
);
}

#[doc(hidden)]
pub fn print_internal(args: Arguments) {
interrupts::without_interrupts(|| {
if let Some(mut serial) = get_serial() {
serial.write_fmt(args).unwrap();
}
});
}
```

因此,按照预期,在调用 `drivers::serial::init()` 后,如果能够正常看到 `[+] Serial Initialized.` 的输出,说明串口驱动已经成功初始化。

!!! question "为什么在输出时使用 `get_serial()` 获取串口?"

在内核中,为了确保串口不会同时被多方“一起访问”,需要用互斥锁来保护串口的访问。

`get_serial()` 函数尝试获取串口的互斥锁,如果获取成功,则返回一个 `MutexGuard`,这个 `MutexGuard` 将会在离开作用域时自动释放互斥锁。

然而,与 `get_serial_for_sure()` 更加强势,如果无法获取直接 panic,不留余地,这通常用于“必须获取到,否则是异常”的情况。

**对于前者的情况,如果在已经获取了串口的互斥锁之后,依然尝试进行 `print!` 输出,将会导致对应的输出内容被忽略,因为串口已经被占用。**

### 日志输出

Expand Down
24 changes: 21 additions & 3 deletions docs/labs/0x02/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,19 @@ static int init_serial() {
}
```

同时,为了能够接收到 IO 设备的对应中断,你需要为 IOAPIC 启用对应的 IRQ:

```rust
enable_irq(Irq::Serial0 as u8, 0); // enable IRQ4 for CPU0
```

!!! warning "请勿配置错误"

在实践中,有大量同学将 IRQ (Interrupt Request) 和 IRQ 对应的中断向量号 (Interrupt Vector) 混淆,导致串口中断无法正常工作。

为了承接全部(可能的)用户输入数据,并将它们统一在标准输入,需要为输入准备缓冲区,并将其封装为一个驱动,创建 `src/drivers/input.rs` 文件,使用 `crossbeam_queue` crate 实现一个无锁输入缓冲区。

!!! tip "在 memory 初始化的过程中,你已经有了内核堆分配的能力,可以动态分配内存。"
!!! tip "在 memory 初始化的过程后,你已经有了内核堆分配的能力,可以动态分配内存。"

按照下列描述,补全 `src/drivers/input.rs` 驱动代码:

Expand Down Expand Up @@ -572,7 +582,7 @@ static int init_serial() {

*Note: `String::with_capacity` 可以帮助你预先分配足够的内存。*

串口的输入中断与时钟中断类似,在 `src/interrupt/serial.rs` 中补全代码,为 IRQ4 Serial0 设置中断处理程序:
串口的输入中断与时钟中断类似,在 `src/interrupt/serial.rs` 中补全代码,为 **IRQ4 Serial0** 设置中断处理程序:

```rust
use super::consts::*;
Expand All @@ -594,7 +604,15 @@ fn receive() {
}
```

你需要补全 `receive` 函数,利用刚刚完成的 `input` 驱动,将接收到的字符放入缓冲区。
最后,你需要补全 `receive` 函数,利用刚刚完成的 `input` 驱动,将接收到的字符放入缓冲区。

!!! warning "中断导致的“并发访问”"

或许你会困惑:只有一个 CPU,为什么会导致并发访问,进而导致死锁的产生?就算暂时无法获取锁,等待一会也能获取到吧?

这里需要区分一些概念,与常见的用户态“多线程”的并发不同,中断所导致的“并发访问”是**强制性**的,并且需要**主动恢复**,循环等待的过程**并不存在抢占**。

**基于上述信息,结合思考题 3 给出的例子,尝试描述锁操作、中断的相关过程,完成该思考题。**

## 用户交互

Expand Down
4 changes: 2 additions & 2 deletions docs/labs/0x03/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ pub struct ProcessContextValue {

在 PID 的设计中,**本实验使用 `0` 表示当前没有进程在运行**,因此在 `Processor` 结构体初始化时,将其初始化为 `0`

为了支持*实际上不计划支持的*多处理器,实验设计把 `Processor` 结构体放在一个静态数组之中,数组的长度为 `MAX_CPU_NUM`
为了给扩展实验的多处理器支持预留可行性,实验设计把 `Processor` 结构体放在一个静态数组之中,数组的长度为 `MAX_CPU_COUNT`

```rust
const MAX_CPU_COUNT: usize = 8;
Expand Down Expand Up @@ -546,7 +546,7 @@ pub fn new_test_thread(id: &str) -> ProcessId {
1. 它们是否按照预期的顺序保存和恢复执行?
2. 有没有进程插队、执行状态不正确、执行时间不平均的情况?
3. 它们使用的栈是否符合预期?
4. 有没有进程存在不再调度几次后继续执行的情况
4. 是否有进程存在声明退出后继续执行的情况
5. 就绪队列中是否存在重复的进程?

## 缺页异常的处理
Expand Down
4 changes: 2 additions & 2 deletions docs/labs/0x04/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@

通常而言,用户程序并不直接自行处理系统调用,而是由用户态库提供的函数进行调用。

在编写 C 语言时 `printf``scanf` 等函数并不是直接调用系统调用,以 gcc on Linux 的一般行为为例,这些函数被定义在 `glibc` 中,而 `glibc` 会处理系统调用。相对应的,在 Windows 上,也会存在 `msvcrt` (Microsoft Visual C Run-time) 等库。
在编写 C 语言时 `printf``scanf` 等函数并不是直接调用系统调用。以一般的 GNU/Linux 程序为例,这些函数被定义在 `glibc` (GNU C Library) 中,而 `glibc` 会处理系统调用。相对应的,在 Windows 上,也会存在 `msvcrt` (Microsoft Visual C Run-time) 和 `ucrt` (Universal C Runtime) 等库。

为了让用户态程序更好地与 YSOS 进行交互,处理程序的生命周期,便于编写用户程序等,需要提供用户态库,以便用户程序调用。

Expand Down Expand Up @@ -950,7 +950,7 @@ The factorial of 999999 under modulo 1000000007 is 128233642.

2. 😋 尝试在 `kernel/src/memory/frames.rs` 中实现帧分配器的回收功能 `FrameDeallocator`,作为一个最小化的实现,你可以在 `Allocator` 使用一个 `Vec` 存储被释放的页面,并在分配时从中取出。

3. 🤔 基于帧回收器的实现,在 `elf` 中实现 `unmap_range` 函数,从页表中取消映射一段连续的页面,并使用帧回收器进行回收。利用它实现进程栈的回收(利用 `ProcessData` 中存储的页面信息),页表的回收将会在后续实现用实现,暂时不需要处理
3. 🤔 基于帧回收器的实现,在 `elf` 中实现 `unmap_range` 函数,从页表中取消映射一段连续的页面,并使用帧回收器进行回收。之后,在合适的地方,结合 `ProcessData` 中存储的页面信息,利用这个函数实现进程栈的回收。其他进程资源(如页表、代码段、数据段等)的回收将会在后续实验中实现,目前暂时不需要考虑

4. 🤔 尝试利用 `UefiRuntime``chrono` crate,获取当前时间,并将其暴露给用户态,以实现 `sleep` 函数。

Expand Down
4 changes: 2 additions & 2 deletions docs/labs/0x05/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ lock inc qword [obj.main::COUNTER::h2889e4585a2a2d30]

正因如此,在进行内核编写的过程中遇到的 `Mutex``RwLock` 等用于保障内核态数据一致性的锁机制**均是基于自旋锁实现的**_你可能在之前的实验中遇到过系统因为自旋忙等待导致的异常情况_

#### 自旋锁
### 自旋锁

自旋锁 `SpinLock` 是一种简单的锁机制,它通过不断地检查锁的状态来实现线程的阻塞,直到获取到锁为止。

Expand Down Expand Up @@ -482,7 +482,7 @@ unsafe impl Sync for SpinLock {}

在进行循环等待时,可以使用 `core::hint::spin_loop` 提高性能,在 x86_64 架构中,它实际上会编译为 `pause` 指令。

#### 信号量
### 信号量

得利于 Rust 良好的底层封装,自旋锁的实现非常简单。但是也存在一定的问题:

Expand Down
5 changes: 4 additions & 1 deletion docs/labs/0x06/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 实验六:硬盘驱动与文件系统

!!! tip "<br/>&nbsp;<span style="font-weight: bold; float: right">by</span>"
!!! tip "作为一座巨大的图书馆的守护者,你需要确保每一本书都被正确地归类、保护和维护,以便访客在任何时候都能找到他们需要的知识。<br/>&nbsp;<span style="font-weight: bold; float: right">by ChatGLM</span>"

## 实验目的

Expand All @@ -23,6 +23,9 @@

对于本次实验内容,你需要参考学习如下实验资料:

- [文件系统概述](../../wiki/fs.md)
- [ATA 硬盘简介](../../wiki/ata.md)
- [FAT 文件系统](../../wiki/fat.md)

## 实验任务与要求

Expand Down
Loading

0 comments on commit f815f81

Please sign in to comment.