第一周练习总结
第一周的 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
即可。