0%

第三阶段总结报告

完成的独立工作

前期学习内容

  • 阅读《200行讲透Rust Future》(原文)
  • 阅读《Embassy Book》(原文)
  • 阅读《通过迭代 WebServer 逐步深入 Rust 异步编程》(原文)

项目实践内容

  • 首先由代码demo在debug单步执行中了解整个embassy的基础执行流程,关于executor和future部分有了自己的理解,于是产生了一个改进的思路,尤其是对于原来embassy的interrupt executor实现机制的改进思路
  • 由之前十分熟悉的UCOSII实时操作系统出发,进行功能裁剪和修改,适配到embassy的底层做一个线程运行时,让我们能够同时使用多个embassy thread executor,并且支持他们之间的优先级抢占,解决了原来interrupt executor方式支持的优先级抢占的两个问题:
    1. 优先级会占用中断,使得可用的外设中断减少,并且不灵活
      项目的实现做到了不使用额外中断,只需要pendsv和systick中断来维护线程的切换以及时间片优先级的调度
    2. 高优先级任务如果没有await的操作,也就是没有内部future会poll返回pending的话,其它低优先级任务不会得到执行,它会独占MCU/CPU,除非它被设计为timer wait一段时间来定时交出控制权
      这里我们的项目实现是通过stride算法(略有修改),实现优先级抢占的线程调度算法,一个executor会独自占用一个线程,从而赋予了其优先级以及实现了executor之间的抢占,而且通过stride算法的调度策略,避免了高优先级executor独占MCU/CPU
  • 调试工作的实施:学习到了rcore里面的makefile的思路,gdb配合openocd调试,通过读反汇编代码以及查看内存情况,解决了代码过程中的一些比如栈溢出,莫名进入hardfault,时钟频率不对等的bug

一些体会与收获

感觉前期的学习思考是最关键的时期,两周的时间都是花在看文章,跑跑demo程序,然后理解,思考,没有前面对于rust异步机制的理解,后面阅读embassy的代码肯定是无从下手的。中期的embassy部分,我觉得真的很需要一个真实的板子来跑它的示例程序,这样一步步调试着学习,能很快的明白整个流程,如果只是单纯看着代码做的话,很难把握整体的流程,而且调试过程中可以对embassy里面代码做修改,来查看你猜想的结果,这样对于细节处理的地方也能搞清楚,因为对于我这种情况(从训练营才开始接触rust语言)来说,单纯只读代码不运行,不调试,挑战性会很大,这是我自己学习embassy过程中的一点体会吧。

还有对于后期项目开发的方式,我也有一些收获。首先是合作的开发方式选择,最开始和我的队友的确打算分branch分模块开发,但是,对于这部分硬件相关部分(并不是开发一个后端或者前端项目),而且还是在项目起草初期,我们很难把模块分得很细,而且中途更改会很多很频繁,所以我们选择现在很成熟的实时协作方式,在vscode上利用live share插件。到了开发中后期的项目debug以及调试模块的部分,的确可以分branch分模块开发,我们的流程基本是这样。然后是在嵌入式单片机调试方面的一些收获和经验吧,我在调试过程既用到了图形化调试,也用了gdb+dashboard的方式,我觉得这两个方式都很重要,很需要。在单片机上开了gdbserver过后,gdb命令行调试的反汇编查看效果,以及单汇编指令执行过程的效果看起来更好,能够在一些关键的bug查看上很方便(比如查看栈溢出的情况,看看是否溢出到把一些全局变量的内存单元改变了,还比如查看一些hardfault的进入原因,在我们用的cortex-m系列里面,它会把描述信息压入任务栈(也就是psp堆栈),和riscv里面是放入在特定的寄存器里面,主要是查看进入异常的pc以及lr还有当时处理器的状态(xpsr),这样来debug),而图形化的调试更简单,能快速的查看一些其它小的bug,并且可视化查看代码也更方便,所以我的推荐是两个都使用。最后想谈谈我在移植UCOSII线程模块到embassy的项目的思考吧,其实观察到embassy这边的任务是一个统一的栈空间,而纯的线程支持的话就会是让每个线程独占一个栈,这样会导致栈的划分肯定是有内碎片的,所以统一的栈空间能让这些任务对于栈的利用率更好,一个任务的开销就变得更小,我们跑出来的测试效果也显示出在混合线程的情况下,任务的切换情况以及开销是优于纯的线程,只是和纯协程的情况几乎相同(在线程的切换systick较高10ms这种情况下),后续也想看看如果把线程和协程的混合更彻底,是否能真正做到无缝的task切换(目前实现方式的“缝隙”是源自于线程切换走后时间片的10ms会让另一边被切走的executor阻塞较长时间)

总结我的整个训练营的体会

开始

我刚好是在4.7号训练营开营的时候报的名,在刚开始做这个训练营的任务的时候,我就觉得这些lab的设计真的很好,使用官方的rustlings这种资料以及rcore实验和详细的参考书。我因此变得很有兴趣并且毫不犹豫的分享给我身边的同学和室友,让他们也来参加。事实上我能有机会在这里发言,离不开在训练营当中和室友之间相互学习,讨论分享,这些让我学到了更多,做出了更好的成果,这也是我训练营里面额外的收获。

中间

在前两个阶段的实验是对于操作系统的实践学习部分,很好的弥补了我在课内学习操作系统所缺乏的实践不足,这里我想引用LINUX之父说的一句话来说明这部分对于我学习操作系统的重要性: Talk is cheap. Show me the code。在这个过程里面,我慢慢的习惯上了命令行调试,自己也开始写脚本来做自动化的流程,从前不那么熟悉的makefile、shell脚本还有gdb都习惯起来了。不过不管怎么说,debug的过程还是有些煎熬的,遇到隐蔽的bug,还得看看内存或者看看寄存器。但是做实验的时候真的是干劲十足,那几天里面,每天就是起床,然后写lab,每过一个实验就去看看排行榜上排名提高了多少,正好就在5.1劳动节快结束的那几天把lab实验部分做完了,后面我就重新去看了rcore-tutorial book并且把它上面的思考题给做了,真的觉得这本书写得很不错的,是我入门操作系统看的第一本书,给我的印象很好。

结束

然后就是参加项目6的过程了,最开始选择这个项目就是看到了embassy,查到它是一个嵌入式的异步框架,自己感兴趣的是在嵌入式方面,而异步这个内容对我是一个新鲜未知的东西,于是我就更有兴趣来从项目中学习新的内容,也有一点信心能做出点什么。但是完成这个项目学习后,我发现我的收获不止于这些内容,因为作为大二的学生,其实我就逐渐开始迷茫于自己到底要做什么了,特别是当我做了一些经典的lab比如:csapp、cs144以及rcore,并且也学了编译原理、数据库、计网、操作系统还有一堆的数学基础之后,我不知道自己能做出什么东西。我只知道自己对计算机的体系结构感兴趣,还想做与嵌入式相关的内容,但是该从什么地方切入,怎么才算作是做研究,我一点头绪也没有。而在参加项目6这6周的时间里面,它更重要的是给我打开了一个小的缺口,让我知道在操作系统这个广阔的领域里面可以接纳许多不同的诉求,许多学到的内容以及想要应用的场景都可以和操作系统相关联,或者说操作系统和许多地方都能联系起来。在我和我的队友这一个多月的项目所做的成果中,我们在研究异步操作系统的同时,也给我们之前在学校里面所涉及的四轴飞行器的项目优化有了思路,对于如何做研究,怎么做也有了认识,在我自己的目标里面,我是希望能做一些虽然长期且有挑战,但是很有意义的事情。而这次的经历,就让我知道在我本科接下来的时间里面可以在什么领域做工作,并且这些工作是极具挑战且很有意义的。

感谢

最后我想感谢和我一起工作的队友:施诺晖同学,我和他是室友,做这个项目和他交流学习中收获很多,后续开发过程中和他的合作让我能够高效的完成工作。

还有很感谢向老师很耐心的指导而且老师也没push得很紧张,让我能在学校考试和训练营项目的交替中成功的都分得了合适的时间片完成任务。

第三阶段:R4L&跨内核驱动框架

只完成了前三个练习,后面各种大作业+期末考试没时间完成最后一个练习,还是比较可惜。

整个阶段3下来收获还是挺大的,在学校的操作系统大作业里只写过简单的字符设备驱动,在这里才真正把嵌入式、外设等结合进来。

  • 了解了跨内核驱动框架的含义和意义。
  • 了解了linux内核模块,设备树,设备IO(内存映射)等。
  • 阅读树莓派数据手册,完成了跨内核框架pure driver层和adapter层关于bcm2711的gpio驱动编写。
  • 对驱动和嵌入式有了更深入的了解。

继我的训练营第 1-2 阶段总结之后,终于结束了第 3 阶段。

这里的内容与我的幻灯片《阶段 3 学习总结 + 实现 rCore 异步定时器驱动》基本一致。

参加训练营以来的所有笔记,都在 https://zjp-cn.github.io/os-notes

每周任务(均完成)

开发日志

学习笔记

其他工作

致谢

向老师是我遇见的最好的老师!感谢向老师在这 6 周时间里倾囊相授,尤其对我充满了耐心、热情和鼓励。与您所交流的一切,远远大于我在训练营里学习的所有知识,也是我所得到的最具价值的收获。

在最后一周,我其实在笔记本里,用笔写了很多很多页想告诉向老师的话,但最终没有开口。

我是一个很容易受到鼓励而逐渐喜欢一个知识领域的人。十四年前,我向英语老师请教的第一个问题,打开了我对英语的兴趣大门,在之后的八年直到人生再无英语课堂,我一直是英语课堂上最跃跃欲试的学生,也常常从第一节晚自习缠着英语老师请教各种问题,直到晚自习放学而不知疲惫。七年前,我发现自己能够自学编程,随后在参加各种数学建模比赛中,负责编程,在收获名次的喜悦中,发展起对编程的爱好,而编程这个兴趣持续到现在。我只是凭借自己的努力而缓慢地学习着,所以,当向老师您每周热情地给予我宝贵的学习机会,我内心充满了感激。我甚至在第三阶段的前几周里,就不止一次地梦见您成为了我的老师,当梦醒来,我就已经知足了。但即便我有时非常希望请教您,我依然不得不拒绝这些机会:我不想麻烦任何人,尤其不想浪费向老师的时间和精力 —— 费心去教一个操作系统领域之外的人,是不值得的事情;此外,我害怕因为向老师而发展起对这个领域的兴趣,维持现有的兴趣已经让我支付了巨大的机会成本和沉没成本。


感谢陶要仲同学将异步运行时放置到时间中断处理函数里面,这是完成向老师所提的第二个步骤的最重要的一环;也谢谢你在最后一周、连续三个晚上倾听我喋喋不休,最终我们愉快地合作成功。


感谢操作系统训练营里的每位老师!感谢第三阶段杨杰老师、赵方亮老师对我的帮助。感谢 rCore 教程的作者陈渝老师、吴一凡老师(如果我了解错误或者仍有遗漏,请告诉我 ta 的名字)。rCore 教程是我阅读操作系统领域的第一本书,它带我领略到操作系统环环相扣的精巧设计,是我能够完成第二、三阶段非常重要的基础。


感谢全球的、开放的、包容的 Rust 社区!我所有的 Rust 知识来自 Rust 社区,来自我所阅读的每一篇文档、每一个博客、每一条帖子、每一则帮助、每一处讨论……是社区的力量,让我发现了新的兴趣,培养了新的习惯。没有积累充分的 Rust 知识储备,我不可能参加这个训练营,也不可能轻松阅读每一行 rCore、embassy 里的 Rust 代码,更不可能在训练营里与各位结识,最终走到现在。

第三阶段总结

第三阶段我参加的是向勇老师的项目6:异步操作系统的实现。

总的来说,6周的项目实训过程中,读了很多代码和文档,做了很多事情,学到了很多的东西。

在参加训练营之前,我对异步的了解其实也就是局限于说我们IO要异步,让出控制权,仅此而已。

但是在训练营中,我真正的接触到了何谓真正的异步,以及如何去实现它,受益匪浅。

同时我也将ArceOS的宏内核版本,将其进行了异步化,使整个内核都使用rust的async和await异步编程实现。

同时它也是无栈协程的切换方式,且对协程采用多栈复用的抢占。

第三阶段成果

在成果上,实现了对ArceOS家族系的Starry的异步化改造,使用async和await等rust提供的异步编程编写内核,已实现核心主体框架;同时对通用操作系统内核下协程的不合理性做调整,实现内核栈池,通过少量堆栈的复用实现对诸如键盘输入等高优先协程处理的抢占机制。

为后来者留下的东西,首先是代码产出:有充足的代码仓库参考,我将各个小步骤的代码都保存了下来。从原始版本 -> task实现 -> 同步无栈协程与显式执行流 -> 异步无栈协程 -> 多栈复用版本。

同时留下了若干个人的文档心得:考虑到训练营很多同学对从同步到异步的过程一头雾水,其实我本人最开始也是这样的,我个人认为更好的办法是看别人的代码,看看别人是怎么实现了,跟着别人学,先模仿,再谈能不能超越,因此留下文档如下:

  1. 我个人参考的两个内核的阅读和解析心得(去年OS内核唯二的异步内核)。链接:https://www.jensei.cn/?p=217
  2. 以Starry为基准的,整个改造过程是如何实现的,包含了从一个ArceOS宏内核(或者说rCore)如何一步步改为无栈异步内核。链接:https://www.jensenwei.cn/?p=221
  3. 我个人对其中一些机制的理解,比如async+await,poll等。链接:https://www.jensenwei.cn/?p=220

当然,更多的中间过程结果在项目六,向老师的文档中有所记录,这里不再列出。

第三阶段感受和体会

总的来说,第三阶段也算有一定的产出,这么长时间也算没白干,身为一个寄算机人,一切都要落实到代码产出上,光写文档吹牛逼不是一个合格的程序猿。

对我这个异步小白来说,理解和实现异步,再到利用异步的过程,是比较复杂且困难的,而且所面对的是如此大的一个内核,其藕断丝连的耦合性会对改造造成相当大的麻烦。尤其是我们对整个调度和进程管理的部分进行一次彻底的重构,这种挑战相当之大。

但是挑战之大,也是挺过来了,在改造整体框架下,大概前前后后推倒重来了十多次,这也是为什么我那么注重保存每一个小步骤的代码。

总的来说,训练营中我给自己列了一个很高的挑战,也尽力的去完成它了,最后的结果我个人是非常满意的,达到了自己对自己的要求。

第三阶段总结报告

相关进度

前期学习

项目实践

  • rcore 中整合 embassy, 首先遇到的问题就是在 executor 代码中里调试程序出错,后来发现了是因为在 poll 之后的一致性锁服务中加入了M态不是S态,将配置文件中的riscv加上一个s-mode的特征属性就好了。
  • 最后一学期在整合 embassy 提供的时钟中断服务时,因为没有找到关于 Driver trait 的具体实现,加上自身实力不济,并未能成功实现相关功能,是周积萍同学帮忙写的,然后我们测试了在单任务,多任务下的时钟中断服务,发现了各种各样的bug,例如任务的阻塞、无限嵌套时钟中断、忽然打出满屏的#号等等(按理来说地址越界是有相应的处理的,不知道这个为什么会直接不处理,可能不是地址越界造成的问题),有些是 embassy循环时的机制问题,有些则还未能解决,但并不妨碍我们去重构timer.rs中的相关定时器代码。
  • 最后在将代码放入 timer.rs 的定时器中并加以重构之后,我们测试了相关用例,发现符合我们的需求,并且能够正常运行。

感谢

  • 很感谢周积萍同学帮忙讲解 embassy 相关的运行机制,以及帮我解决 embassy 中的相关问题。
  • 也很感谢向老师每周周会时提出的建议,让我对之后的学习有了更加明确的方向。
  • 感谢夏令营的各位老师和助教们的帮助和支持,同时还提供了一个这么好的平台供我们学习成长。

内容

三阶段的项目我选择了Rust for Linux驱动开发方向。

具体内容是用Rust重写PL011串口驱动,并能通过跨内核驱动框架,使驱动在arceos和linux运行

这次的项目基于Raspi4b or Raspi3b in Qemu进行开发。

我使用的开发的仓库地址是(R4L_DRV)[https://github.com/Oveln/R4L_DRV]

练习部分

内容

练习部分分三个

  • 用Rust for Linux写一个简单的杂项设备驱动,能做到输入和输出
  • 用跨内核驱动框架,实现一个设备驱动,能控制Raspi4b上的某个引脚,从而控制LED的亮灭

遇到的困难

此前我完全没有接触过驱动的编写,对硬件的原理知之甚少。经过很长一段时间的RTFM,以及了解硬件编程相关的知识。

同时对Linux设备驱动相关的内容也进行了比较充足的了解。

收获

练习部分的收获主要是

  • 第一次接触了Linux的设备驱动,对文件树中的/dev/xxx的设备文件有了认知
  • 更加明白了什么叫Every thing is File
  • 稍微看的懂电路原理图了x
  • 对很长的文档的恐惧逐渐消退

实战部分:使用Rust重写PL011驱动

问题一

串口驱动的内容相较于简单的GPIO繁琐且复杂,需要我进一步的了解Linux的串口子系统tty-uart_driver-uart_port相关的注册关系。

在经过一些努力

  • RTFS
  • 找网上的资料学习(特别感谢野火的文档教程

这个问题逐渐的被解决了

问题二

Rust for Linux经过数年的发展,在某些方面有了完备的抽象。但这次项目所涉及的uart_driver、uart_port、console等结构体没有被支持。

这个问题没什么简单的办法,在Rust for Linux社区中也没有找到好的实现。只能自己做。

如何抽象函数指针

比如,在uart_port中有很多void*类型的函数指针,如何对这些函数指针进行良好的Rust抽象就成了一大问题。

我在R4L的代码中找到的解决方案是

1
2
3
4
struct ops {
void* print_int(int)
void* print_char(char)
}

以上代码可以通过Rust进行如下封装

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
31
32
33
34
pub struct OperationsVtable<T>(marker::PhantomData<T>);
pub trait Ops {
fn print_int(x: i32);
fn print_char(x: char);
}
impl<T: Ops> OperationsVtable<T> {
unsafe extern "C" fn print_int(x: c_int) {
T::print_int(x);
}
unsafe extern "C" fn print_char(x: c_char) {
T::print_char(x);
}

const VTABLE: bindings::ops = bindings::ops {
print_int: Some(Self::print_int),
print_char: Some(Self::print_char)
};

pub(crate) unsafe fn build() -> *const bindings::ops {
&Self::VTABLE as *const _
}
}

// 在定义了上面三段后就可以定义一个自己的Ops

struct MyOps;

impl Ops for MyOps {
...
}

// 之后想要得到MyOps的对应的C的ops结构体只需要

let ops = OperationsVtable<MyOps>::build();

现在的成果和收获

对Linux的C代码中的serial_core.h中的uart_driver和uart_port以及console做了比较完整的抽象。

我也能够比较好的使用Rust中的Pin使数据固定在内存上的某个地方了。

第三阶段总结

前期实验

第三阶段前期老师带领我们基于Arceos做了两周的实验

  • 练习1:支持彩色打印 println!
    较为简单,且有多种解法,可以在项目不同的层次进行修改来支持彩色打印
  • 练习2:支持HashMap数据类型
    在官网找到HashMap的源码然后进行一定增删就可以实现,因为初学rust,有些高级用法很生疏,改起来有点费劲。
  • 练习3:为内存分配器实现新的内存算法
    我的实现比较粗暴,有些内存空隙直接不要了
  • 练习4:解析dtb并打印
    在crate.io中有现成的解析dtb的crate,但是需要进行一定修改才能满足任务要求
  • 练习5:抢占式调度算法
    这个任务比较简单,改一下crates/scheduler/src/fifo.rs就好了

第二周的五个实验如下:

  • 实验一:从外部加载应用
  • 实验2:把应用拷贝到执行区域并执行
  • 实验3:通过 ABI 调用 ArceOS 功能
  • 实验4:正式在 App 中调用 ABI
  • 实验5:支持内核和应用分离的地址空间及切换

对于第二周的五个实验总体来说就是把外部应用APP写入PFLASH中,然后在内核中写一个APP加载器,为APP初始好寄存器后跳转到APP执行,同时支持多地址空间以及给APP传递内核API_TABLE的地址。第二周让我印象深刻的是跳转到APP执行前对寄存器的初始化很重要,如果初始化不到位可能会导致运行APP的时候出现各种错误。同时实验4也让我学到了如何以通过传参的方式支持APP调用内核API。

之后就是进入真正的项目阶段了,项目阶段是在lkmodel上开发,lkmodel相比Arceos我个人感觉难度还是有一定提升,且前两周刚熟悉完Arceos的代码和模块(T_T),一开始还是挺畏难的。不过最后在做的过程中慢慢熟悉lkmodel后也就不那么害怕了。

进入真正的项目阶段

musl入手

由于musl编译出来的app系统调用要比glibc简单的多且少得多,因此我先尝试支持用musl-gcc编译的hello_world程序。
经历大概如下:

  • 支持加载hello_world.bin程序并执行
  • 支持对elf的解析
  • 支持对hello_world.elf程序的执行

支持glibc

要支持glibc要实现和空实现一些syscall,让我印象比较深的是writev_syscall
原本lkmodel的writev_syscall实现如下:

1
2
3
4
5
6
7
8
9
10
pub fn writev(fd: usize, iov_array: &[iovec]) -> usize {
assert!(fd == 1 || fd == 2);
for iov in iov_array {
debug!("iov: {:#X} {:#X}", iov.iov_base, iov.iov_len);
let bytes = unsafe { core::slice::from_raw_parts(iov.iov_base as *const _, iov.iov_len) };
let s = String::from_utf8(bytes.into());
error!("{}", s.unwrap());
}
iov_array.len()
}

但是运行的时候发现没有输出,于是我简单的在writev里面加了一个early_console::write_bytes(bytes);。但发现输出的时候遇到了奇怪的现象,例如输出一个Hello_world,他会进行多次输出,并且输出的字符个数逐渐减少。如:

1
修改后:

pub fn writev(fd: usize, iov_array: &[iovec]) -> usize {
assert!(fd == 1 || fd == 2);
let mut total_bytes_written = 0;
for iov in iov_array {
//debug!(“iov: {:#X} {:#X}”, iov.iov_base, iov.iov_len);
let bytes = unsafe { core::slice::from_raw_parts(iov.iov_base as *const _, iov.iov_len) };
let s = String::from_utf8(bytes.into());
early_console::write_bytes(bytes);
total_bytes_written += iov.iov_len;
}
total_bytes_written
}

1
2
3
4
5
6

#### 发现的问题及解决思路
1. 在支持glibc的时候由于glibc调用了一些task相关的syscall,需要拿到当前的task_sched_info,但是每次都拿不到(None),就很奇怪,明明也进行了task::init(),为什么拿不到呢?后面一直跟踪拿task_sched_info的源码才发现是通过读gp寄存器拿的,即项目中的```pub fn gen_read_current_raw(symbol: &Ident, ty: &Type) -> proc_macro2::TokenStream ```存也是把task_sched_info的地址存在gp寄存器上```pub fn gen_write_current_raw(symbol: &Ident, val: &Ident, ty: &Type) -> proc_macro2::TokenStream ```。然后我就怀疑gp寄存器在app运行时被改变了,于是查看qemu.log,果然一开始就动了gp寄存器。于是我采用一个全局静态变量来存当前的task_sched_info,这样就可以顺利拿到了。
2. 一开始没有清理.bss,导致运行时各种奇怪的问题。其实一开始没清理.bss是因为我在做payload的时候是创建了一个32M的空文件(全0),然后把app.elf文件写进去,我心想写的时候是全0,我就不用清理了吧,但在debug的时候发现利用lkmodel下的elf crate来对app.elf进行解析的时候竟然把.bss那块儿区域写了一些数据,就因为偷了个懒,导致又浪费一大把时间debug。
3. 在支持vfork时,vfork的子进程由于要copy父进程的进入trap_handler时的寄存器信息以正确返回。在子进程返回时发现其trap上下文的sepc寄存器一直是0,且其他寄存器的值也不正常,经过调试发现是copy父进程的trap上下文时出现了问题,最后发现问题如下:
pt_regs_addr()函数(即拿到trap上下地址的函数)原来是
pub fn pt_regs_addr(&self) -> usize {
    self.kstack.as_ref().unwrap().top() - align_down(TRAPFRAME_SIZE, STACK_ALIGN)
}
1
2
3
但是由于align_down导致减去的值不是TRAPFRAME_SIZE
但在汇编中却是用self.kstack.as_ref().unwrap().top() 直接减去TRAPFRAME_SIZE
如下:

.Ltrap_entry_s:
addi sp, sp, -{trapframe_size}
SAVE_REGS 0
mv a0, sp
auipc a1, 0 # Load the upper 20 bits of the PC into a1
addi a1, a1, 12
call riscv_trap_handler
RESTORE_REGS 0
sret

.Ltrap_entry_u:
addi sp, sp, -{trapframe_size}
SAVE_REGS 1
mv a0, sp
li a1, 1
call riscv_trap_handler
addi t0, sp, {trapframe_size} // put supervisor sp to scratch
csrw sscratch, t0
RESTORE_REGS 1
sret

1
2
3
因此在rust中是用pt_regs_addr()获取trap_frame地址然后进行读写操作是和汇编里面的存储位置不匹配。
4. 不知道是不是因为静态链接的原因,我这边argc,argv,env在栈上的排布和ppt不一样,用ppt的方式排布app拿不到argv
下面是我在其他地方看到的另一种排布方式,用此方式排布app可以正确拿到argv参数

高地址

0

envp[1]
envp[0]
0

argv[1]
argv[0]
argc

低地址

```

  1. 由于要支持多地址空间,因此我需要给每个app重新分配一个页表,并在这个基础上为app分配内存。同时我为了复用lkmodel下的代码,我采用user_mode_thread来创建一个新的进程,因此我需要对user_mode_thread代码进行修改,最重要的是修改地址空间那块,我需要为app准备一个新的地址空间,而不是和内核共享同一个地址空间。于是我考虑更改mm_copy模块,但后面想到不能直接改这里,因为mm_copy这个函数会被多个模块调用,我改了可能其他模块就跑不通了。于是我决定在user_mode_thread外面为app的task.mm重新赋值(即建立新的地址空间),但神奇的事情发生了,赋值后会导致内核的页表(即之前task.mm指向的区域)被回收(Arc指针计数并不为0),之后为了验证是否是我修改了其他模块导致该问题,于是我重新git clone下来了原仓库并在原仓库基础上进行测试,发现还是会出现相同的问题。最后我通过其他手段绕过了该问题。

    总结

    这一个月的项目实习使我debug能力又有了提升同时更加深刻的理解了musl-gcc,glib-gcc的区别,同时也更加深刻的了解了Unikernel。同时也跟同学老师们学到了很多东西。

仓库链接

https://github.com/xhyf77/lkmodel/tree/dev

报告编写时间好像有点晚

第一阶段总结报告

大概是寒假的时候,在网络上寻找一些学习操作系统的资料时,接触到了rcore lab和这个训练营,然后看到有一起学习的群友报名了这个训练营之后,立马顺着链接跟过来了。

在此之前并没有编写过类似的操作系统,了解学习过一些操作系统的知识,而且也对rust不是很熟悉。

rust语言

在参加这个训练营之前,是尝试学习过rust的,当时rust吸引我的原因不是因为它在内存安全方面的独特特性,而是它是一门现代的,系统级编程语言。而我确实也需要这样的语言(尝试为c语言寻找更加现代的替代)。不过当时由于rust特有的难以入门的特性和陡峭的学习曲线,随后便暂时放弃了这个语言的学习。

在参加了这个训练营之后,重新捡起来rust语言,发现也并不是想象中的那么可怕,其语言特性的设计理念,也可以理解。而且从这里,也进一步的体验到了其作为一门现代语言的好处。例如包管理器(安装新的工具链太方便啦),更好的lsp服务器(感觉比clangd强),以及语言提供的恰到好处的抽象设计,都让人感到很舒心。而且作为一门系统级语言,也很方便的可以在之后帮助我编写运行在一些裸机上的应用程序。

rustlings

这个练习前半部分基本都是和语言特性相关的内容,做起来还是很迅速的,基本上没有花费很长的时间便完成了这一部分的内容。但是对于后面的算法题部分,便开始困难起来了。倒不是这些数据结构和算法不清楚,如果使用c语言实现它们,那么将易如反掌,但是对于自己不熟悉,且时时刻刻强调内存安全的rust语言而言,便显得很困难。好在在查阅资料和不断的尝试下,还是将这一部分内容给完成了。

第二阶段总结报告

第二阶段的前三个实验,基本上是连着几天完成的,最后两个实验拖慢了几天。

总的来说,对于这一部分,还是学到了很多知识。

首先说一说riscv方面吧,对于这一方面相对更加熟悉,之前也编写过riscv的处理器和模拟器,并且也使用c语言编写过裸机应用,对于riscv相关的知识方面,还是很顺滑的便掌握了。

对于rust语言,写到这个阶段已经比第一阶段顺手多了,不过在使用这个语言时使用的很多设计理念,例如资源获取即初始化,等等设计方面的考量,还是学习到了很多。或许在完成这一部分内容之后,可以试着使用rust去重构之前写过的一些c语言项目了(乐)。

随后是对于操作系统相关的知识,之前学习过一些操作系统的概念和知识,但是对于真正的将操作系统实现出来,还是第一次尝试做出来。以前学习操作系统时还尝试看过一些linux内核设计相关的书籍,最后的结果是看的比较稀里糊涂。通过这次操作系统设计之后,或许之后入门linux能够更加顺利一些。

前言

在去年秋天听闻了这个训练营,机缘巧合之下结识了Rust和rcore。 也许是由于rust这门语言入门门槛比较高,加之当时没有其他更modern语言的学习经验,只做完Rustlings就已经打算放弃了。从去年秋天到现在一直在写Rust,也了解了许多系统方面的知识,就打算在这次训练营中弥补一些遗憾。

第一阶段

心得

第一阶段更多的是教初学者通过查手册、book或者是问AI来逐渐了解Rust的各种特性,如所有权机制(一个value只能对应一个所有者)、RAII机制(自动回收)、智能指针(个人认为智能指针的学习更多的应该落实于应用场景:即它为什么而存在的问题)、生命周期(我认为它的存在更多的是帮助编译器编译)等,这些是Rust比较有意思的机制,也是大大保障了代码安全性的由来。

体验

有了去年以来一直积累的Rust经验,做Rustlings倒是非常快了。最后出的十道算法题非常有意思,大大加深了我对于智能指针的认识,虽然都是入门的算法题倒算是个很新奇的体验。

资料

由训练营给出的学习资料,同时也推荐Stanford的CS110L。

第二阶段

学习过程

rcore这个类Unix内核对于了解各种OS的各种核心内容而言是非常有帮助的,实际上,若不是有了Rust的一些经验,很难理清由理论转换为具体实现的过程。

对于rcore的学习,刚开始我十分痛苦,看着后面的实验要求一时间无计可施,只能由各种已经实现的syscall一个个去trace他们的调用关系,才有了一点点的大致思路。但具体实现起来又有其他的问题,这时候我的目光就放在了具体的数据结构上,需要彻底弄明白每一个数据结构的具体功能、与其他数据结构的关联,才能够说很好的掌握了整个流程。

rcore总结

  • Chapter1-2:需要关注于环境如何一步步搭建,如何切换特权级
  • Chapter3:难点在于理解任务切换时候的流程:trap上下文的保存。应用在内核中处理时, 其 Trap 控制流调用__switch函数,这个函数非常重要,里面就是任务的上下文寄存器。
  • Chapter4:对于我而言,虚拟内存几乎是最难的一个章节了,以至于看到实验要求的时候说注意数据被切分成两个不同的页表中时我甚至不知道它想表达什么,反复观看、查找资料之后才能艰难完成。
  • Chapter5:关键在于理解有关进程的每一个数据结构,以及它的调度机制,重点需要弄明白suspend_current_and_run_next这个函数以及整个task的工作流程:take_current_task 来取出当前正在执行的任务,修改其进程控制块内的状态变成Running,随后将这个任务放入任务管理器的队尾。接着调用 schedule 函数来触发调度并切换任务。
  • Chapter6:文件系统相比起前面的章节有些异常庞大了,需要抓住核心抽象以及它的层次结构。
  • Chapter7:了解进程之间如何通信,关注于各种不同的通信方式。
  • Chapter8:并发是OS绕不开的一个话题,掌握各种锁+线程的概念对于理解并发有奇效,但其实Rust的很受欢迎的异步并发并没有提及,想来是语言特性而不属于OS教学层面上的。推荐一个了解Rust异步并发非常好的读物:Async in depth | Tokio - An asynchronous Rust runtime

一些心得

我觉得最重要的,也是支撑我艰难走过rcore这道难关的就是一个观念,在南京大学的OS课程上反复强调的:操作系统就是一个应用程序。这也是我在进行每一个lab后最大的感触。在接管了trap之后,所有的一切都是逻辑上的开发,与不同的应用程序没有什么区别。

资料

  • 南京大学OS课程
  • MIT6.S081: xv6 book
  • CSAPP