0%

转眼间训练营都结束了,这里大致总结一下我的第三阶段。

我第三阶段选择的课题是跨内核驱动开发,具体的要求,就是实现一个驱动,其能够在arceos和linux上运行,且其纯驱动代码部分不必修改。

一个设备在操作系统看来无非就是一些寄存器的集合,根据设备手册,我们可以知道如何通过修改和读取寄存器来控制和了解设备状态,对任何一个操作系统来说,都是如此,这也是纯驱动(也就是直接操作寄存器)代码不必修改的原因。

但是,对不同的内核,我们还需要不同的驱动适配器,比如,在linux中,我们需要向内核注册这一个设备(比如注册成杂项设备),并将设备抽象成一个文件,实现抽象的read和write方法。

此外,不同的内核地址映射的方法可能不同,在适配器中我们需要调用各自内核映射地址的接口,得到对应寄存器的地址,然后就可以使用纯驱动部分代码了。

在这个阶段里,我实现了一个GPIO点灯驱动,并在linux和arceos里成功运行,在这个过程中,知道了如何为linux编写驱动,rust是如何与kernel相配合的,以及在开发过程中由于踩各种坑被迫学习了一些关于linux kernel是如何编译的知识。

训练营结束之际,想感谢各位老师的帮助,各位同学的陪伴,也想感谢坚持到最后的自己。

学习内容:

rust异步编程,阅读200行代码讲透RUST FUTURES、rust圣经异步模块、embassy book,向勇老师的视频《并发与处理器、操作系统和编程语言》。

成果:

使用Tokio模块实现豆瓣图书爬虫。

学习心得:

通过阅读资料,学习rust 异步,针对IO密集型程序,异步相比多线程能够有较大的性能提升。rust 语言通过async和await 支持通过接近同步代码形式编写异步程序。rust 异步需要使用异步运行时,标准库并没有提供,常用的第三方库Tokio。
将异步移植到OS里面,遇到一些问题和困惑。1、ArceOS引入异步的话,是否只要引入异步运行时就可以;2、引入的运行时是否能用运行在标准库的运行时。
在继续学习,后续努力在ArceOS 网络模块引入异步时。

第三阶段总结报告

完成的独立工作

前期学习内容

  • 阅读《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