总结报告 很高兴参加第三阶段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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //参数是 需要合并的app文件名称 let mut args =env::args(); args.next(); let files_number:u16=args.len() as u16; let mut output_file: File =File::create("new_apps.bin").expect("open fails"); output_file.write(&files_number.to_be_bytes()).expect("write file numbers fails"); for file in args{ let length:u16= fs::metadata(file.clone()).expect("open file fails").len() as u16; println!("APP:{}:{}",file,length); output_file.write(&length.to_be_bytes()).expect("writing file length fails"); } args=env::args(); args.next(); for file in args{ let mut source_file=File::open(file).expect("Opening file fails"); let mut buffer=Vec::new(); source_file.read_to_end(&mut buffer).expect("reading file fails."); output_file.write_all(&buffer).expect("writing file fails."); }
相应的在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 2 3 4 5 6 7 8 9 /// Shutdown the whole system, including all CPUs. pub fn terminate() -> ! { info!("Shutting down..."); sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason); warn!("It should shutdown!"); loop { crate::arch::halt(); } }
所以只需要在loader中调用这个terminate函数就可以实现关机操作。
练习5
练习5
照如下要求改造应用 hello_app:
把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为 fn putchar(c: char) 。
基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str) ,可以支持输出字符串。
应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。
思路:将三部分调用的内联汇编代码都封装在函数中,在入口函数中顺序调用即可。
看样子不难的程序我又卡了很久很久,跟编译器斗智斗勇了很久。
1 2 3 4 5 6 unsafe extern "C" fn _start(_entry:usize){ ENTRY=_entry; hello(); puts("ArceOS exercise 5."); terminate(); }
理想的执行过程是依次调用hello、puts、terminate函数后退出。但是无论怎么尝试,程序只会执行第一个调用的函数后退出。我就很疑惑:能够执行第一个调用的函数,那说明我的函数封装应该没有问题,可是为什么无论调用多少次,只会执行一次。而且调用多少次都不会改变编译生成的文件大小。然后我又生成了汇编代码进行查看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 _start: addi sp, sp, -32 sd ra, 24(sp) sd s0, 16(sp) sd s1, 8(sp) sd s2, 0(sp) addi s0, sp, 32 mv s1, a0 .Lpcrel_hi0: auipc s2, %pcrel_hi(_ZN9hello_app5ENTRY17hc4afe4ceb535dcc3E.0) sd a0, %pcrel_lo(.Lpcrel_hi0)(s2) mv t2, a0 #APP li t0, 1 mv a7, t2 slli t0, t0, 3 add t1, a7, t0 ld t1, 0(t1) jalr t1 #NO_APP ld a0, %pcrel_lo(.Lpcrel_hi0)(s2) bne a0, s1, .LBB0_2 ld ra, 24(sp) ld s0, 16(sp) ld s1, 8(sp) ld s2, 0(sp) addi sp, sp, 32 ret
然后发现start内确实只有我第一个调用函数的汇编代码。难道是为我的代码被编译器给优化了? 然后尝试了各种方法,改成debug模式,修改优化等级,修改使用的寄存器等。比如在Cargo.toml中修改优化等级为0。
1 2 [profile.dev] opt-level = 0
均没有任何效果。 然后为又重新审视了每个汇编代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 unsafe fn hello() { core::arch::asm!(" li t0, {abi_num} mv a7,t2 slli t0, t0, 3 add t1, a7, t0 ld t1, (t1) jalr t1 ", abi_num = const SYS_HELLO, in("t2") ENTRY, clobber_abi("C"), // 告诉编译器 clobber 了通用寄存器 options(noreturn) ) }
然后发现了问题所在:options(noreturn)是告诉编译器这段汇编代码没有返回,也就是说后面的代码永远也不会执行,因此编译器会把后面调用的函数代码全都丢掉。因此只会执行第一次函数调用。注释掉即可解决问题。
练习6
练习 6:
仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
现在有两个应用,让它们分别有自己的地址空间。
让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环 之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。
思路:需要为每个应用分配独立的地址空间,并在跳转到app之前切换到app的地址空间,返回时再重新切换回内核空间。
因此在切换到app的地址空间之前,需要先保存当前的satp寄存器的值,执行完app后重新加载。