-
Notifications
You must be signed in to change notification settings - Fork 439
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #206 from MiaoHao-oops/master
add MiaoHao-oops' stage3 summary
- Loading branch information
Showing
1 changed file
with
120 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
--- | ||
title: 2023开源操作系统训练营第三阶段总结报告-郝淼 | ||
date: 2023-11-29 14:30:43 | ||
categories: | ||
- oscamp 2023fall arceos unikernel | ||
tags: | ||
- author:MiaoHao-oops | ||
- 2023秋冬季开源操作系统训练营 | ||
- 第三阶段总结报告 | ||
--- | ||
|
||
## 第一周练习总结 | ||
|
||
第一周的 5 个练习的主要目的是了解 ArceOS 的基本代码组织结构,为后面的任务打下基础。 | ||
|
||
### 练习 1、2 | ||
|
||
**练习 1** 要求实现彩色打印 `println!`,这个任务我采用了修改 `axstd::println!` 来实现,这是出于架构兼容性的考虑,不应修改太过底层的代码。具体实现是利用了 ANSI 转义序列。 | ||
|
||
**练习 2** 要求支持 `HashMap` 并通过测试。之所以 Rust 核心库中没有 `HashMap`,是因为 `HashMap` 需要调用产生随机数的接口,而这个接口是架构相关的。因此,练习 2 的核心是实现 `axstd` 可调用的 `fn random() -> u128` 接口。 | ||
|
||
自底向上实现 `random` 的过程: | ||
- `axhal::random::random()`:在 `axhal` 层实现架构相关的 `random()` 接口,它调用了 `axhal::time::current_ticks()` | ||
- `arceos_api::random::ax_random()`:定义 `ax_random` 接口,通过 `pub use axhal::random::random as ax_random;` 将其直接实现为 `axhal::random::random()` | ||
- 在 `axstd` 中可调用 `arceos_api::random::ax_random()` | ||
|
||
### 练习 3、4 | ||
|
||
**练习 3** 要求实现内存分配器 `early`。ArceOS 中包含两个内存分配器 `palloc` 和 `balloc`,`palloc` 用于以页单位的内存分配,`balloc` 被指定为 Rust 的 `#[global_allocator]`,这样我们就可以使用 Rust 中如 `Vec`,`String` 等这些堆上可变长的数据类型。这两个内存分配器都包含在一个全局内存分配器 `GLOBAL_ALLOCATOR` 中。 | ||
|
||
`early` 分配器既是 `palloc` 又是 `balloc`,因此这里遇到一个问题:`GLOBAL_ALLOCATOR` 中使用了一对智能指针分别指向了 `palloc` 和 `balloc` 实例,但由于 `early` 分配器的特点,`early` 在全局只有一个实例,并且其中的 `palloc` 和 `balloc` 还需要共享数据。 | ||
|
||
因此,我将 `GLOBAL_ALLOCATOR` 可能的类型分为两种: | ||
|
||
- `SeparateAllocator`:分离式内存分配器,`palloc` 和 `balloc` 是两个,当 `balloc` 可用内存不足时,向 `palloc` 请求内存 | ||
- `AIOAllocator`:一体式内存分配器,`palloc` 和 `balloc` 是同一个,两个内存分配器中共享剩余可用内存 | ||
|
||
此外,我定义了 `trait GlobalAllocator`,与原来的 `GlobalAllocator` 类中实现的方法保持一致。而后,为 `SeparateAllocator` 和 `AIOAllocator` 分别实现 `GlobalAllocator`,然后在 `toml` 中增加关于 `early` 的选项,即可达成练习目标。 | ||
|
||
**练习 4** 要求通过 `rust_main()` 的参数 `dtb` 解析 dtb。这一练习的关键是找到一个合适的 crate,我使用的是 `hermit-dtb = "0.1.1"`。 | ||
|
||
### 练习 5 | ||
|
||
练习 5 要求将 FIFO 算法改变为抢占式的。FIFO 算法原本维护了一个队列,靠进程主动放弃 CPU 触发调度。在抢占式中,需要将原本的 `List` 修改为双端队列 `VecDequeue`,这样在 `remove_task(&mut self, task: &Self::SchedItem)` 中,修改为先找到 `task` 的下标,然后根据下标将其从队列中删除;在 `put_prev_task(&mut self, prev: Self::SchedItem, preempt: bool)` 如果此时是可以抢占的,就将 `prev` 放入队尾,否则将其放在队头,相当于调度器选择的还是当前的任务;最后,将 `task_tick` 的返回值恒置为 `true` 这样每次时钟中断发生时都能进行调度。 | ||
|
||
## 第二周练习总结 | ||
|
||
第二周在一个个小练习后逐渐实现了一个可以加载二进制文件的 loader,并且加载的应用有独立的地址空间。 | ||
|
||
### 练习 1、2 | ||
|
||
实际上练习1、2的任务是实现一个简易的文件系统。我的设计如下: | ||
|
||
```Rust | ||
struct ImgHeader { | ||
app_num: usize, | ||
} | ||
|
||
struct AppHeader { | ||
app_size: usize, | ||
} | ||
``` | ||
|
||
将 1 个 `ImgHeader` 放在 pflash 头部,而后紧接着`app_num` 个 `AppHeader` 用于指示每个应用的大小。 | ||
|
||
生成二进制镜像,添加这写头部信息我是用 C 语言实现的,在此不再赘述。 | ||
|
||
### 练习 3 | ||
|
||
批处理的方式下,先加载第一个应用,然后运行;而后加载第二个应用,然后运行。这两个应用均被加载到一个固定的地址。 | ||
|
||
在 loader 中,使用 `jalr` 指令通过函数调用执行一个应用,因此,在进入应用程序的主函数时,`ra` 寄存器已经被加载为返回地址,想要从应用程序返回,只需要将函数签名返回值部分的 `-> !` 改为 `-> ()` 或者直接删除即可。这样,应用程序生成的汇编代码最后是一条 `ret` 指令,恰好能返回到 loader。 | ||
|
||
### 练习 4、5 | ||
|
||
练习 4 将 `sys_terminate` 封装为对 `axstd::process::exit` 的调用即可。 | ||
|
||
练习 5 我将所有的 abi 调用和为一个分发函数: | ||
|
||
```rust | ||
fn abi_entry(abi_num: usize, arg0: usize) { | ||
match abi_num { | ||
SYS_HELLO => abi_hello(), | ||
SYS_PUTCHAR => abi_putchar(arg0 as u8 as char), | ||
SYS_TERMINATE => abi_terminate(arg0 as i32), | ||
_ => panic!("unsupport abi call!"), | ||
} | ||
} | ||
``` | ||
|
||
然后将这个函数的地址作为第一个参数传入应用,即通过 `a0` 传参: | ||
|
||
```rust | ||
unsafe extern "C" fn _start(abi_entry: usize) -> ! | ||
``` | ||
|
||
在应用中,封装 `putchar` 等函数: | ||
|
||
```rust | ||
fn putchar(c: u8) { | ||
unsafe { | ||
core::arch::asm!(" | ||
li a0, {abi_num} | ||
la t0, {abi_entry_addr} | ||
ld t0, (t0) | ||
jalr t0", | ||
abi_num = const SYS_PUTCHAR, | ||
abi_entry_addr = sym ABI_ENTRY, | ||
in("a1") c, | ||
clobber_abi("C"), | ||
) | ||
} | ||
} | ||
``` | ||
|
||
实际上,所有 abi 函数都是跳转到 `abi_entry` 执行,不同之处在与参数的传递,我规定第一个参数 `a0` 是 abi 号,后面的是 abi 函数的参数。在 `putchar` 中,`a0` 被设置为 `SYS_PUTCHAR`,`a1` 被设置为 `c` 的值。注意,在内联汇编的结尾要加上 `clobber_abi("C")`,保持寄存器在执行这段汇编代码前后的一致,否则将出现混乱。 | ||
|
||
### 练习 6 | ||
|
||
目前,在批处理且每个应用地址空间相同的情况下,练习 6 实现起来比较简单,保持每个应用复用全局的应用页表 `APP_PT_SV39` 即可。 |