第三阶段: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实现 -> 同步无栈协程与显式执行流 -> 异步无栈协程 -> 多栈复用版本。
同时留下了若干个人的文档心得:考虑到训练营很多同学对从同步到异步的过程一头雾水,其实我本人最开始也是这样的,我个人认为更好的办法是看别人的代码,看看别人是怎么实现了,跟着别人学,先模仿,再谈能不能超越,因此留下文档如下:
当然,更多的中间过程结果在项目六,向老师的文档中有所记录,这里不再列出。
总的来说,第三阶段也算有一定的产出,这么长时间也算没白干,身为一个寄算机人,一切都要落实到代码产出上,光写文档吹牛逼不是一个合格的程序猿。
对我这个异步小白来说,理解和实现异步,再到利用异步的过程,是比较复杂且困难的,而且所面对的是如此大的一个内核,其藕断丝连的耦合性会对改造造成相当大的麻烦。尤其是我们对整个调度和进程管理的部分进行一次彻底的重构,这种挑战相当之大。
但是挑战之大,也是挺过来了,在改造整体框架下,大概前前后后推倒重来了十多次,这也是为什么我那么注重保存每一个小步骤的代码。
总的来说,训练营中我给自己列了一个很高的挑战,也尽力的去完成它了,最后的结果我个人是非常满意的,达到了自己对自己的要求。
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]
练习部分分三个
此前我完全没有接触过驱动的编写,对硬件的原理知之甚少。经过很长一段时间的RTFM,以及了解硬件编程相关的知识。
同时对Linux设备驱动相关的内容也进行了比较充足的了解。
练习部分的收获主要是
串口驱动的内容相较于简单的GPIO繁琐且复杂,需要我进一步的了解Linux的串口子系统tty-uart_driver-uart_port相关的注册关系。
在经过一些努力
这个问题逐渐的被解决了
Rust for Linux经过数年的发展,在某些方面有了完备的抽象。但这次项目所涉及的uart_driver、uart_port、console等结构体没有被支持。
这个问题没什么简单的办法,在Rust for Linux社区中也没有找到好的实现。只能自己做。
比如,在uart_port中有很多void*类型的函数指针,如何对这些函数指针进行良好的Rust抽象就成了一大问题。
我在R4L的代码中找到的解决方案是
1 | struct ops { |
以上代码可以通过Rust进行如下封装
1 | pub struct OperationsVtable<T>(marker::PhantomData<T>); |
对Linux的C代码中的serial_core.h中的uart_driver和uart_port以及console做了比较完整的抽象。
我也能够比较好的使用Rust中的Pin使数据固定在内存上的某个地方了。
第三阶段前期老师带领我们基于Arceos做了两周的实验
crates/scheduler/src/fifo.rs
就好了第二周的五个实验如下:
对于第二周的五个实验总体来说就是把外部应用APP写入PFLASH中,然后在内核中写一个APP加载器,为APP初始好寄存器后跳转到APP执行,同时支持多地址空间以及给APP传递内核API_TABLE
的地址。第二周让我印象深刻的是跳转到APP执行前对寄存器的初始化很重要,如果初始化不到位可能会导致运行APP的时候出现各种错误。同时实验4也让我学到了如何以通过传参的方式支持APP调用内核API。
之后就是进入真正的项目阶段了,项目阶段是在lkmodel上开发,lkmodel相比Arceos我个人感觉难度还是有一定提升,且前两周刚熟悉完Arceos的代码和模块(T_T),一开始还是挺畏难的。不过最后在做的过程中慢慢熟悉lkmodel后也就不那么害怕了。
由于musl编译出来的app系统调用要比glibc简单的多且少得多,因此我先尝试支持用musl-gcc编译的hello_world程序。
经历大概如下:
要支持glibc要实现和空实现一些syscall,让我印象比较深的是writev_syscall
原本lkmodel的writev_syscall实现如下:
1 | pub fn writev(fd: usize, iov_array: &[iovec]) -> usize { |
但是运行的时候发现没有输出,于是我简单的在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 |
|
pub fn pt_regs_addr(&self) -> usize {
self.kstack.as_ref().unwrap().top() - align_down(TRAPFRAME_SIZE, STACK_ALIGN)
}
1 | 但是由于align_down导致减去的值不是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 | 因此在rust中是用pt_regs_addr()获取trap_frame地址然后进行读写操作是和汇编里面的存储位置不匹配。 |
0
…
envp[1]
envp[0]
0
…
argv[1]
argv[0]
argc
–
低地址
```
user_mode_thread
来创建一个新的进程,因此我需要对user_mode_thread
代码进行修改,最重要的是修改地址空间那块,我需要为app准备一个新的地址空间,而不是和内核共享同一个地址空间。于是我考虑更改mm_copy模块,但后面想到不能直接改这里,因为mm_copy这个函数会被多个模块调用,我改了可能其他模块就跑不通了。于是我决定在user_mode_thread
外面为app的task.mm重新赋值(即建立新的地址空间),但神奇的事情发生了,赋值后会导致内核的页表(即之前task.mm指向的区域)被回收(Arc指针计数并不为0),之后为了验证是否是我修改了其他模块导致该问题,于是我重新git clone下来了原仓库并在原仓库基础上进行测试,发现还是会出现相同的问题。最后我通过其他手段绕过了该问题。报告编写时间好像有点晚
大概是寒假的时候,在网络上寻找一些学习操作系统的资料时,接触到了rcore lab和这个训练营,然后看到有一起学习的群友报名了这个训练营之后,立马顺着链接跟过来了。
在此之前并没有编写过类似的操作系统,了解学习过一些操作系统的知识,而且也对rust不是很熟悉。
在参加这个训练营之前,是尝试学习过rust的,当时rust吸引我的原因不是因为它在内存安全方面的独特特性,而是它是一门现代的,系统级编程语言。而我确实也需要这样的语言(尝试为c语言寻找更加现代的替代)。不过当时由于rust特有的难以入门的特性和陡峭的学习曲线,随后便暂时放弃了这个语言的学习。
在参加了这个训练营之后,重新捡起来rust语言,发现也并不是想象中的那么可怕,其语言特性的设计理念,也可以理解。而且从这里,也进一步的体验到了其作为一门现代语言的好处。例如包管理器(安装新的工具链太方便啦),更好的lsp服务器(感觉比clangd强),以及语言提供的恰到好处的抽象设计,都让人感到很舒心。而且作为一门系统级语言,也很方便的可以在之后帮助我编写运行在一些裸机上的应用程序。
这个练习前半部分基本都是和语言特性相关的内容,做起来还是很迅速的,基本上没有花费很长的时间便完成了这一部分的内容。但是对于后面的算法题部分,便开始困难起来了。倒不是这些数据结构和算法不清楚,如果使用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这道难关的就是一个观念,在南京大学的OS课程上反复强调的:操作系统就是一个应用程序。这也是我在进行每一个lab后最大的感触。在接管了trap之后,所有的一切都是逻辑上的开发,与不同的应用程序没有什么区别。
本篇内容为基础阶段 - Rust编程
的总结,分为环境配置
及语言学习
两个板块。
因为语言学习涉及内容较为零散,故将DAY0-DAY?汇聚成一篇。
环境配置部分主要流程就是根据教程内容一步步安装,同时需要注意以下几点:
按照LearningOS仓库中readme指定的教程一步步操作即可
注意没有curl
需要先通过如下命令安装(其他同理):
1 | sudo apt install curl |
之前配置过,故省略
注意:配置ssh后,git push到github时若仍然提示输入密码,可修改.git/config文件中的url,从HTTPS https://github.com/achintski/opencamp-24sp-daily.git 更新为SSH git@github.com:achintski/opencamp-24sp-daily.git
因此我穿插查阅/参考了如下资料:
并通过穿插完成rustlings
作业进行巩固练习
注意:第一次学一个概念时一定要打好基础,不要为了追求进度而忽略质量
HelloWorld
类型
*原生类型
* 布尔 bool:两个值 true/false。
* 字符 char:用单引号,例如 'R'、'计',是 Unicode 的。
* 数值:分为整数和浮点数,有不同的大小和符号属性。
* i8、i16、i32、i64、i128、isize
* u8、u16、u32、u64、u128、usize
* f32、f64
* 其中 isize 和 usize 是指针大小的整数,因此它们的大小与机器架构相关。
* 字面值 (literals) 写为 10i8、10u16、10.0f32、10usize 等。
* 字面值如果不指定类型,则默认整数为 i32,浮点数为 f64。
* 数组 (arrays)、切片 (slices)、str 字符串 (strings)、元组 (tuples)
* 指针 & 引用
* 函数
* 闭包
条件
循环
结构化数据
IO
Q:任何行为背后都有动机,Rust特性这样设计的动机是什么呢?
变量绑定–let
不可变变量 vs 常量
语句 vs 表达式
所有权 & 生命周期
Python/Java
等往往会弱化堆栈的概念,但是要用好 C/C++/Rust
,就必须对堆栈有深入的了解,原因是两者的内存管理方式不同:前者有 GC
垃圾回收机制,因此无需你去关心内存的细节。copy
/ move
borrow
(借用)&
与可变借用&mut
大致步骤:终端输入命令rustlings watch
、修改代码、删掉注释并自动跳转下一题
注意:后两个资料中概念介绍顺序和习题涉及概念顺序一致
技巧:学会分析编译器提示,有的题目是语法错误,有的是考虑不周导致测试过不去
vec2
enums3
strings3&strings4
into()
hashmaps2
HashMap
的get()
方法只能返回值的引用*
也需要转移所有权quiz2
Command::Append(n) => {}
中的n是&usize类型,可以使用for i in 0..*n
完成遍历不可变引用的string
,要想修改需要先将其clone
给一个可变变量options1
match
,也可以用if
options2
Options枚举
的本质目的:解决Rust
中变量是否有值的问题,因此第二个任务中需要两层嵌套的Some
if let
/ while let
本质上是match
match
中匹配后的绑定options3
Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y)
处,value partially moved here;而在最后返回值y
处,value used here after partial moveref
和&
errors2
Result
的函数,一定要显式进行处理?
操作符(本质是宏,同时可以链式调用)?
操作符,也可以使用match
errors3
?
操作符只能在返回值为Result
/ Option
的函数中使用use std::error::Error;
main
函数增加返回值 -> Result<(), Box<dyn Error>>
Ok(())
match
/ if else
errors4
match guard
if
& else
errors6
impl
模块self
、&self
和&mut self
及所有权self
表示实现方法的结构体类型的实例
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对实现方法的结构体类型的实例
的不可变借用&mut self
表示可变借用PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation)
仅可以用来处理x<=0的情况,而x非数字的情况无法处理?
操作符:Err
/ None
类型直接立即结束,提前返回;否则从Ok
/ Some
中取出返回值作为?
操作符的结果generics1
&str
vs String
_
来让编译器自动推断generics2
1 | struct Point<T> { |
impl<T>
Point<T>
不再是泛型声明,而是一个完整的结构体类型,因为我们定义的结构体就是 Point<T>
而不再是Point
Point<T>
类型,你不仅能定义基于T
的方法,还能针对特定的具体类型,进行方法定义trait(特征)
impl Trait for Type
:为Type类型实现Trait特征impl Trait
impl Trait
此时为语法糖,可用特征约束
写出完整版多重约束
:impl T1 + T2
quiz3
T
:impl<T: std::fmt::Display> ReportCard<T> {...}
(根据编译器help提示)lifetims1
<'a>
longest
时,那生命周期'a
的大小就是x
和y
的作用域的重合部分,换句话说,'a
的大小将等于x
和y
中较小的那个1 | help: consider introducing a named lifetime parameter |
lifetimes2
longest
函数中,string2
的生命周期也是'a
,由此说明string2
也必须活到 println! 处,可是string2
在代码中实际上只能活到内部语句块的花括号处}
,小于它应该具备的生命周期'a
,因此编译出错(编译器没法知道返回值没有用到string2
)lifetimes3
<'a>
&'a
lifetime others
impl
中必须使用结构体的完整名称,包括<'a>
,因为生命周期标注也是结构体类型的一部分!tests4
#[should_panic]
:The test passes if the code inside the function panics; the test fails if the code inside the function doesn’t panic.unsafe fn
来进行定义unsafe
声明的函数时,一定要看看相关的文档,确定自己没有遗漏什么 –>iterators1
Iterator trait
的类型next()
Option<Item>
类型,当有值时返回Some(Item)
,无值时返回None
mut
可变,因为调用next
会改变迭代器其中的状态数据(当前遍历的位置等),而for
循环去迭代则无需标注mut
,因为它会帮我们自动完成iterators2
iter()
Iterator adapters
(迭代器适配器)map()
、filter()
、take()
、zip()
、rev()
collect()
、sum()
、any()
Copy
类型,消费者在取得每个值后,在迭代器被清除后,集合里的元素也会被清除。集合会只剩“空壳”,当然剩下的“空壳”也会被清除一个例子:
1 | let v1: Vec<i32> = vec![1, 2, 3]; |
map 函数的闭包并没有获得迭代器的所有权。具体解释如下:
v1.iter()
创建了一个针对v1
中元素的迭代器。这个迭代器是对v1
的不可变引用,也就是说,它拥有对v1
中元素的借用权,但并不拥有所有权。
map(|x| x + 1)
是对上述迭代器应用的一个闭包。闭包内部的|x| x + 1
表示对迭代器产生的每个元素x
加上 1。在这个过程中,闭包接收的是x
的不可变引用,同样没有获取x
或迭代器的所有权。
综上所述,闭包并未获得迭代器的所有权。它只是在map
函数执行期间,对迭代器提供的每个元素借用并进行计算。一旦map
函数结束,闭包对元素的借用也随之结束,不会影响到v1
或其迭代器的所有权状态。
…
如果您想让闭包返回的新值形成一个新的集合(如 Vec
1 | let incremented_values: Vec<i32> = v1.iter().map(|x| x + 1).collect(); |
在这里,collect()
方法会消费map
返回的迭代器,并将其内容收集到一个新的Vec<i32>
中。然而,即使如此,闭包本身仍然没有获得迭代器的所有权,而是collect()
函数在处理过程中获取了所有权并完成了数据的转移。
iterators3
iter()
takes elements by reference.iter_mut()
takes mutable reference to elements.into_iter()
takes ownership of the values and consumes the actual type once iterated completely. The original collection can no longer be accessed.collect()
会根据函数返回值自动调整格式iterators4
iterators5
1 | fn count_for(map: &HashMap<String, Progress>, value: Progress) -> usize { |
1 | fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize { |
collection.iter()
创建一个迭代器,它遍历 collection
中的每一个 HashMap
引用。HashMap
应用 flat_map(|map| map.values())
,将每个 HashMap
的值迭代器扁平化为单个包含所有 HashMap
值的迭代器。filter(|val| *val == value)
,筛选出与目标 value
相同的 Progress
枚举值。count()
方法计算筛选后的元素数量,即符合条件的 Progress
枚举值的总数,返回这个计数值作为函数结果。smart_pointers(智能指针)
1 | fn main() { |
foo
函数中,a
是 String
类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从 foo
函数转移给 main
中的 b
变量时,栈上的智能指针被复制一份赋予给 b
,而底层数据无需发生改变,这样就完成了所有权从 foo
函数内部到 b
的转移。Rust
中,凡是需要做资源回收的数据结构,且实现了 Deref
/DerefMut
/Drop
,都是智能指针
arc1
let shared_numbers = Arc::new(numbers);
:将 numbers
向量封装在一个 Arc<Vec<u32>>
中。Arc
允许多个线程同时拥有对同一数据的访问权,且其内部的引用计数机制确保数据在所有持有者都不再需要时会被正确释放。这样,numbers
可以在多个线程间共享而无需复制整个向量,既节省了内存,又保证了线程安全性let child_numbers = Arc::clone(&shared_numbers);
:创建 shared_numbers
的克隆(实际上是增加其引用计数)。每个线程都获得一个指向相同底层数据的独立 Arc
thread::spawn
创建一个线程move
关键字:指示闭包在捕获外部变量时采取“所有权转移”策略,而非默认的借用策略join()
方法会阻塞当前线程,直到指定的线程完成其任务。unwrap()
处理 join()
返回的 Result
,在没有错误时提取出结果(这里没有错误处理,因为假设所有线程都能成功完成)。这样,main()
函数会等待所有子线程计算完毕后再继续执行cow1
threads1
thread::spawn
函数并传递一个闭包,在闭包中包含要在新线程 执行的代码。Rust
标准库提供的thread::JoinHandle
结构体的join
方法,可用于把子线程加入主线程等待队列,这样主线程会等待子线程执行完毕后退出。unwrap()
threads2
1 | println!("jobs completed {}", status.lock().unwrap().jobs_completed); |
jobs completed x
,x
的值有时候全是10,有时候有一部分是10;放到循环外就是一次jobs completed 10
threads3
1 | | |
tx
变量move
到第一个闭包后,已经无法在该闭包外获取了,而在第二次进程创建仍然尝试move
。通过为每个变量创建tx
的clone
,我们确保了每个闭包都拥有其独立的sender
,从而避免了use after move
错误macros4
clippy1
clippy3
is_none()
检查确保了 my_option
是 None
,因此接下来不应该再尝试调用 unwrap()
Vec::resize
方法用于改变已有向量的长度,如果新的长度大于当前长度,则填充指定的默认值。resize(0, 5)
并不是正确做法,因为这会让人们以为你要填充默认值5到一个空向量中,而实际上你是从一个非空向量开始的。vec![1, 2, 3, 4, 5]
起始,然后得到一个空向量,你应该直接使用 clear
方法,而不是 resize
。as_ref_mut
using_as
1 | --> exercises/conversions/using_as.rs:17:11 |
f64
类型没有实现 Div<usize>
trait,因此无法执行除法操作。为了解决这个问题,我们可以将 values.len()
转换为 f64
类型,以便进行除法运算。values.len() as f64
将 values.len()
的结果转换为 f64
类型,以便与 total
执行除法操作。from_into
Person
结构实现From<&str>
traitfrom_str
FromStr
trait 来将字符串解析为 Person
结构体,并返回相应的错误类型 ParsePersonError
tests5
这段代码中,modify_by_address
函数使用了 unsafe
关键字来标记它的不安全性。根据注释,我们需要在函数的文档注释中提供有关函数行为和约定的描述。
以下是修改后的代码:
1 | /// # Safety |
在函数的(文档)注释中,我们明确了对 address
参数的要求,即它必须是一个有效的内存地址,并指向一个可变的 u32
值。我们还提醒调用者必须满足这些要求,并在不满足要求时可能会导致的未定义行为。
在函数内部,我们使用了 address
参数将其转换为一个可变的 u32
指针 value_ptr
,然后通过解引用该指针获取可变引用 value
。最后,我们将 value
设置为 0xAABBCCDD
。
test6
raw_pointer_to_box
函数,它将一个原始指针转换为一个拥有所有权的 Box<Foo>
。我们需要根据给定的指针重新构建一个 Box<Foo>
对象。test7&test8
tests7
部分,我们设置了一个名为 TEST_FOO
的环境变量,并使用 rustc-env
指令将其传递给Cargo。tests8
部分,我们使用 rustc-cfg
指令启用了名为 pass
的特性。test9
algorithm1
Ord
and Clone
traits for the generic type T
to enable value comparisons and cloning, respectively.algorithm2
定义两个指针 current
和 prev
,分别指向当前节点和上一个节点。初始时 current
指向链表的头节点,prev
为 None
。
进入循环,在每次迭代中:
node
。current
移动到下一个节点。next
指针指向上一个节点 prev
。prev
不为 None
,则更新上一个节点的 prev
指针指向当前节点。prev
为 None
,说明当前节点是新的尾节点,更新 self.end
为当前节点。prev
更新为当前节点。循环结束后,将 self.start
更新为最后一个节点,即反转后的新头节点。
algorithm9
next
中,对vec
的处理除了要把最后一个元素拷贝到下标为1
处,还需要把尾部元素用pop()
删除。可以合起来写(后面加一个?
),也可以分开。 1 | fn next(&mut self) -> Option<T> { |
本地通过但autograde没通过:可以在actions-GitHub Classroon Workflow-最新的记录-Autograding complete的第二个日志中查看
之前在学习操作系统的文件系统部分时,被国内的部分课本深深“恶心”到了,有幸阅读OSTEP以及一些其他的国外著作(如Unix Internals)并走了许多弯路后我发现一个问题:对于初学者来说,操作系统这种非常“工程类”的学科,(入门/初级阶段)应该采取的正确学习思路是用“增量法”,具体来说:就是从一个具有最基础功能的操作系统出发,不断分析现有问题、解决问题从而实现功能上的完善(即增量),这也是符合操作系统发展的历史脉络的。好巧不巧的是,rcore采取的正是这种教学策略:
要是能早点遇到该多好☹…
rcore的文档非常之详细(对比xv6/pintos等),初学者很容易陷入到细节中去,因此在阅读文档/代码前建议先看一下每章的引言(相当于论文的abstract),明确每章要干什么以及代码的大致框架
我自己采用的是(之前别的实验就配置好的)wsl2+ubuntu18.04+vscode+git,具体操作步骤文档内容写的很详细,网上也有很多相关教程
根据文档一步步操作至:出现报错
sudo apt-get install ninja-build
随后继续按照文档操作
最终:
在bashrc文件中配置路径:
不建议花费太多时间,个人感觉比较靠谱的策略就是“迅速掌握一门语言的50%”,剩下的内容现学现用(按需调用)
重点:所有权和生命周期、类型系统
推荐资料:rust语言圣经、清华rust课程ppt
在确定好思路后一定要看一下测试代码再动手coding,有些测试并没有覆盖所有情况,因此一些特别繁琐的情况可以忽略掉(不是本次实验的重点)
锻炼自己快速上手工程类代码的能力
1 | struct TaskInfo { |
任务:新增系统调用sys_task_info
分析:sys_task_info本质上是对参数中用户程序传入的指针指向的结构体赋值,因此本次实验的核心围绕这些值展开,需要思考的点就是:这些值要存放在什么地方?什么时候初始化?什么时候更新?为了获取这些值需要设计几个函数,这些函数哪些只能内部使用哪些是开放接口?
具体思维过程:
tcb
中存储syscall_times
以及进程首次被调度的时间start_time
,在需要时再将他们拼装成TaskInfo
tcb
初始化syscall_times[syscall_id]
用户程序(及first task
)对应的tcb第一次被调用时,更新start_time
(这就需要思考:负责进行任务调度的功能在哪里实现?怎么判断是否为第一次?)任务:重写sys_get_time
和sys_task_info
,增加sys_mma
p和sys_munmap
分析:因为引入了虚拟内存机制,因此sys_get_time
和sys_task_info
系统调用传入的参数中,指针指向的地址是属于用户地址空间的,无法直接在内核地址空间中使用,需要进行转换。sys_mmap
和sys_munmap
手册里描述的有点模糊,实际上通过阅读样例可知,该系统调用的功能就是以页为单位进行分配/回收,而且不会出现复杂的情况(比如跨越area
,部分可回收部分不可…),关键点就是边界条件、port
以及每次分配/回收都要同时操作pagetable
和areas
,需要注意的点就是接口的设计问题(没有暴露出来的内容,需要用接口传参数进去在本地处理)
具体思维过程:
start_time和syscall_times:tcb中添加,new中初始化,run_task切换进程中更新时间,各个系统调用(位于process.rs和fs.rs)中更新次数
task_info:new定义,更新
mmap和munmap:
系统调用次数更新:update_syscall_times
page_table.rs和memory_set.rs和frame_allocator.rs中:一些用于检查的函数
注:
框架分析:思考读一个文件时发生了什么?
easy-fs/src
easyfs 文件系统的整体架构自下而上可分为五层:
easy-fs-fuse
在(linux上的文件模拟出来的)一个虚拟块设备上创建并初始化文件系统
操作系统中对接easy-fs文件系统的各种结构
块设备驱动层
将平台上的块设备驱动起来并实现 easy-fs 所需的 BlockDevice Trait
easy-fs层
借助一个块设备BlockDevice,打开EasyFileSystem文件系统,进而获取Inode数据结构,从而进行各种操作
内核索引节点层
任务:sys_enable_deadlock_detect
分析:银行家算法(即通过对已知数据的计算完成死锁判断)很好实现,关键是在何时/以什么形式记录/更新数据
具体思维过程:
lazy_static
来实现),这涉及了初始化及更新的问题。对于数组的下标,我们用task_id
和sem_id/mutex_id
来区分即可保证唯一性impl
中实现一个new
函数,把数组初始化为0/1,并在tcb
初始化时调用;每次检测后(检测前也可以),我们需要将finish
和work
数组单独初始化一次available
、allocation
和need
数组,其中回收时(sem_up/mutex_unlock
)available+1
、allocation-1
,分配时(sem_down/mutex_lock
),若不能分配,则need+1
,若能分配则available-1
、allocation-=1
。所有情况都需要考虑死锁检测,若检测成功则继续,若检测不成功则返回-0xdead(这里我们的实现不够优雅,需要在检测不成功时对数组恢复,实际上优雅的做法是把数组操作放在检测之后进行)。我们必须在最底层函数(sem_up/mutex_unlock/sem_down/mutex_lock
)中实现,因为一但检测失败我们需要停止后续操作并立即返回。sem_id/mutex_id
需要用参数传递进去,以及为此需要修改trait
…