第一阶段报告
首先感谢活动主办方给了我们学习的机会和学习的资源,感谢助教一个月来的解答疑惑和帮助建立学习环境
初次接触 Rust,是在学校学习操作系统课程的时候。这门课的练习课中,需要用 Rust 语言来完成编程练习。在学习 Rust 之前,我对编程语言的学习并没有很大的热情。因为在我的印象里除了学习第一门编程语言 C 的时候花了些功夫,之后学习的 Java,Python,Javascript 之类的都是熟悉一下语法后就很快上手。当时我看学校里课程的安排也就是大概给学习 Rust 一周的时间,后面就开始做各种练习了,所以我并没从 Rust 这门语言中期待过什么特别复杂的东西。
真正开始学习的时候,发现事情好像并没有我想的那么简单。我发现除了要理解 Rust 的生命周期,所有权等概念意外,还要关注很多底层的细节。慢慢地我就明白了,想要真正理解 Rust 这门语言,我需要深入思考 Rust 的设计哲学,多看看 The Rust RFC Book,收获会非常大。
对于 Rust 这门编程语言的学习,离我最开始接触有一段时间了。这次训练营的第一阶段,其实对我个人来说是对这门编程语言的复习和巩固。
学习 Rust 我感觉没有很难,多读一读 The Rust Programming Language, The Rust RFC Book 和 标准库文档,对于初期学习完全足够。对于 Rustlings,我并不是特别喜欢,对于学习来说帮助并不大,里面的练习对于学习者来说就是难者不会,会者不难。多写多练多交流,有疑惑不要迟疑,打开 Rust Playground多写示例探索,在我看来是最好的学习方法。
对于语言本身那些细碎的知识点,也没做过详细的笔记。也就是在学习闭包的时候写过一篇总结:Rust中的闭包与关键字move
参加这次训练营也是想重新拾起 Rust,并将其运用到实践当中。
我对rust很有兴趣,是通过rust社区来到此训练营,对我而言理解编译原则过于困难,停留在看简单看点语法的阶段,在实践中遇到很多困难。希望经过训练营学习后第一可以熟练使用rust语言开发。然后可以认识更多的伙伴去探讨os相关的架构和知识。在第一阶段训练中,确实让我感到惊艳,每想到rust还有那么多的技巧,方便的训练工具让我巩固了基本的知识。
主要感受体现在:
1. build.rs 之前编程中很少考虑到,训练中保存就会自动检查,确实减少了很多焦虑;
2. 在尝试完成 test.7 时各种摸不到头脑,也是查了很久才知道cargo还可以加`:`增加编译参数;
3. 对使用 unsafe 过于恐惧,感觉失去编译器的说明总觉得会出问题,当然在算法一中也是尝试了好久才完成,不是很理解但是也得到了锻炼的机会,不至于感到害怕; 虽然在我答题的过程有些草率也碰到了一些unsafe知识的困惑,但是也在完成训练的过程中提升了对基础语法的理解,也提升了我对使用rust的信心。
在群里的氛围也是很好,讨论了一些知识,也分享了很多学习资料,相信经过完整的训练,定会有收获。

几年前就萌生了用 Rust 制作一个操作系统的想法, 但 x86 的复杂度又有点让人望而却步, 而当时恰好遇到了 riscv, 恰好遇到了 rCore. 到现在, 终于有了一个可以亲身加入训练营的机会, 在此感谢各位老师和学长们为此的付出
几年前将自己学习 Rust 的过程汇总成笔记写成教程放在 个人博客 上了. 这里只再简单总结一下一些重要概念内容
所有权 (Ownership)
所有权是 Rust 的一个核心概念, 用于确保内存安全. 它规定了一个变量 (或数据) 在任何时刻都有且仅有一个所有者. 当所有者离开作用域时, 其所拥有的数据将自动释放, 防止内存泄漏. 通过移动赋值和不可变 / 可变借用, 得以控制数据的访问和生命周期
泛型 (Generics) 与生命周期 (Lifetimes)
泛型允许创建适用于多种类型的数据结构和函数, 无需重复实现. 生命周期可被视为一种特殊的泛型. 生命周期注解是用来描述引用的有效期的一种机制. 在函数签名或结构体定义中, 通过类似 'a 这样的符号来表示生命周期参数, 并通过上下文推断或显式声明来确保引用不会超过其底层数据的有效期
Traits (特性)
特质是 Rust 中的一种接口抽象, 它定义了一组方法签名, 供实现特质的类型遵守. 通过 traits, Rust 实现了面向对象编程中的多态性.
智能指针
Rust 提供了一系列智能指针类型, 如 Box<T> Rc<T> Arc<T> RefCell<T> 等, 它们分别用于管理堆上的唯一所有权, 引用计数共享所有权, 线程安全的引用计数共享所有权以及在编译时静态检查之外增加运行时借用检查等场景
多线程与异步编程
std::thread 模块相关API. Rust 的 Ownership 和借用规则有助于防止数据竞争和死锁等问题,Mutex, RwLock 等同步原语进一步加强了线程间的安全通信async/await 关键字,提供了高效非阻塞IO的能力. tokio 和 async-std 等库提供了异步编程的基础设施, 包括任务调度, TCP / HTTP 网络编程等除了这些基本语法, 在练习中也实践了手动实现链表, 双向链表, 堆栈, 深度优先和广度优先算法, 排序算法等. 通过 Rustlings 练习, 可以逐步掌握以上各个概念的实际应用, 通过解决实际问题加深对 Rust 编程特性的理解, 并学会如何在实践中遵循 Rust 的安全性和性能原则, 为接下来的第二阶段打下基础, 以构建安全高效的 OS 内核
此外也感谢群内的大佬们, 没事翻翻大佬的聊天记录总能学到新东西 :)
一些学习链接:
在21年下半年的时候,当时想着给前端的某个项目赋能,由于前端项目过于臃肿、代码质量不高,导致了不单单是开发还是维护都提出了巨大的工程量,提升前端的编译速度和开发效率的情况迫在眉睫,无意中发现Rust在webassembly领域是有一地之席的、在前端基建方面具有与生俱来的优势。
所以很单纯的目的就是想利用Rust这门高性能、安全性的语言来提升前端打包的效率。
目前Rust社区里已经有使用Rust为基础开发的前端打包工具Turbopack。
非常自然的我进入到了Rust这门语言的学习。
由于Rust,我是有基础的。所以在第一阶段我可能更加注重的是Rust基础的巩固,同时加深使用Rust语言的算法描述。
学一门语言,除了学习各类语言相同的编程概念外,其中,学习该语言自身的特性和适用场景也是非常重要的。
Rust作为一门系统级编程语言,采百家之长,自身形成一些Rust语言所特有的语言特性,其中变量的所有权和生命周期就是Rust自身语言特性中最璀璨的一块。
👇👇👇现在给大家总结和分享一下我在开源操作系统训练营第一阶段的总结。👇👇👇
在学习Rust的过程中,如果你想从其他语言迁移到Rust,必须要经过一段时期的思维转换(Paradigm Shift)
从命令式(imperative)编程语言转换到函数式(functional)编程语言、从变量的可变性(mutable)迁移到不可变性(immutable)、从弱类型语言迁移到强类型语言,以及从手工或者自动内存管理到通过生命周期来管理内存,难度是多重叠加。
而 Rust 中最大的思维转换就是变量的所有权和生命周期,这是几乎所有编程语言都未曾涉及的领域。
只列出我认为学习Rust这门语言的内容和我入手其他语言的经验之谈。
先把握大体后追踪语言细节。
Rust这门语言最耀眼的设计就在于变量的所有权机制和生命周期,这也是Rust这门语言我认为最具代表性的特征。
它带给了Rust语言最本质或者说最核心的点、就是内存的管理。致使使用这门语言的人要有强烈的内存分布和管理内存的能力,所有权和生命周期是 Rust 和其它编程语言的主要区别,也是 Rust 其它知识点的基础。
1 |
|
动态数组因为大小在编译期无法确定,所以放在堆上,并且在栈上有一个包含了长度和容量的胖指针指向堆上的内存。
在调用 find_pos() 时,main() 函数中的局部变量 data 和 v 作为参数传递给了 find_pos(),所以它们会被放在 find_pos() 的参数区。

按照大多数编程语言的做法,现在堆上的内存就有了两个引用。不光如此,我们每把 data 作为参数传递一次,堆上的内存就会多一次引用。
但是,这些引用究竟会做什么操作,我们不得而知,也无从限制;而且堆上的内存究竟什么时候能释放,尤其在多个调用栈引用时,很难厘清,取决于最后一个引用什么时候结束。所以,这样一个看似简单的函数调用,给内存管理带来了极大麻烦。
对于堆内存多次引用的问题,我们先来看大多数语言的方案:
C/C++要求开发者手工处理:非常不便。这需要我们在写代码时高度自律,按照前人总结的最佳实践来操作。但人必然会犯错,一个不慎就会导致内存安全问题,要么内存泄露,要么使用已释放内存,导致程序崩溃。
Java等语言使用追踪式GC:通过定期扫描堆上数据还有没有人引用,来替开发者管理堆内存,不失为一种解决之道,但 GC 带来的 STW 问题让语言的使用场景受限,性能损耗也不小。
ObjC/Swift使用自动引用计数(ARC):在编译时自动添加维护引用计数的代码,减轻开发者维护堆内存的负担。但同样地,它也会有不小的运行时性能损耗。
现存方案都是从管理引用的角度思考的,有各自的弊端。我们回顾刚才梳理的函数调用过程,从源头上看,本质问题是堆上内存会被随意引用,那么换个角度,我们是不是可以限制引用行为本身呢?
这个想法打开了新的大门,Rust 就是这样另辟蹊径的。
所以所有权机制规定了以下3点来限制引用。
在这三条所有权规则的约束下,我们看开头的引用问题是如何解决的:

原先 main() 函数中的 data,被移动到 find_pos() 后,就失效了,编译器会保证 main() 函数随后的代码无法访问这个变量,这样,就确保了堆上的内存依旧只有唯一的引用。
本质上来讲这就是所有权机制所保证的
其中又牵扯出了移动(Move)和借用(borrow)、引用(Reference)的规则
变量data的所有权被移动给另一个data1了、后续data1的所有权移动到了sum函数中、导致了变量data和data1
都被移动、无法使用。
可以看到,所有权规则,解决了谁真正拥有数据的生杀大权问题,让堆上数据的多重引用不复存在,这是它最大的优势。
1 | fn main() { |
但是,这也会让代码变复杂,尤其是一些只存储在栈上的简单数据,如果要避免所有权转移之后不能访问的情况,我们就需要手动复制,会非常麻烦,效率也不高。
Rust 考虑到了这一点,提供了两种方案:
如果你不希望值的所有权被转移,在 Move 语义外,Rust 提供了 Copy 语义。如果一个数据结构实现了 Copy trait,那么它就会使用 Copy 语义。这样,在你赋值或者传参时,值会自动按位拷贝(浅拷贝)。
如果你不希望值的所有权被转移,又无法使用 Copy 语义,那你可以“借用”数据。
其实,在 Rust 中,“借用”和“引用”是一个概念,只不过在其他语言中引用的意义和 Rust 不同,所以 Rust 提出了新概念“借用”,便于区分。
本质上来将就是不希望所有权被转移、又无法使用 Copy 语义。
打个比方:我拥有一本书,会有以下情况
我把这本书送给了朋友、那我就丧失了这本书的拥有权(所有权)。
我把这本书借给了朋友、朋友不拥有这本书、但有借用,可以修改内容(可变的引用),当然我也可以提醒他不要在我的书上做标记(不可变的引用)。
其中实现Copy trait的语义的意思就是、朋友将这本书拿去复印了、他拥有了复印这本书的所有权、所以我们俩都拥有了不同的所有权。
当然也会有不同的情况发生,比如说这本书是我们俩一起出钱买的,我们俩都应该拥有这本书的所有权。进而Rust引入Rc、Arc、Cell、RefCell等智能指针来实现一个变量会产生多所有权的情况。
第一阶段的基础还是蛮重要、一些使用Rust语言描述的语言细节还是需要多多关注,为我们后面的内容打好基础。
进一步感谢开源操作系统社区的老师和技术人员,感谢你们辛苦的付出🌾🌾🌾。
期待第二阶段的学习、展开新的篇章。
@FinishTime: 2024-04-23 00:35:51
我是一名软件工程专业的大二本科生,曾参与过2023年秋冬季训练营。
在去年的训练营中,我学习了rust,但因为第一次学习,且当时需要准备期中考试和各科作业,没有按时通过第一阶段。
虽然我后续进入了二阶段群,但没有完成第二阶段的实验部分,今年重新参与,打算认真跟进下去。
我按照“rust语言圣经”上的讲解顺序进行学习,分别学习了:
在一阶段过程中,我遇到了一定的问题,并尝试进行解决。以下内容是我认为,不是那么“较为基础”的问题。
在primitive_types2.rs中,注释要求尝试传入unicode字符,来理解rust中的char。
但是,我通过https://emojidb.org/rust-emojis这个emojis网址,拿到了一个emoji:☢️,进行测试,却直接编译出错:
1 | // primitive_types2.rs |
我与队伍里的成员们进行了初步的实验和讨论,确定报错的原因是:“☢️”无法被解析成字符,必须被解析成字符串。那么,问题来了,为什么它无法被解析成字符,而其它的unicode码可以被解析成字符?
首先,我们进行了实验,从emojis网站中寻找了其他的emoji字符,发现大部分的emoji竟然都能被解析成字符。那问题就是出在“☢️”这个emoji上。
我们队内通过查找rust官方的文档,发现,文档中说,unicode码被分为了两大类,一类是UTF-16,一类是UTF-8。
那么,这两大类的unicode字符有什么区别?
我们通过搜集资料、询问ai等方式,得到:
UTF-16码点在0xD800到0xDFFF的范围内
UTF-8码点在0x80到0x10FFFF的范围内
如图
如图
我们的总结是:UTF-8是8位的,其由一个字节进行表示,而UTF-16是16位的,其由两个字节进行表示,很可能是因为“☢️”是UTF-16编码,导致其无法被解析成rust里的“字符char”。
在编程的过程中,我意外发现,在if语句内,通过不写分号的方式进行返回,会产生问题,程序无法编译,见下图:
1 | // structs3.rs |
在遇到这个问题之后,我自己分析不出来为什么会出现这样的错误。于是,我去找队伍里的学长进行咨询。学长在他的教程书里找到了答案。出现这个问题的原因是,rust的但if语句的返回值必然是返回单元(),因此,我不使用return返回true时,破坏了if语句的语法规则,导致出错。具体教程如下:
在第一阶段中,我在我们专业的操作系统实验课程中,使用rust编程,完成了实验内容,具体完成了:
在第一阶段的学习中,我巩固了我们所掌握的rust基础,并开始学习操作系统相关的知识。在这一部分,我还培养了我的思考能力,深入思考了rust的安全与不安全的地方。同时,通过和群友的交流,我也展开了对更多其它知识点的思考,比如“Copy”、“Clone”特征和“Drop”的冲突之处。
最重要的是,我深深意识到:Talk is cheap, show me the code!
目前,我已经开始了第二阶段部分内容的学习,希望可以顺利的完成第二阶段的所有内容。之后参与到第三阶段的项目中,继续提高自己的能力。
@FinishTime: 2024-06-01 01:39
我尝试实现了用os跟踪所有的task,记录他们的虚拟空间和真实空间,当task有需要时,由os分配真实的物理空间,并完成映射。但是,这样导致os的复杂度增加,并且我完成的代码无法通过测试。
我尝试进行了调试,但发现难以判断到底出现了什么问题,所以我只好换用另一种破坏可见性但简单易实现的方法,不需要os再去跟踪所有的task的空间,而是让task直接能够看到自己的真实空间,并且允许task自己执行mmap和munmap。我仅仅把之前为os实现的核心逻辑代码复制粘贴过来,并配置了一些接口,就通过了ci测试。
而在ch6时,我经受了非常大的折磨!平心而论,要求实现的逻辑并不是很复杂,只是需要层层的传递,最后由“easy-fs”进行实际干活即可。但我遇到了两个极为恶心的问题。
第一是出现了“File is shorter than the first ELf header part”,翻译过来就是,文件比文件描述符还短。我通过调试,发现是在测试“ch5_spawn0”的时候,报了这个错误。我分析,这个问题可能的最直接原因只有三个,要么是文件本身就不存在,要么是该文件没被读取进来,要么是该文件被损坏了。我通过查看测试用例,发现该文件是存在的,那就只可能是该文件没被读进来或者该文件已被损坏。分析概率,我严重怀疑是该文件已被损坏了。毕竟,读入文件的代码只比ch5多了2行,我的ch5已经通过了,况且读入文件的底层实现不是我做的,那很可能不是读入文件的时候出现的问题。最有可能就是我写的“在可以释放内存时进行内存释放”的操作有问题,导致损坏了其他的文件,比如这个文件。
但是,我翻来覆去的看我的代码,应该是没有问题的,如果它存在问题,那其它涉及释放内存的地方怎么就没事呢?如果是偶然情况,那也不可能我每次进行测试,它都是这里崩溃。我开始怀疑,是不是我其它地方的代码写的有问题?所以我去重新检查我所有的代码,并且感觉没有什么问题。这该怎么办?迫于无奈,我将代码回滚到了初始版本,打算重新写。但就在我重写完成后,执行测试,还是那个问题!
又经历了一天的苦思冥想,我决定向万能的群友们请教。在群友们的热心帮助下,我隐约找到了解决方案:
如图
我按照文档,将easy-fs的cache部分改正,果然就没有这个问题了。
(吐槽:easy-fs的cache关我什么事啊?也没说要改他啊…这个问题卡了我大约3天)
但是刚刚那个只是第一个问题!还有一个很坑的问题,我在解决上一个之后,又遇到了“IoError”!报错提示为“VitBlock:IoError”,真是奇怪,怎么还出现IoError了?我再次尝试了大量的解决方法,各种寻找可能的问题,但还是不知道原因。但是,我突然发现,在LOG=TRACE或LOG=DEBUG下,我执行测试,居然没有报错!这是为什么?我意识到了一个可能的关键:时间!在使用LOG时,程序的运行时间会变慢!这可能就给os留足了时间,使其能够完成从存储中的加载,就不会引发错误。而不使用LOG时,可能就因为时间过短,导致os来不及完成加载,从而引发IoError。但这是为什么呢?经过分析,我定位到了出错的核心位置,那里我使用的是if let 语句。后续,我将其改成了let之后再进行match,就解决了问题。
这激起了我的兴趣:通过上述现象,难道说if let是一个异步非阻塞的语句吗?我请教了我的学校里的老师,但老师也不清楚。我尝试在网上找一些能够对其“是否异步”测试的方法,但都没有找到。最后只能暂时放一放,先干3阶段吧。
rCore真是要命,可算过来了。
三阶段,我选择的是项目8,在之前的旁听过程中,老师留了一些作业,因为之前弄rCore,没有管这部分,我会在后续完成这部分。
明天还有作业要做。。。两科呢
提供的 Rust 代码实现了一个通用的二叉堆,支持最小堆和最大堆的功能。下面是这个实现的核心组成部分的详细解释:
结构定义 (Heap<T>):
T: Default:这个约束确保堆中的元素可以被初始化为默认值。这对于确保堆的向量(items)可以在索引0处有一个占位符,简化索引计算非常有用。count:跟踪堆中的元素数量。items:一个向量,存储堆元素。堆使用基于1的索引以简化父子计算,索引0保持为虚拟。comparator:一个函数指针,用于确定元素的排序,允许灵活定义最小堆或最大堆。构造函数 (new(comparator)):
堆操作:
add):在堆的末尾插入一个新元素,然后将其向上移动以维护堆属性。这是通过比较添加的元素与其父元素并根据比较函数进行必要的交换来完成的。next in Iterator implementation):移除根元素(根据堆的类型是最小或最大),将其替换为堆中的最后一个元素,然后将此元素向下移动以维护堆属性。在此过程中,堆的大小减少一个。辅助函数:
parent_idx, children_present, left_child_idx, right_child_idx, smallest_child_idx:这些辅助函数用于计算父和子的索引或检查子节点的存在,这对于在添加和删除操作期间导航堆至关重要。类型特定的堆构造器 (MinHeap 和 MaxHeap):
a < b,对于最大堆是 a > b)轻松实例化 Heap<T> 的方法。测试 (tests module):
Default 的任何类型 T,并可以使用在实例化时传递的任何比较函数,使其可以适应不同的排序需求。next 方法的 Iterator 特质,允许堆在标凈 Rust 习惯用法中使用,如循环和其他基于迭代器的操作。