总结报告
很高兴参加第三阶段ArceOS Unikernel的课程学习与训练,
经过第三阶段的学习,让我对rust、unikernel、linux等都有了更深入的理解。经过这些实验与练习,提升了我对rust语言的理解,感受到rust的条件编译的强大。学习了ArceOS的层次结构、模块化设计理念,让我对Unikernel的概念有了更具象化的理解。十分感谢石磊老师的细心讲解、助教老师孙应娥的热心帮忙以及群里的同学们的讨论指导。
下面是逐个练习的总结与反思。
第一周练习
练习 1 &2
练习1
支持彩色打印println!。以apps/helloworld为测试应用。
要求:不能在helloworld程序本身改,要在下面的库或更深层次的组件修改练习2(附加题)
支持HashMap数据类型。以apps/memtest为测试应用。
要求:在ulib/axstd中支持HashMap类型预期输出:执行 make A=apps/memtest ARCH=riscv64 run
思路:练习1就是在println!宏的实现上添加带颜色的格式控制就可以了。
练习2根据提示是参考rust官方的hash表实现,但是官方实现内容太多了,错综复杂的依赖不好解决。根据老师在群里的提示,只需要实现测试用到的函数(new、iter、insert等)就好了,然后就顺利解决了。
我在参考官方库的时候,发现官方库实际是在 用hashbrown::hash_map,进行二次封装的。所以这个练习有一个偷懒的做法
1 | use hashbrown::hash_map as HashMap; |
练习3
练习3
为内存分配器实现新的内存算法early,禁用其它算法。early分配算法以apps/memtest为测试应用。
这题是我卡的最久的一个练习,我一开始以为需要涉及底层内存的分配(实际上是分配好底层的内存了),实际上为了简化练习,底层内存已经分配固定了,只是需要给应用分配地址。
因此我一开始走了很多弯路,我的原本的想法是将byteAllocator和pageAllocator进行结合,或者是使用byteAllocator和BitAlloc结合。导致这样的想法是为以为BitAlloc是会控制底层的内存分配(不得不说实在太蠢了)。
尝试了各种方法后,请教了王格格同学,知道了正确的做法:维护byte_pos和page_pos来进行内存分配就好了。
实际的细节中要注意内存地址的对齐,为在练习5中发现了在练习3中的问题。即内存地址的大小和地址都要进行对齐。
练习4
解析dtb(FDT的二进制格式),打印物理内存范围和所有的virtio_mmio范围。以apps/memtest为测试应
用。
当ArceOS启动时,上一级SBI向我们传递了dtb的指针,一直会传递到axruntime,我们就在
axruntime中执行解析并打印。
思路:需要查阅相关资料,了解DTB,了解FDT相关的内容。感谢群里的郝淼同学分享的hermit-dtb库,解析dtb很方便。在解析的过程中需要注意的点是内存地址和大小的字段在reg字段中,需要将reg一分为二。
练习5
练习5
把协作式调度算法fifo改造为抢占式调度算法。让测试应用通过
难点:主要是要理解arcos中的调度模块的代码,启用preemt的feature。
第二周练习
练习1&2
练习 1:
main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。执行通过。练习 2:
在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。
第二个应用包含唯一的汇编代码是 ebreak 。
如实验 1 的方式,打印出每一个应用的二进制代码
练习1和练习2是关系紧密的,都是要给image添加文件头,可以直接做练习2。
我们需要知道应用的数量,以及每个应用的字节长度,所以很自然地可以想到,首先文件头第一个字段应该是 应用数量,然后接着是每个应用的应用大小,最后是每个应用的应用二进制内容。
因此我设计的文件格式如下
| field| length|
| ———– | ———– |
| app_amount| 2 bytes|
| app0_size | 2 bytes|
| app1_size | 2 bytes|
| …… | 2 bytes|
| appn_size | 2 bytes|
| app0_bin | app0_size bytes|
| app1_bin | app1_size bytes|
| ……. | …….|
| appn_bin | appn_size bytes|
应用数量和应用大小字段长度都是2个字节,因此我用rust写了个小程序将文件头写入文件。
1 | //参数是 需要合并的app文件名称 |
相应的在arcos中的loader程序就要先读取前两个字节,获取app数量,然后再依次读取app长度和app的内容
要注意的细节:要维持文件大小在32M,否则qemu无法加载,文件大小在Makefile文件中写死了。
练习3
练习 3:
批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi
思路:如何把控制权重新回到arceos中,重新返回到loader中执行
练习4
练习 4:
本实验已经实现了1 号调用 - SYS_HELLO,2 号调用 - SYS_PUTCHAR,请实现 3 号调用 -
SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。
思路:熟悉arceOS的代码,寻找相关的模块代码。我的想法是关机的调用实际上是应该控制硬件来进行操作的,可能跟sbi相关的,应该在硬件抽象层,所以为主要是看了axhal模块的代码。然后发现了相关的调用代码
1 | /// Shutdown the whole system, including all CPUs. |
所以只需要在loader中调用这个terminate函数就可以实现关机操作。
练习5
练习5
照如下要求改造应用 hello_app:
- 把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为
fn putchar(c: char) 。- 基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str) ,可以支持输出字符串。
- 应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。
思路:将三部分调用的内联汇编代码都封装在函数中,在入口函数中顺序调用即可。
看样子不难的程序我又卡了很久很久,跟编译器斗智斗勇了很久。
1 | unsafe extern "C" fn _start(_entry:usize){ |
理想的执行过程是依次调用hello、puts、terminate函数后退出。但是无论怎么尝试,程序只会执行第一个调用的函数后退出。我就很疑惑:能够执行第一个调用的函数,那说明我的函数封装应该没有问题,可是为什么无论调用多少次,只会执行一次。而且调用多少次都不会改变编译生成的文件大小。然后我又生成了汇编代码进行查看。
1 | _start: |
然后发现start内确实只有我第一个调用函数的汇编代码。难道是为我的代码被编译器给优化了?
然后尝试了各种方法,改成debug模式,修改优化等级,修改使用的寄存器等。比如在Cargo.toml中修改优化等级为0。
1 | [profile.dev] |
均没有任何效果。
然后为又重新审视了每个汇编代码块。
1 | unsafe fn hello() { |
然后发现了问题所在:options(noreturn)是告诉编译器这段汇编代码没有返回,也就是说后面的代码永远也不会执行,因此编译器会把后面调用的函数代码全都丢掉。因此只会执行第一次函数调用。注释掉即可解决问题。
练习6
练习 6:
- 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
- 现在有两个应用,让它们分别有自己的地址空间。
- 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环
之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。
思路:需要为每个应用分配独立的地址空间,并在跳转到app之前切换到app的地址空间,返回时再重新切换回内核空间。
因此在切换到app的地址空间之前,需要先保存当前的satp寄存器的值,执行完app后重新加载。