在室友的推荐中了解到了开源操作系统训练营,其中一个部分就是rCore,之前在了解rust的时候就知道这是一个基于rust编写的os kernel,于是趁此机会认真学习了一番。总的来说,了解到了rust在os kernel编写中的许多优雅的写法,同时也在我已经学习过NEMU和本院基于xv6的osdi课程的基础上学习到了更多更细致的os的设计和权衡。如果只追求通过评测,需要写的代码量很少,时间大多花在阅读和理解代码上。比较难受的点在于每一章的自己实现的内容需要在后面的章节中维护,而不同章节是没有公共祖先的branch,合并容易冲突,我自己使用的是git cherry-pick。
启动!!!
- 在网上搜索rCore能搜到很多来源的信息,清华大学本身有一个完全体https://github.com/rcore-os/rCore,也有一个教学版https://github.com/rcore-os/rCore-Tutorial-v3,learningos社区的版本是基于原教学版的,与原教学版区别不大(人肉git diff之后发现基本没有重大差异,最多的是加了
#![deny(missing_docs)]导致写了很多注释,,,)。我使用的是learingos的仓库(进入opencamp网站上给出的github classroom链接,接受邀请后会生成一个类似https://github.com/LearningOS/2026s-rcore-你的用户名的仓库),里面github action的流水线会在push之后进行评分,需要在opencamp网站的个人设置里面设置对应的github用户名,才会将成绩同步到排行榜。 - 本人使用的qemu版本是11,跟仓库里面附带的rustsbi不兼容(无法跳转到kernel的入口),换成opensbi(即不写明-bios时qemu默认采用的版本)仍然不行,经过评论区高人指点发现需要自己找到opensbi的jump版本(jump的行为就是最后跳转到写死的入口地址,区别于默认的dynamic版本会跳转到前一级引导程序指定的地址),即
fw_jump.bin。使用opensbi需要注意,在使用sbi提供的接口进行串口读时,如果没有读到字符返回的是usize::MAX而不是0。另外经过测试,rCore-Tutorial-v3附带的rustsbi可以在qemu11上正常运行。 - 本人使用的是archlinux,riscv64的gcc等工具链可以使用AUR方便安装,不需要特别安装gdb,arch的gdb包已经支持多种架构,可以使用参数指定。
- learningos的文档是简略版的,推荐阅读原版的文档https://rcore-os.cn/rCore-Tutorial-Book-v3/,里面内容更丰富,并且有好心人的评论。
kernel
- 用户态协程的设计https://github.com/chyyuu/example-coroutine-and-thread,原代码中
nx1可能会造成理解上的困难,可以将每个协程的入口改为跳板,由跳板跳往真正的入口,自然返回后跳向t_return。理解协程很重要的一点是在非叶子函数(在函数内部会调用其他函数的函数)的序言中会保存返回地址ra以免被覆盖,然后在尾声中复原。
1 | fn trampoline() { |
- RAII:大量使用RAII,在rust中体现为给结构体实现
Droptrait,例如BlockCache在销毁时自动sync,FrameTracker在销毁时自动回收ppn用于未来的分配,PidHandle在销毁时自动回收pid lazy_static!:rust中用static定义全局变量,但是要求在编译时赋予一个常量值,那么对于一些需要比较复杂的初始化逻辑的全局变量就可以使用这个宏,保证在第一次使用这个静态变量时,它内部的初始化代码才会执行,并且多线程安全,并且封装了初始化的逻辑,不必在每一次使用时判断是否初始化过。- 很优秀的设计是把文件系统单独做成了一个可以在本机运行的
crate,这样就方便进行调试,虽然我没用到哈哈。 - kernel的链接脚本清晰明了,在
config.toml里面指明。 - 执行流:用户程序trap进内核是关中断的,除非内核主动shedule否则不会被打断当前的执行流。schedule会切换到其他用户程序的内核上下文,在被切回来的时候就仿佛睡了一觉(执行了一个很久才返回的普通函数),然后正常trap返回。我们有单独的shedule函数,区别于使用同一个trap函数,shedule函数专门用于切换内核态的上下文,好处是可以保存更少的寄存器,性能更好。
- 地址空间:内核的地址空间独立于用户程序的地址空间,即KPTI,用于防范meltdown漏洞。
- 跳板页面:在执行完切换页表的指令后,cpu继续执行
sfence.vma(这条指令在执行切换页表之前就被取指单元给预取了),执行的效果是清空TLB并且flush掉流水线,强制cpu重新按照pc + 4去取指令,因为我们直接让跳板页面在内核和用户程序的地址空间有相同的虚拟地址,所以取到的指令依然是我们想要的。按理说我们只需要让sfence.vma指令后面的指令能顺利执行就ok。 bitflags!:直观操作位掩码。Filetrait:在c语言中实现不同文件类型的读写需要函数指针,而rust通过dyn的虚表机制可以方便的实现。Weak智能指针用于感知管道的写口是否已经关闭(被销毁)。From和Intotrait:简洁清晰的类型转换。slice:切片是胖指针,包含元素类型,首元素地址和总元素数量信息。IntoIterator和Iteratortrait:实现了迭代器的trait就可以使用for循环了。- 指定一个全局分配器之后就可以爽用各种需要堆空间的数据结构了。
- 线程相关的代码我感觉写的比较灾难。。。
core::fmt::Writetrait:教会程序怎么打印&str,就可以自动获得rust提供的格式化系统,进而实现打印输出。这就是将格式化和字符输出解耦的好处。- 有几篇文档我觉得很有必要精读。
- 地址空间:“我们为何将应用的 Trap 上下文放到应用地址空间的次高页面而不是内核地址空间中的内核栈中呢?原因在于,在保存 Trap 上下文到内核栈中之前,我们必须完成两项工作:1)必须先切换到内核地址空间,这就需要将内核地址空间的 token 写入 satp 寄存器;2)之后还需要保存应用的内核栈栈顶的位置,这样才能以它为基址保存 Trap 上下文。这两步需要用寄存器作为临时周转,然而我们无法在不破坏任何一个通用寄存器的情况下做到这一点。因为事实上我们需要用到内核的两条信息:内核地址空间的 token ,以及应用的内核栈栈顶的位置,RISC-V却只提供一个 sscratch 寄存器可用来进行周转。所以,我们不得不将 Trap 上下文保存在应用地址空间的一个虚拟页面中,而不是切换到内核地址空间去保存。”
- 其他的嘛不想写了。。。
用户程序和库函数
#[linkage = "weak"]:弱符号,如果有强符号的话优先链接强符号,没有强符号则链接弱符号作为保底。opt-level = "z":关掉循环展开等占空间的优化,尽可能复用代码,尽可能减小文件体积。lto = true:链接时优化,可以跨crate进行死代码消除和内联等优化。