#第一阶段
开营前提前刷了下 rustlings,虽然这次练习代码不是最新的 rustlings,但考察内容是一样的。除此之外本次练习还扩充了一些编译和数据结构的题目,数据结构的题目写起来还是有点卡。不说了… LeetCode 走起。
作为想了解操作系统小白一枚,希望能跟完全程吧(🤔)。
#第一阶段
开营前提前刷了下 rustlings,虽然这次练习代码不是最新的 rustlings,但考察内容是一样的。除此之外本次练习还扩充了一些编译和数据结构的题目,数据结构的题目写起来还是有点卡。不说了… LeetCode 走起。
作为想了解操作系统小白一枚,希望能跟完全程吧(🤔)。
因为学长的推荐,正准备自己开始做rcore lab的时候突然在rcore的官方repo里面看到news:
开源操作系统训练营报名!
wow,看到里面的正是自己想要了解学习的内容,一下子打起了12分精神,感觉很切合自己所在的嵌入式方向,并且完美的满足自己想要在更深平台上学习的想法(之前是在stm32的机器上跑过简单的ucOSII 实时操作系统)。
感觉自己花在学rust的时间挺长的,主要是想更深入的学习这个语言(正巧大二上学了编译原理),在rustlings上花了不少时间,不想一个个说语法了,只是记得smart_pointers的特性很有意思狠狠的理解了,当然还有所有权(第一次见到在编译阶段去强调这个概念的语言,之前写malloc实验的时候有想过能不能在写语言的时候把内存的管理考虑好),option之类的东西和c++真的很像,前面的智能指针也是c++那一套的东西(有种写cs144的感觉)。范型的使用我就类比之前学java的时候的用法了,让我记忆深刻的还有rust对于错误处理包装成一个enum,居然是个枚举,还有它的宏,也太多了吧(学c的时候确实体会过宏的强大)。
最后10个algorithms花了小半天写完,确实算是对之前的学习合起来应用了一下。
记录的笔记我就留在个人博客上了,因为用的notion写博客,试试推送很方便,所以习惯了:
从做实验说起,做rcore实验的周期是起伏的,在5.1假期冲刺完成了4个lab,之前4.28号完成第一个lab。但是当时做的策略从最开始先把全部文档(tutorial3.6的)看完再做实验,变为先扫一遍guide文档,然后做lab,遇到不懂的去翻guide文档,这样才把速度提上来,因为当时也只有5.1假期是完整的时间能做rcore,后面被期末考试割得支离破碎,然后接下来每天有时间细读一下文档,再把前面跳过的问答题补上,就完成了这个整个第二阶段的要求。
对于看的每个chapter以及lab,我做了记录,集中在这里:2024春夏OS训练营
第一反应是真的觉得学到很多,学校里面讲的内容只有概念,对于代码,涉及到的也只有linux提供的部分系统调用的使用而不是实现。而从rcore这部分的实验,特别是tutorial里面讲的内容,最开始看链接脚本的配置,bss段的清空,我之前是从来没有在完整的计算机平台写这些的,只有在stm32开发板,移植ucOSII实时操作系统的时候,写过一部分,但是单片机需要考虑的内容和完善的一个计算机应该考虑的内容还是相差较远,而且这次是拿rust进行OS开发,对我之前学的rust基础部分是很大提升。想想makefile也用的更熟练了,以前很是不喜欢命令行gdb调试,总要配配vscode的gdb(launch.json)来用用图形化,现在配合dashboard加上写.gdbinit脚本,流畅的使用gdb能够在调试中得到更多信息(不过当然还是想以后找个方法对于这种情况也能上vscode的gdb插件)。
从批处理操作系统开始,到初步的一个分时操作系统,来到ch3的部分,对系统调用的流程更加清晰,cpu硬件和os的配合(特权级别的切换,内核栈、用户栈的切换,sscratch寄存器的使用),系统调用的认识就变成了一个软中断。
感觉难起来的就是地址空间的出现,而且这个就是我特别特别想自己写代码的部分,因为自己嵌入式这边用的单片机(stm32f401re)是没有mmu的,对于课上提到的地址虚拟化这部分的认识,完全是概念上的,只会做题,算页表占用、分配。但是在rcore的实验里面,尤其是读tutorial,riscv的satp CSR用于管理MMU的配置,设置根页表所在物理号,还有我们这里采取的”双页表”地址空间设计,内核和用户程序都各自拥有地址空间,trap的时候也会伴随地址空间切换到内核的,随之而来的在切换部分实现地址平滑过渡的trampoline(跳板页)以及因为只有一个sscratch所以把内核栈设置到用户空间,这些设计细节,远远超过了平时课上的那点概念认识,也让我对原来很陌生,很迷惑的分页机制有了具体的认识。而且这里面rust的代码的书写风格也给了我很深刻的印象,利用rust的生命周期管理,不用时时刻刻都自己去写drop(因为真的很容易就搞忘或者弄混乱了),而是在设计数据结构的抽象的时候,就反映这些依赖关系,交给编译器去放置drop(大部分)。
后面就是引入了进程的完整概念(体现到数据结构),这里对于僵尸进程有一个初步的认识(不过后面加上线程过后这个僵尸进程的完整效果才展现出来),有了进程,我们顺其自然的再加上文件系统,这样从文件中加载进程执行代码,开始有了一个现代分时操作系统的雏形。文件系统虽然是给的一个easy实现(只有根目录一个目录),但是我觉得扩展出其它目录好像也并不困难(因为两者的核心内容是一样的):超级块、索引节点位图、索引节点区域、数据块位图、数据块区域。不过之前UCOSII实时操作系统里面也接触过位图(它的任务调度很依赖于位图),把自己想成计算机,想想给一个文件名,怎么找到它所在的数据块或者如何为它创建一个数据块保存在文件系统里面,就能理解每个部分的作用。
最后就是到线程的引入(前面还有进程通信,不过那部分没看的很仔细),实验实现了一个进程里面线程的死锁检测,这里让我影响最深的还不是算法实现,实现其实很简单,但是测试用例的设计反而让我记忆很深刻,我在想我有时候就需要设计一个死锁的例子,但是总是有时候马上不能想到一个比较好的例子(想出来的都是很简单的而且不一定能真死锁的(有时候执行不产生死锁)),所以分析测试用例的时候还给了我很多思路。
老实说,参加这次的训练营还算是运气,正好在一阶段报名最后一天看到这个训练营,自己本来是听学长介绍,打算做rcore实验来弥补课内动手的不足,结果正好发现这里有个训练营而且会做rcore,于是在看到的第一时间就报名了,直到现在,我依然觉得这是我幸运的一点,没有这种有些紧张的氛围,以及日程安排的指导,我肯定没有这么强的动力在这么短的时间完成。也很感谢和我一起做题的室友,和他们讨论的时候让我学到了更多内容。
我其实在去年的时候就对训练营略有耳闻,但是由于在准备算法竞赛,并且对未来研究生的方向还没有一个明确的规划,所以没有参加。而现在,我基本上已经确定了未来要往体系结构和操作系统方向发展,所以第一时间便报名参加了训练营。
第一阶段学的内容是 Rust。Rust 也是我很喜欢的一门语言,因为他性能高、内存安全、又有良好的包管理器支持,相比于 C、C++,它的语言表达能力更加强大,也有很多好用的语法糖;而相比于 Java、Python 等,它又更为严谨、更贴近硬件。
经过这一段时间的学习,我越学越觉得 Rust 的很多特性其实是为了给他严格的所有权机制打补丁。最明显的就是生命周期了,还有诸如 unsafe
等。刚好最近同时也在一家公司实习做操作系统内核开发,正在使用 C 语言,因此对这两门语言的风格深有所感:
unsafe
。在 Rust 身上可以找到很多为了弥补所有权机制而设计的语法,因此在学习的时候才会觉得 Rust 的语法很复杂。不过这种「语言规定好的规范」对于多人之间的项目合作,特别是开源来说,就是一种优势了。相比于 C 语言项目之间可能存在代码风格相差巨大的情况,Rust 写出来的代码基本上不会有太大的风格差异,这样在参考别人的代码,以及贡献代码的时候就会更为轻松。
第二阶段开始进入操作系统领域的学习了。训练营采用的是清华大学的 rcore 教程,不过相比原版的 rCore-Tutorial-v3 有了很大的变化,删去了很多不必要的任务,任务的指引也更加清晰,教材方面也根据代码做了很多简化。整体上是更容易上手了。
在这几章的学习内容中,我认为最难理解的是虚拟地址页表那一章。一开始我一直困惑于“操作系统到底如何分配内存,如何知道地址是否合法的”,后来经过了解才知道,虚拟地址其实是软件和硬件共同实现的,操作系统需要做的是维护页表,而 cpu 会利用操作系统维护的页表,通过硬件来判断地址是否合法,以及将虚拟地址转换为真实的物理地址。所以整个计算机的发展软硬件是不可分开的。
在完成了第二阶段的学习后,我和队友便着手开始了计算机系统能力大赛的开发。一开始我们是打算基于 arceos 进行开发的,但是了解后才发现 arceos 似乎没有提供一个基础的内核框架,而只是提供了很多的“组件”,而我们此时仅剩下不到两周的时候,根本无法在这么短的时间从头自己写一个内核出来,因此最后还是选择基于训练营第二阶段结束后的 rcore 版本进行开发。
也是得益于训练营阶段对 rcore 代码深入学习的经历,让我们在两周不到的时间里,完全原创地完成了操作系统比赛初赛的赛题。接下来是复赛,我们打算继续基于初赛的代码,加入虚拟文件系统、无栈协程等特性,为复赛做好准备!
第三阶段我选择了项目二《ArceOS 宏内核》,原因是我正在参加今年的计算机系统能力大赛(操作系统内核实现赛道),正好项目二和我们的比赛内容较为贴近,可以相互之间参考与借鉴一下。
ArceOS 是 unikernel。其特点是模块组件化,这样在构建操作系统镜像的时候,就可以选择性地编译模块,从而达到尽可能减少系统镜像体积的目的,对嵌入式等低性能的场景有很大的帮助。
但是 unikernal 的缺点是没有用户态和内核态的区分,也就意味着应用程序的权限管理会对内核的稳定性有很大的隐患。其次,ArceOS 目前只支持一个应用程序,不能作为一个通用的内核来使用。
因此,项目二的目的就是为了让 ArceOS 支持用户态和内核态、支持多应用的并发执行,向宏内核靠拢。
rust第一阶段总结:
第一阶段主要是对rust语言进行一个系统的学习,并进行rustlings习题的完成,其实前一百道题还好,对于我来说更像完形填空一样,对我来说后十道算法题比较有难度
本来算法就比较薄弱,再用不熟悉的语言进行数学,挑战还是相当大的(,不过在第一阶段中,我对于rust的理解有了深一层的概念。相信可以在第二阶段更进一步
#前言
这并不是我第一次参加了,正如我去年结营仪式上说的,我今年又来了。时光匆匆,下半年就要考研了,目前我依然在猛刷各种竞赛,希望今年能更上一步,有所收获!
#第一阶段的总结:
由于之前已经参加过了,因此前100题非常轻松的就刷掉了,今年多出来了10题算法题,我一直很喜欢用rust写算法,写起来是真的爽,llvm的优化也很爽,甚至能硬生生的将$n^2$的算法给优化成$n*log(n)$的级别,非常逆天。这10道算法题我只在第八题上卡了一会,疑似是workflow评测的bug?反正我本地跑的单元测试都能通过,最终还是整个重写了一遍才过,非常奇怪。于是顺利的在开始的第二天早上写完了110道题。
战斗,爽!
rust 有趣捏
关于所有权的规则:
貌似没涉及,但是觉得好玩捏
谢谢THU捏
太喜欢rust 了
二阶段的收获确实很大,以前也有写过mit的xv6 lab,看过很多os的书籍,但是从rcore中还是学到了很多。我认为rcore的长处是
但是我认为,rcore的练习对我不是那么有吸引力,毕竟在os上修修补补已经是常见的lab手段,我觉得文档,构建过程才是真的精髓。如过可以在文档中空一些代码让学生补全,直接参与构建os,相比会更有吸引力。但是我理解这样做难度肯定上升,不太好让基础不劳的学生学习(这样做的话,我这个ddl战士应该也写不完了)。
总而言之,我认为rcore真的很棒,我认为是比肩mit了,感谢写rcore的人士,您们真牛。
日程上,很羞愧,五个lab在三天中赶完,所以感觉也不必要详细记录了,但是可不是我只学了三天,在训练营开始之前我就偶然发现了rcore的文档,基本看了一边,之前也有尝试顺着rcore的教程从头写os(最后只写到第5章。。)感觉还是有点抱歉的,很想参与训练营更多的课程,但是最近学校的事情多,只好做ddl战士了。
首先非常有幸参加了本次开源操作系统训练营,感谢诸位讲师的无私奉献和合作平台的支持,提供了一次深入探索操作系统内部机制与Rust编程语言应用的宝贵机会.在近期的学习中,我深入了解了Rust程序设计语言,主要参考了《Rust程序设计语言》和rust圣经以及训练营课程。
对rust语言初步了解,能使用rust语言完成rustlings练习
Rust基础语法与特性:详细学习了Rust的基本语法结构,包括变量、数据类型、控制流、函数等。重点掌握了Rust的所有权系统、借用检查器和生命周期等核心特性,这些特性确保了Rust的内存安全性。
并发与异步编程:学习了如何使用线程、协程和异步I/O进行高效并发处理。深入了解了互斥锁、通道等如何用rust进行实现。
模块与包管理:了解了如何创建模块和包,对Cargo管理项目的依赖和构建过程有了初步的了解。
1.rust的所有权系统和借用检查器有效地避免了空指针解引用、数据竞争等常见的内存错误,和以往学过的c语言相比减少了因程序员的疏忽而导致的bug
2.rust采用多线程与 async/await 相结合,使用复杂度换取可控性和性能,通过对锁和atomic的了解,为接下来在操作系统中的lab实现打下了坚实基础
3.理解了rust的生命周期机制,有助于避免悬垂引用等内存安全问题,写出安全的代码
1.对理论知识只是粗浅的了解,写练习时仍需翻阅资料
2.知识遗忘速度过快,只是有了全体框架,具体内容往往停留纸上谈兵,无法学以致用
3.做练习只是听从编译器的指挥,无法自主解决问题
1.在进行第二阶段的学习过程中不断复习学过的知识避免遗忘
2.根据oj题目,使用rust语言完成练习,加深对语言的印象
3.重写rustlings,不再是简单的通过,再完成的同时注释实现题目的思路。
关于const:
Rust的常量必须加类型。那为什么常量不推导呢? 这是因为故意这样设计的,指定类型就不会因为自动推导类型出问题。
static, const, let 声明变量有什么区别? - Rust语言中文社区
1 | // variables6.rs |
数组(array)的三要素:
数组必须要初始化,以下这种写法会报错:
1 | fn main() { |
改为这样就编译通过:
1 | fn main() { |
iter_mut():
iter_mut() 创建一个可变引用迭代器。当你想要修改集合中的元素时,应使用 iter_mut()。iter_mut() 返回的迭代器将生成集合中每个元素的可变引用。
1 | let mut v = vec![1, 2, 3]; |
1 | // move_semantics2.rs |
方法1:
将vec0的内容clone一份传进函数,然后返回值的所有权交给vec1,此时vec1=[22, 44, 66],vec0=[]
然后再把vec0的内容clone一份传进函数,然后返回值的所有权交给vec0,此时vec1=[22, 44, 66],vec0=[22, 44, 66]
这个时候不管是vec0还是vec1都拥有一片自己的堆上的空间,二者互不相关,因此vec1.push(88)只会改变vec1的值,且vec0的值也还存在
1 | fn main() { |
方法2:
首先,创建了vec0的一个可变引用&mut vec0,将这个可变引用传入函数,函数接受一个可变引用类型,然后对其进行操作,也就是操作了vec0指向的那片堆,因此在函数内部,vec0就已经变成了[22, 44, 66]
然后最后返回vec.to_vec(),相当于又创建了一个新的vec,作为vec1绑定的值,因此vec1和vec0又变成了互不相关的
1 | fn main() { |
这个的意思就是函数不再接收参数,而是直接在里面新创建一个包含[22, 44, 66]的vector返回
1 | // move_semantics4.rs |
结构体,有几点值得注意:
需要注意的是,必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。
结构体方法:
Unlike functions, methods are defined within the context of a struct,and their first parameter is always self
, which represents the instance of the struct the method is being called on.
we still need to use the &
in front of the self
shorthand to indicate that this method borrows the Self
instance, just as we did in rectangle: &Rectangle
. Methods can take ownership of self
, borrow self
immutably, as we’ve done here, or borrow self
mutably, just as they can any other parameter.
方法参数里面不止&self:package.get_fees(cents_per_gram)
比如这个,get_fees是结构体的方法,它在结构体里面是这样定义的:
1 | fn get_fees(&self, cents_per_gram: i32) -> i32 { |
也就是说,第二个参数跟在&self后面就好了,在外部调用结构体时只需要传入那个另外的参数
例子:
1 | // structs3.rs |
更为复杂的枚举:
Move:包含了一个匿名结构体
Echo:包含了一个String
ChangeColor:包含了三个整数
Quit:没有关联任何数据
1 | enum Message { |
模式匹配和模式绑定
1 | // enums3.rs |
字符串的字面量是切片
一般写字符串可以这样写:let s = "Hello, world!";
实际上,s
的类型是 &str
,因此你也可以这样声明:let s: &str = "Hello, world!";
String转&str:取引用即可
1 | fn main() { |
&str转String:"hello,world".to_string()
或者String::from("hello,world")
String的操作(必须把String声明为mut)
由于 String
是可变字符串,因此只有String
这种字符串可以被操作更改
push()
,在末尾追加字符;push_str()
,在末尾追加字符串
这两个方法都是在原有的字符串上追加,并不会返回新的字符串,即返回值是()
insert()
方法插入单个字符,insert_str()
方法插入字符串字面量
也是在原有的字符串上面操作,没有返回值
replace
该方法可适用于 String
和 &str
类型
该方法是返回一个新的字符串,而不是操作原来的字符串
1 | // strings3.rs |
问题1:
关于hashmap的更新实例中:为什么修改count的值就可以修改hashmap的键值呢?
1 | use std::collections::HashMap; |
解答:
map.entry(word)
返回了一个 Entry
枚举类型的值,该枚举有两个变体:Occupied
和 Vacant
。Entry
枚举表示 HashMap
中某个键对应的条目。
当调用 or_insert
方法时,如果 word
对应的条目已经存在,则 or_insert
方法会返回该条目的值的可变引用(Occupied
变体),如果该条目不存在,则会在 map
中插入一个新的键值对,然后返回新插入的值的可变引用(Vacant
变体)
问题2:
这道题里面有这样一个语句:
1 | **let mut basket = get_fruit_basket(); |
其中,basket是这样来的:
1 | fn get_fruit_basket() -> HashMap<Fruit, u32> { |
所以,为什么basket已经是一个hashmap了,还要用*解引用呢?
解答:
*解引用解的不是basket,而是basket.get(&Fruit::Apple),因为get会返回一个指向该条目键值的引用
跟这段代码一个道理:
1 | let mut scores = HashMap::new(); |
or_insert()会返回一个指向该条目键值的引用,因此也需要把它解引用来跟5比较
这道题主要注意Append这个命令:迭代器是向量中每个元素的引用(想想这是肯定的,不然在for循环里面所有权都丢失了的话有点太危险),而这种引用默认就是不可变引用,那么使用String类型的push_str()自然就是要报错的,因为这个方法是改变原字符串而不是返回一个新的字符串。
1 | // quiz2.rs |
主要是if let语句和while let语句的使用
首先,Option实际上是一种枚举类型,包含两个枚举成员:
1 | enum Option<T> { |
所以下面代码中的if let Some(word) = optional_target {}实际上就是将optional_target这个枚举类型的值结构到word上,然后进行一个操作
while let语句也是一个道理,由于optional_integers的元素都是Some类型的变量,因此首先要把optional_integers的值解构到Some(integer)上,然后再进行操作,因此出现一个Some包裹一个Some
1 | // options2.rs |
关于if let的详细讲解
if 和 if let 表达式 - Rust 参考手册 中文版
ref和&:
1 | let c = 'Q'; |
1 | // options3.rs |
关于errors:
关于parse:
parse返回的是一个Result枚举类型,成功的话将会是Result::Ok(),失败是话是Result::Err,因此想获取parse成功时返回的正确值,需要将返回值unwrap()
1 | fn main() { |
认识到Result这个枚举类型:[Result](https://rustwiki.org/zh-CN/std/result/enum.Result.html)
是 [Option](https://rustwiki.org/zh-CN/std/option/enum.Option.html)
类型的更丰富的版本,描述的是可能的错误而不是可能的不存在。
而做题的时候这个例子,返回的错误类型是std::num::ParseIntError
1 | // errors2.rs |
还可以用?:
1 | pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { |
在函数里面使用?:
因为?的逻辑是这样的:
如果返回的不是Err,就正常运行并且将Result枚举类型的值结构给cost
但如果返回的是Err,函数就会立刻终止,然后返回一个Result::Err类型的值,因此必须给函数设置返回值
1 | // errors3.rs |
但是在main函数里面返回一个Result::Ok(String::from("implement success!"))
是不合法的,因为main
函数通常有两种有效的返回类型:()
(表示成功)和Result<(), E>
(表示成功或出现错误)。main
函数的返回类型必须实现Termination
trait,该trait规定了程序的终止行为
可以像这样用匹配:
1 | fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> { |
但感觉这里也可以直接if判断:
1 | if value > 0 { |
转换错误类型:将标准库定义的错误类型转换为自定义的ParsePosNonzeroError
1 | // errors6.rs |
关于map_err:
代码:
1 | // 处理parse()出错的情况 |
s.parse()
尝试将s进行转换:map_err()
就会返回ParsePosNonzeroError::from_parseint
这个自定义的错误类型然后直接返回,而不是ParseIntError
map_err()
就会返回一个Result::Ok(value)作为s转换后的值——因此后面一定要加?,不然出错的情况确实可以正常返回,但是没出错时Result类型的值是无法与i64类型的x绑定的,而?可以将正常返回的值直接unwrapPositiveNonzeroInteger
类型的结构体,x的值作为结构体的值,然后调用结构体方法new()ParsePosNonzeroError::from_creation
这个自定义的错误类型然后直接返回,而不是CreationError
使用泛型
1 | // generics2.rs |
特征的多重约束
1 | // traits5.rs |
关于格式化字符串format!:并不是所有的泛型都可以用这个函数,必须要有Display特征的泛型才可以,因此使用T: Display这个特征约束
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
像这段代码的意思就是:约束T必须具有特征Summary,且item1和item2都是T类型的引用
1 | // quiz3.rs |
关于结构体的生命周期:
不仅仅函数具有生命周期,结构体其实也有这个概念,只不过我们之前对结构体的使用都停留在非引用类型字段上。细心的同学应该能回想起来,之前为什么不在结构体中使用字符串字面量或者字符串切片,而是统一使用 String
类型?原因很简单,后者在结构体初始化时,只要转移所有权即可,而前者,抱歉,它们是引用,它们不能为所欲为。
既然之前已经理解了生命周期,那么意味着在结构体中使用引用也变得可能:只要为结构体中的每一个引用标注上生命周期即可:
1 | struct ImportantExcerpt<'a> { |
结构体 ImportantExcerpt
所引用的字符串 str
生命周期需要大于等于该结构体的生命周期。
迭代器:
迭代器是会被消耗的:
当使用了next(),迭代器就会被消耗,相当于取出了那一个元素,那个元素其实已经不在迭代器里面了,因此最开始我直接将first转换为大写后直接返回了迭代器剩余的元素,这会导致返回的字符串没有预期的第一个字符
1 | // Step 1. |
用迭代器实现递归:使用消费者适配器的方法
1 | pub fn factorial(num: u64) -> u64 { |
在 Rust 中,(1..=num)
是一个范围(Range)表达式,表示从1到num
(包括num
本身)的一个范围。这个范围实际上是一个迭代器,它可以生成从1到num
的所有数字序列。
在这种情况下,我们可以通过 (1..=num)
创建一个包含从1到num
的数字序列的迭代器。然后,我们调用迭代器的 product()
方法,这个方法将迭代器中的所有元素相乘,得到它们的乘积作为结果。
因此,(1..=num).product()
这个语句的意思是:先生成从1到num
的数字序列迭代器,然后计算这个序列中所有数字的乘积,最终得到阶乘的结果。
关于闭包和迭代器方法的使用
不用循环,找出map里面有多少个键值为value的元素
1 | fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize { |
.values()
:使用了HashMap的value方法,获取了一个包含所有键值的迭代器。
pub fn [values](https://rustwiki.org/zh-CN/std/collections/struct.HashMap.html#method.values)(&self) -> [Values](https://rustwiki.org/zh-CN/std/collections/hash_map/struct.Values.html)<'_, K, V>
一个以任意顺序访问所有值的迭代器。 迭代器元素类型为 &'a V
,即对键值的引用
.filter()
:创建一个迭代器,该迭代器使用闭包确定是否应产生元素。给定一个元素,闭包必须返回 true
或 false
。返回的迭代器将仅生成闭包为其返回 true 的元素。
.count()
:消耗迭代器,计算迭代次数并返回它。此方法将反复调用 [next](https://rustwiki.org/zh-CN/std/iter/trait.Iterator.html#tymethod.next)
,直到遇到 [None](https://rustwiki.org/zh-CN/std/option/enum.Option.html#variant.None),
并返回它看到 [Some](https://rustwiki.org/zh-CN/std/option/enum.Option.html#variant.Some)
的次数。 请注意,即使迭代器没有任何元素,也必须至少调用一次 [next](https://rustwiki.org/zh-CN/std/iter/trait.Iterator.html#tymethod.next)
。
返回一个所有哈希表里面键值为指定状态的数量和:
1 | fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize { |
.map()
:Iterator特征的函数,获取一个闭包并创建一个迭代器,该迭代器在每个元素上调用该闭包。.sum()
:将迭代器里面的每个元素相加,所以这么写也是对的:1 | let iter = collection.iter().map(|map| count_iterator(map, value)); |
Box堆对象分配 - Rust语言圣经(Rust Course)
1 | // 使用Box来定义Cons,将List存到堆上,那么List的大小就固定了 |
1 | fn main() { |
线程返回值是如何得到的?
在 handle.join().unwrap()
中,join()
方法会等待线程执行完成并获取线程的返回值,即每个线程的执行时间(以毫秒为单位),然后通过 unwrap()
方法将其取出并存储在 results
向量中。
线程编号和时间是如何输出的?
results.into_iter().enumerate()
: into_iter()
方法将 results
向量转换为一个拥有所有权的迭代器,enumerate()
方法对迭代器进行索引迭代,返回一个元组 (index, value)
,其中 index
表示元素在迭代器中的索引,value
表示元素的值。
为什么这里需要克隆发送方?
1 | fn send_tx(q: Queue, tx: mpsc::Sender<u32>) -> () { |
my_macro
宏定义前加上 #[macro_export]
属性,使得宏可以在模块外部使用1 | pub mod macros { |
模式匹配:
$()
中包含的是模式 $x:expr
,该模式中的 expr
表示会匹配任何 Rust 表达式,并给予该模式一个名称 $x
$x
模式可以跟整数 1
进行匹配,也可以跟字符串 “hello” 进行匹配: vec!["hello", "world"]
$()
之后的逗号,意味着1
和 2
之间可以使用逗号进行分割,也意味着 3
既可以没有逗号,也可以有逗号:vec![1, 2, 3,]
*
说明之前的模式可以出现零次也可以任意次,这里出现了三次匹配一次:
1 | #[rustfmt::skip] |
匹配多次:
1 | #[rustfmt::skip] |
在裸指针 *const T
中,这里的 *
只是类型名称的一部分,并没有解引用的含义
下面的代码基于值的引用同时创建了可变和不可变的裸指针,创建裸指针是安全的行为,而解引用裸指针才是不安全的行为 :
1 | let mut num = 5; |
1 | pub fn merge(list_a:LinkedList<T>,list_b:LinkedList<T>) -> Self |
因为用到了clone,因此必须限定T是具有Clone特征的泛型
方法一
std::mem::replace(self, reversed_list);
交换新链表和self的所有权方法二
1 | impl<T: Clone> LinkedList<T> { |
其中while循环那段可以这样改:
1 | while let Some(node_ptr) = current_node { |
因为要递归实现插入和查找,所以应该将search和insert实现为TreeNode的方法,所以TreeNode方法的实现应该要写在最前面
二叉树节点的方法:
1 | // 此时self是二叉树里面的一个节点 |
1 | fn search(&self, value: T) -> bool { |
二叉树的方法
1 | // Insert a value into the BST |
1 | // Search for a value in the BST |
1 | // 坑:size记得-=1 |
1 | fn bracket_match(bracket: &str) -> bool |
1 | // 出队,出第一个元素 |
1 | pub fn push(&mut self, elem: T) { |
1 | impl<T> Heap<T> |
1 | fn next(&mut self) -> Option<T> { |
用到了get_mut()
1 | // 无向图结构的邻接表,用HashMap来存储,键是String,键值是一个向量,其中i32可能表示边长 |
1 | // 添加成功返回true,添加失败返回false |
唠嗑一番:在此次开源操作系统集训营之前,我实际上已经参加过若干次了。最早的时候,是我对象在2022年看到了集训营的开营通知,并在GitHub的issue上回复来报名。然而,由于我之前没有留意到相关信息,错过了过去好几期的机会。这一次也算做是自己重来的机会吧。不过说起来,这个rustlings的确是我从去年一直做到了现在。今年直接新增10道数据结构题, 本fw直接没法也就上了亿点点unsafe, 好在之前有经验,于是三天冲刺完成,就有了本文之第一阶段总结。
Rust何物者也?一个令人安如磐石的C++ Promax (钟离老爷子看后点个赞) ,胜任WebAssembly、普通命令行应用乃至操作系统编写。语法漂亮,match与macro非常赞。具有成体系的包管理器,安装导入新模块轻松如Python,rust-analyzer也很赞,提供比Pylance更好的高亮提示。运行效率也可以,看似充满了函数,其实llvm优化的也挺不错(
算啦,后面是正经时间,将展示本人写的代码示例和正经点的Rust学习感想,再次感谢清华大学开源操作系统集训营!
1 | struct Edge { |
有了Rust这样一门强大而复杂的编程语言,我的学习之旅也就充满了挑战和收获。在学习Rust的过程中,我深刻体会到了其独特的设计理念和功能特性,这些特性使得Rust在系统编程领域有着独特的优势和价值。
首先,Rust的内存管理机制让我印象深刻。通过所有权(ownership)、借用(borrowing)和生命周期(lifetime)等概念,Rust实现了内存安全和线程安全,避免了常见的内存错误和数据竞争问题。虽然这些概念在开始时让我感到有些困惑,但通过不断的实践和阅读文档,我逐渐掌握了它们,并开始感受到它们带来的好处。与其他语言相比,Rust的内存管理机制让我感到更加自信,因为我知道我的程序不会因为内存错误而崩溃。
其次,Rust的模式匹配(pattern matching)和错误处理机制让我眼前一亮。模式匹配不仅可以用于解构数据结构,还可以用于控制程序的流程,使得代码更加清晰和易于理解。而错误处理方面,Rust引入了Result和Option等枚举类型,强制程序员处理可能出现的错误情况,避免了传统的异常机制带来的一些问题。学习如何利用这些特性编写健壮的代码是我学习Rust过程中的一大收获。
此外,Rust的并发编程支持也给我留下了深刻的印象。通过标准库提供的基于消息传递的并发模型,以及原子类型和同步原语等工具,Rust让并发编程变得更加安全和容易。我曾经尝试用Rust编写多线程程序,并发现在Rust的帮助下,我可以更加轻松地管理线程之间的通信和同步,避免了常见的并发错误。