0%

前言

2022年在和同事聊天的时候了解到了Rust,在他的推荐下“投资未来”,学习了Rust这门语言。前期确实困难重重,Rust的
学习曲线确实比较陡峭,在三本Rust著作(《Rust权威指南》,《Rust实战》,《Rust程序设计》)和极客时间《陈天 · Rust编程第一课》的帮助下算是成功入门。

2023年参加了“第三界中国Rust开发者大会”,收益破丰,也是在这里第一次了解到“操作系统训练营”,今年在公众号“Rust语言中文社区”
了解到2024训练营要开办了,果断报名。

希望通过训练营的学习与实践能进一步夯实Rust语言功底,掌握操作系统知识,拓展知识面。

第一阶段总结

第一阶段主要是Rust的基础学习部分,110道题目巩固了不少基础语法知识。让我感觉最有收获的是后面的
10道算法题,之前有用Rust刷过一些LeetCode,但用Rust来实现栈,堆,队列等经典数据结构却是从没做过的,这一次有了了解。
除了实现数据结构外,这次做题也第一次编写“有意义的”unsafe代码,它终于不再那么神秘莫测了。

RISC-V

参加训练营是第一次接触RISC-V,目前正在学习《RISC-V-Reader》。RISC-V有一个很鲜明的特点是模块化,这似乎是技术发展的未来趋势,各种现代编程语言都
在使自己模块化,如Java,生活中常用的设备也在模块化,如笔记本电脑,手机,甚至我这个月刚买的小米鱼缸,RISC-V走在了ISA模块化的前沿。

下一步

目前打算在第二阶段开始前看完《RISC-V-Reader》,之后再补充一些操作系统的知识,为第二阶段的学习做好准备。

开端

从同学听到有关这个开源夏令营的事情,一方面对rust这种新兴语言颇为感兴趣,另一方面也正巧学习计算机系统知识,认识到计算机系统的精妙,也希望借此机会提高自己对计算机系统的理解,获得一定的开源经验。

链接

主要的笔记都记录在notion中,主要包含我rustlings过程中的思考和体会,记录比较精简。
https://ash-chair-1e2.notion.site/193e1434faf44e5587e71865fc9614af?v=6e2890c3bd084a77b864410b009d9706&pvs=4

第一阶段总结

这一阶段主要是熟悉rust语法以及相关知识。在我的学习中看来,rust主要有以下几个难点:

  1. 控制权转换。
    • rust中,move是默认语义,而浅拷贝(在rust中被称为引用)成为了次要语义,这使得程序需要不断地考虑生命周期
  2. 引用的简写
    • rust中让我最不能够理解的是引用的自动解引用功能,我认为一个强类型语言可以用更加明了的方式进行引用和非引用的区分。当然这只是我的浅薄观点,因为引用和非引用的自动转换实在让我伤透脑筋。
  3. 生命周期
    • 实际上rust中存在对生命周期非常精准的控制,然而受限于我的学习速度,还没有对生命周期参数和控制有更深的了解。
  4. 大量的现代特性
    • 在学习rust的过程中,我体会到了很多“旧语言”没有的特性,例如函数式编程、模板约束、宏的元编程等特性。这不禁让我想起了学习现代C++的某些特性(但是根本没有实际使用过)

第二阶段

接下来,主要就是学习riscv指令集,同时准备把更多时间花在系统课程上,配合CSAPP学习一个操作系统的基础实现。

学习Rust有一段时间了,做rustlings中途没有写blog,正好总结一下作为初学者对Rust的一些关注点和总结。

没写完,就先这样吧(逃)

todo!();

Rust基本语法

变量

Rust变量声明使用let,类型放在变量名后面。例如:

1
let x: i32 = 114514;

Rust也能自动推断类型:

1
2
let i = 114514;
let s = "string".to_string();

i被自动推断为i32s被自动推断为String

有些情况Rust也不能自动推断类型,比如:

1
let v = vec![];

报错信息如下:

1
2
3
4
5
6
7
8
9
10
error[E0282]: type annotations needed for `Vec<_>`
--> exercises/variables/variables1.rs:10:9
|
10 | let v = vec![];
| ^ ------ type must be known at this point
|
help: consider giving `v` an explicit type, where the placeholders `_` are specified
|
10 | let v: Vec<_> = vec![];
| ++++++++

这是因为Rust是一门静态类型语言在编译期必须得知变量类型的大小。

函数声明

声明方式大致如下。

1
2
3
fn func(a: i32, b: i32) -> i32 {
a + b
}

…很多内容在其他语言都有类似的概念,只是语法稍有不同,因为懒惰不再赘述

Rust的特色

作为一门年轻的语言,Rust整合了许多其他语言的优势,比如C/C++的底层系统编程能力、Ruby 的包管理器Bundler (cargo)等等。而除了严格的编译期检查、所有权和生命周期机制实现的内存安全,Rust也有许多其他大大小小的特色和亮点,比如:

组合优于继承

表达式

模式匹配

闭包

使用Option来表达可能为空的值

使用Result来处理异常

各种智能指针

使用RefCell实现内部可变性

Rust与数据结构和算法

学Rust当然要从链表写起!
==学Rust千万不要从链表写起!==

Rust如何实现链表?

按照C/C++的一贯做法,我们可能会写出:

1
2
3
4
struct node {
val: i32,
next: Box<node>,
}

然而实际用起来,会发现你用不起来

1
2
3
4
5
6
let n = node {
val: 114514,
next: Box::new(node {

})
}

问题就在于Box里必须要有东西,即它任何时候必须指向一个有效的元素!别说链表末尾节点如何实现,我们甚至无法完成头节点(任何节点)这样一个递归定义的形式。

于是我们想到使用Option来表示可能为空的值,从而能够定义单个节点。

1
2
3
4
struct node {
val: i32,
next: Option<Box<node>>,
}

面向rCore的Rust

为了完成rCore,还需要深入了解Rust的哪些?

unsafe

外部接口

Rust编译和链接

Rust项目结构

另外推荐:《Rust死灵书》

始于rust

在参加训练营之前,我学rust有大概一年的时间了。rust是一门让我惊叹的语言,同时我也学到了很多。第一次完整阅读一本英文书正是官方的the book,它极大的提高我对英语的兴趣以及阅读能力。在rust之前,我学过c、c++、java、python以及go,但是毫无疑问我现在是rustacean,因为rust的设计哲学简直太赞了,其中特别是rust对option和错误的优雅处理。
学完rust后我就经常逛rust中文网,刚好了解到开源操作系统训练营,由此开始了我的开源操作系统之旅。

第一阶段总结

前100题主要是熟悉rust语法,其实这次我是第二次参加了,因此前100题对我来说没什么压力。后十题是算法题,涉及链表、二叉树、图等。学过rust的同学知道,在rust中处理自引用的数据结构相对其它语言会难上许多,不过真正了解过Box、NonNull等之后会感觉还好。这次二刷rustlings我又深入看了下标准库的Box、NonNull等结构,收获颇丰!

avetar

第一阶段 Rustling心得

Rustlings对于rust的上手帮助很大,不过它的难度比较松弛,而额外添加的10道算法题又弥补了这一部分。尽管如此,我在完成rustlings过程中还是遇到了很多问题和疑惑,解决这些问题让我对rust的理解提高了很多

24 vecs2

1
2
3
4
5
fn vec_map(v: &Vec<i32>) -> Vec<i32> {
v.iter().map(|element| {
element*2
}).collect()
}

在这里出现了map和collect方法。map和collect的搭配是rust非常常用的元素处理方法,代表对迭代器中的每一个元素都进行传入的闭包的操作,并最后collect进一个集合中,collect可以指定集合的类型,如collect::<Vec<i32>>(),非常的好用

39 strings4

1
2
3
4
5
6
7
8
9
10
string_slice("blue");
string("red".to_string());
string(String::from("hi"));
string("rust is fun!".to_owned());
string("nice weather".into());
string(format!("Interpolation {}", "Station"));
string_slice(&String::from("abc")[0..1]);
string_slice(" hello there ".trim());
string("Happy Monday!".to_string().replace("Mon", "Tues"));
string("mY sHiFt KeY iS sTiCkY".to_lowercase());

string主题的练习中,&str和string类型的互相转换是一个重点,但是需要额外注意的不仅是各类函数的适用对象,还有其返回值所有权的不同,如to_lowercase()方法返回的是一个全新的string类型变量,而不是发生了所有权的转移。

51 errors2

1
2
3
4
5
6
7
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1;
let cost_per_item = 5;
let qty = item_quantity.parse::<i32>()?;

Ok(qty * cost_per_item + processing_fee)
}

在这里解析item_quantity时,如果使用unwrap方法,解析错误会导致程序崩溃,而使用?运算符简化之后,如果发生解析错误,? 运算符会自动将 Err 值返回给调用方,从而避免了使用 unwrap 方法导致的潜在崩溃。

72 iterators2

rust的迭代器非常强大,也具有非常多的特性,这里进行整理:

  1. 创建迭代器:你可以通过调用集合的 .iter()、.iter_mut() 或 .into_iter() 方法来创建迭代器,具体取决于你需要对集合进行何种操作(只读、可变或所有权转移)。
    1
    2
    3
    4
    let numbers = vec![1, 2, 3, 4, 5];
    let iter = numbers.iter(); // 创建只读迭代器
    let iter_mut = numbers.iter_mut(); // 创建可变迭代器
    let into_iter = numbers.into_iter();// 创建所有权转移迭代器
  2. 迭代元素:使用 for 循环来遍历迭代器中的元素。在每次迭代中,迭代器会返回一个元素,并将其绑定到指定的变量上。
    1
    2
    3
    4
    let numbers = vec![1, 2, 3, 4, 5];
    for num in numbers.iter() {
    println!("Number: {}", num);
    }
  3. 使用迭代器逐个处理元素:你可以使用迭代器的方法链来对元素进行各种操作。例如,你可以使用 .map() 方法对每个元素进行映射,使用 .filter() 方法进行过滤,使用 .fold() 方法进行累积等等。
1
2
3
4
5
let numbers = vec![1, 2, 3, 4, 5];
let doubled_numbers: Vec<i32> = numbers.iter()
.map(|&num| num * 2)
.collect();
println!("{:?}", doubled_numbers); // 输出: [2, 4, 6, 8, 10]
  1. 惰性求值与及早求值:Rust 的迭代器是惰性求值的,意味着它们只在需要时才会产生元素。这使得你可以在迭代器链中组合多个操作,而不会立即执行它们。只有在需要结果时(例如调用 .collect() 方法)才会触发迭代器链的执行。
    1
    2
    3
    4
    5
    6
    let numbers = vec![1, 2, 3, 4, 5];
    let sum: i32 = numbers.iter()
    .filter(|&num| num % 2 == 0)
    .map(|&num| num * 2)
    .sum();
    println!("Sum: {}", sum); // 输出: 12

104 algorithm4

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
fn insert(&mut self, value: T) {
if self.root.is_none() {
self.root = Some(Box::new(TreeNode::new(value)));
return;
}
let mut root = self.root.as_mut().unwrap();
loop {
if value > root.value {
if root.right.is_none() {
root.right = Some(Box::new(TreeNode::new(value)));
break;
} else {
root = root.right.as_mut().unwrap();
}
} else if value < root.value {
if root.left.is_none() {
root.left = Some(Box::new(TreeNode::new(value)));
break;
} else {
root = root.left.as_mut().unwrap();
}
}
else { break; }
}
}

在算法题中涉及了好几次获取可变引用和unwrap,这里要注意的点是unwrap会导致所有权的转移,所以需要首先调用as_mut获取可变引用,再进行unwrap,防止出现所有权问题。

第二阶段:

第二阶段的总结发布在我的个人博客上:笔记-rCoreLab实验笔记

第三阶段:

第三阶段主要的时间在进行系统能力大赛的内核开发,有关比赛内核开发的日志发布在我的个人博客上:【开发日志】chaos开发日志

在完成宏内核方向的作业时,我也遇到了一些值得记录的错误。按照作业要求中修改代码之后并没有按照预期发生panic,mmap syscall的执行完全正常。为了出现预期的错误,我们将map flag修改为MAP_FIXED,经过修改,mmap syscall出现错误,返回了-1,但是仍然不会出现panic。通过阅读mmap的完整实现源码后我发现,StarryOS的处理没有任何问题,问题的根本来自于我错误认识了MAP_ANONYMOUS的意思,将其用MAP_FIXED替换,正确的做法应该是同时加上二者。

到这里,整个开源操作系统训练营彻底结束了,在这两个月时间里我对操作系统的理解从一问三不知到一知半解,再到现在有能力逐步开发内核,训练营的引导功不可没。训练营提供的绝好平台让我和许许多多高手可以一同凭借自己的爱好和热情学习操作系统,这会是我操作系统方向研究的开始,也会是我一辈子都不会忘记的经历。感谢训练营为我提供的机会,也要感谢我对操作系统的热爱。

#第一阶段

开营前提前刷了下 rustlings,虽然这次练习代码不是最新的 rustlings,但考察内容是一样的。除此之外本次练习还扩充了一些编译和数据结构的题目,数据结构的题目写起来还是有点卡。不说了… LeetCode 走起。

作为想了解操作系统小白一枚,希望能跟完全程吧(🤔)。

第一阶段总结报告

事件0:报名!

因为学长的推荐,正准备自己开始做rcore lab的时候突然在rcore的官方repo里面看到news:

开源操作系统训练营报名!

wow,看到里面的正是自己想要了解学习的内容,一下子打起了12分精神,感觉很切合自己所在的嵌入式方向,并且完美的满足自己想要在更深平台上学习的想法(之前是在stm32的机器上跑过简单的ucOSII 实时操作系统)。

事件1:rust,启动!

感觉自己花在学rust的时间挺长的,主要是想更深入的学习这个语言(正巧大二上学了编译原理),在rustlings上花了不少时间,不想一个个说语法了,只是记得smart_pointers的特性很有意思狠狠的理解了,当然还有所有权(第一次见到在编译阶段去强调这个概念的语言,之前写malloc实验的时候有想过能不能在写语言的时候把内存的管理考虑好),option之类的东西和c++真的很像,前面的智能指针也是c++那一套的东西(有种写cs144的感觉)。范型的使用我就类比之前学java的时候的用法了,让我记忆深刻的还有rust对于错误处理包装成一个enum,居然是个枚举,还有它的宏,也太多了吧(学c的时候确实体会过宏的强大)。

最后10个algorithms花了小半天写完,确实算是对之前的学习合起来应用了一下。

记录的笔记我就留在个人博客上了,因为用的notion写博客,试试推送很方便,所以习惯了:

Rust基础积累—常更

第二阶段总结报告

做实验

从做实验说起,做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 语言,因此对这两门语言的风格深有所感:

  • C 的原则就是「完全相信使用者」,因此你可以用 C 实现几乎所有操作,非常自由。但是为了安全,必须人为设计一些规范来进行约束。
  • Rust 的原则则是「完全不相信使用者」,所以你会发现 Rust 的很多语法都是为了约束程序员,强迫程序员写出安全的代码。但是,有些时候编译器还是不够聪明,或者说是无法进行判断,因此必须开点绿灯——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道题。
战斗,爽!