0%

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 中包含两个内存分配器 pallocballocpalloc 用于以页单位的内存分配,balloc 被指定为 Rust 的 #[global_allocator],这样我们就可以使用 Rust 中如 VecString 等这些堆上可变长的数据类型。这两个内存分配器都包含在一个全局内存分配器 GLOBAL_ALLOCATOR 中。

early 分配器既是 palloc 又是 balloc,因此这里遇到一个问题:GLOBAL_ALLOCATOR 中使用了一对智能指针分别指向了 pallocballoc 实例,但由于 early 分配器的特点,early 在全局只有一个实例,并且其中的 pallocballoc 还需要共享数据。

因此,我将 GLOBAL_ALLOCATOR 可能的类型分为两种:

  • SeparateAllocator:分离式内存分配器,pallocballoc 是两个,当 balloc 可用内存不足时,向 palloc 请求内存
  • AIOAllocator:一体式内存分配器,pallocballoc 是同一个,两个内存分配器中共享剩余可用内存

此外,我定义了 trait GlobalAllocator,与原来的 GlobalAllocator 类中实现的方法保持一致。而后,为 SeparateAllocatorAIOAllocator 分别实现 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的任务是实现一个简易的文件系统。我的设计如下:

1
2
3
4
5
6
7
struct ImgHeader {
app_num: usize,
}

struct AppHeader {
app_size: usize,
}

将 1 个 ImgHeader 放在 pflash 头部,而后紧接着app_numAppHeader 用于指示每个应用的大小。

生成二进制镜像,添加这写头部信息我是用 C 语言实现的,在此不再赘述。

练习 3

批处理的方式下,先加载第一个应用,然后运行;而后加载第二个应用,然后运行。这两个应用均被加载到一个固定的地址。

在 loader 中,使用 jalr 指令通过函数调用执行一个应用,因此,在进入应用程序的主函数时,ra 寄存器已经被加载为返回地址,想要从应用程序返回,只需要将函数签名返回值部分的 -> ! 改为 -> () 或者直接删除即可。这样,应用程序生成的汇编代码最后是一条 ret 指令,恰好能返回到 loader。

练习 4、5

练习 4 将 sys_terminate 封装为对 axstd::process::exit 的调用即可。

练习 5 我将所有的 abi 调用和为一个分发函数:

1
2
3
4
5
6
7
8
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 传参:

1
unsafe extern "C" fn _start(abi_entry: usize) -> !

在应用中,封装 putchar 等函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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_PUTCHARa1 被设置为 c 的值。注意,在内联汇编的结尾要加上 clobber_abi("C"),保持寄存器在执行这段汇编代码前后的一致,否则将出现混乱。

练习 6

目前,在批处理且每个应用地址空间相同的情况下,练习 6 实现起来比较简单,保持每个应用复用全局的应用页表 APP_PT_SV39 即可。