第一周练习总结
第一周的 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的任务是实现一个简易的文件系统。我的设计如下:
1 | struct ImgHeader { |
将 1 个 ImgHeader 放在 pflash 头部,而后紧接着app_num 个 AppHeader 用于指示每个应用的大小。
生成二进制镜像,添加这写头部信息我是用 C 语言实现的,在此不再赘述。
练习 3
批处理的方式下,先加载第一个应用,然后运行;而后加载第二个应用,然后运行。这两个应用均被加载到一个固定的地址。
在 loader 中,使用 jalr 指令通过函数调用执行一个应用,因此,在进入应用程序的主函数时,ra 寄存器已经被加载为返回地址,想要从应用程序返回,只需要将函数签名返回值部分的 -> ! 改为 -> () 或者直接删除即可。这样,应用程序生成的汇编代码最后是一条 ret 指令,恰好能返回到 loader。
练习 4、5
练习 4 将 sys_terminate 封装为对 axstd::process::exit 的调用即可。
练习 5 我将所有的 abi 调用和为一个分发函数:
1 | fn abi_entry(abi_num: usize, arg0: usize) { |
然后将这个函数的地址作为第一个参数传入应用,即通过 a0 传参:
1 | unsafe extern "C" fn _start(abi_entry: usize) -> ! |
在应用中,封装 putchar 等函数:
1 | fn putchar(c: u8) { |
实际上,所有 abi 函数都是跳转到 abi_entry 执行,不同之处在与参数的传递,我规定第一个参数 a0 是 abi 号,后面的是 abi 函数的参数。在 putchar 中,a0 被设置为 SYS_PUTCHAR,a1 被设置为 c 的值。注意,在内联汇编的结尾要加上 clobber_abi("C"),保持寄存器在执行这段汇编代码前后的一致,否则将出现混乱。
练习 6
目前,在批处理且每个应用地址空间相同的情况下,练习 6 实现起来比较简单,保持每个应用复用全局的应用页表 APP_PT_SV39 即可。