0%

2024开源操作系统训练营第一阶段总结-刘梓陆

2024 开源操作系统训练营第一阶段总结-刘梓陆

本来想在这里放一个图片的,但是看群里好像图片不太好处理,那就不放了

不过在这里贴一个图片仓库链接:ServiceLogos,作者画得太好看,导致我当天看到就去印贴纸了……

写在前面

我从稍微理解了我的专业之后就一直都很崇拜 Linus,这位堪称传奇的软件工程师在 1991 年 8 月 25 日——他 21 岁时就在网络上发布了 Linux 内核的源代码。现在是 2024 年 4 月 23 日,我也是 21 岁,追逐吗,梦想吗,我也想像他一样写出这样神奇的代码,33 年后的今天,我也要开始了,Linus。

欢迎交流;-)

在写这篇博客时,本人东北大学大三在读,前两年东学西学,Rust 就是之一,说实话,在本学期开始时,我实在找不到什么东西来做,准备先买一本《趣读 Linux 源码》来看,感谢我学弟翊嘉,是他给我推荐了这个操作系统训练营,这样直接上手操作系统的效果比看书要好太多了,真的十分感谢!

我对 Rust 的理解还是比较少,future、unsafe 的内容更是知之甚少,所以大佬求带!看到我这篇总结报告之中有什么不足也请指出!

Rust,很好玩

这一阶段的练习基本上带领我加强了一遍 Rust 的语法,因为我接触 Rust 已经有小半年了,所以对我来说并不算很困难(如果要我 0 基础开始的话可能就会有点受不了了)。话虽如此, rustlings 确实给了我很多细节上的考验,我通过了它们,查缺补漏,学到了很多新东西(这里就当错题集一题一题记录了 XD):

2024_4_24, PS:因为这一个阶段基本上都是一些对 Rust 语法概念上的训练,所以我会着重讲一下对 Rust 一些重要语法概念的理解,本来写了挺多的,但是后面全部都删掉了,因为突然发现我好像也就是鹦鹉学舌,纯纯就是一些对官方文档还有论坛讨论的翻译,所以我会在讲到一个我觉得值得注意的点的时候把链接贴出来,然后提一下这个链接讲了什么,同学们自己去看会好的多

2024_4_25, PS:感觉讲的有点乱了,实际上 Rust 之中的移动、生命周期、所有权的概念真的是耦合在一起的,在讲到一个部分的时候免不了要带一点其它两个部分。

1. ♿ 移动 ♿ 和借用, options3.rs 之中犯的错误

说实话,移动和所有权这个语意在其他语言里面确实存在,但是一般作为规范存在,没有在 Rust 里面管的这么严,在 options3.rs 之中我就犯了这么一个错误:

我在 main 函数之中这样写道:

1
2
3
4
5
6
7
let y: Option<Point> = Some(Point { x: 100, y: 200 });

match y {
Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
_ => panic!("no match!"),
}
y;

这样写会报错(value used here after partial move),因为在最后返回 y 之前 Some(p)就已经把外面 y 的所有权移动到 p 上了,此时 y 指向的是一个无效的内存空间。所以现在的解决方案就是把外面 y 借用到 Some(p)之中,一个关键字 ref 可以帮助我们做到这一点。这个关键字其实用法还是挺多的,一般用于模式匹配

下面是一段在《Rust 编程指南》之中的阐述(在第 10 章 Pattern 小节 Refrence Patterns 部分之中,如果你看的是英文版的 PDF 的话,在 P372 可以找到对这个问题的详细描述):

Rust 的模式匹配支持处理引用的两个特性:Ref 模式借用匹配值的一部分,&模式匹配引用

我个人感觉可以这样理解:Rust 之中的模式匹配就像是在匹配一个正则表达式,&被理解位一个字面值字符,如果写&的话,就会把&引用的那个值“解离”出来,因为匹配了前面的一个&,自然就是匹配后面的这个“值”了。

呃,我又要引用《Rust 编程指南》之中的原话了(毕竟真的这本书很专业)

In an expression, & creates a reference.I a pattern, & matches a refrence

在一个表达式之中,&创造一个引用,在模式之中,&匹配一个引用

当时我也是被震撼,确实有一种对称的美对吧?其实在 rustlings 对应的有关模式匹配训练之中你就可以发现这一点:你可以使用()来创建一个元组,可以使用{}来创建一个结构体,当然就可以使用对应包含(){}的模式匹配来把其中对应的字段解析出来,_这是一个互逆的过程_,我觉得这一点需要好好注意一下,基本上就是模式匹配直观意义上的精髓所在了。

但是 ref更像是一个正则表达式之中的元字符,用于描述匹配规则:我只想要借用匹配的值,而不是移动它们,如果想要借出可变引用的话就要使用ref mut。所以这里这样改改,加一个ref就成功了:

1
2
3
4
match y {
Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
_ => panic!("no match!"),
}

当然,如果匹配的数据实现了Copetrait 的话,你怎么样搞都行,但是在处理有所有权的对象的时候就必须小心处理了,这 Rust 真给我好好上了一课。

1.1 讲点更复杂的怎么样?

在下一部分我想要讲讲我对生命周期和所有权的理解,所以在这里我想先就着这个例子好好借题发挥,先铺垫一下。

实际上options3.rs还能出得更加复杂一点,这种情况就要把ref&同时用上才能解决。在options3.rs之中,该结构体之中的两个成员都是实现了Copytrait 的,那么如果有具有所有权的成员的话,应该如何处理呢?

就像是下面这种情况(实际上你可以在《Rust 程序设计》的 P374 找到它):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Engine {
version: String,
}

struct Car {
engine: Engine,
}

fn main() {
let borrowed_car: Option<&Car> = Some(&Car {
engine: Engine {
version: String::from("混合动力"),
},
});

match borrowed_car {
Some(&Car { engine }) => {
println!("I got the engine!");
}
None => {
println!("noting here");
}
}
}

如果你像上面这样写的话就好像是把一辆借来的车里头的引擎偷出来了。Rust 会降下它的神罚,引导我们做出正确的选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
error[E0507]: cannot move out of `borrowed_car.engine` as enum variant `Some` which is behind a shared reference
--> src/main.rs:16:11
|
16 | match borrowed_car {
| ^^^^^^^^^^^^
17 | Some(&Car { engine }) => {
| ------
| |
| data moved here
| move occurs because `engine` has type `Engine`, which does not implement the `Copy` trait
|
help: consider borrowing the pattern binding
|
17 | Some(&Car { ref engine }) => {
| +++

这里忍不住想要夸一下,Rust 的错误输出真的是太好了!

所以既然车是借的,引擎也应该是借的对不对?这里使用 ref 来把引擎借出来就没事了。

1.2 Temporary lifetime,什么时候会销毁?

实际上假如你修正了上面这个ref的问题仍然会报错,😭 了:

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
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:10:44
|
10 | let borrowed_car: Option<&Car> = Some(&Car {
| ____________________________________________^
11 | | engine: Engine {
12 | | version: String::from("混合动力"),
13 | | },
14 | | });
| | ^ - temporary value is freed at the end of this statement
| |_____|
| creates a temporary value which is freed while still in use
15 |
16 | match borrowed_car {
| ------------ borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
10 ~ let binding = Car {
11 + engine: Engine {
12 ~ version: String::from("混合动力"),
13 + },
14 + };
15 ~ let borrowed_car: Option<&Car> = Some(&binding);
|

这里涉及到一个比较微观的问题:Rust 在背后为我们做了的事。实际上很多时候我们创建的变量的生命周期就只是一个局限于语句(statement)的 temporary lifetime,Rust 为我们做了 lifetime extention 之后才能在 block 之中使用。

在论述这个问题之前,先列一下参考文献吧:

  • 1《Temporary lifetimes》
    • 强烈建议先看这一篇,这是 Rust 开发小组的一次会议纪要,讲了现在 Rust 之中 Temporary lifetimes 的一些情况,在 Rust2024 之中要改进哪一部分,讲的比较通俗,例子也好很多
  • 2《Place Expressions and value Expressions》
    • 这篇 RustRefrence 文档之中讲到了 Rust 之中表达式的分类(类似于 C++之中的左值和右值)
  • 3《Temporary scopes》
    • 这篇文档之中谈到了我之前没有听说过的作用域 Temporary scope,我的理解就是立即值(右值)的一个小作用域
  • 4《Temporary lifetime extension》
    • 这篇文档之中讲到了 Temporary 作用域的提升,也就是为什么说一个 Temporary scopes 在进行赋值等操作之后可以将本来很小的作用域延展到和被赋予值的变量作用域相同的情况

上面这些概念之间的关系就是: The temporary scope of an expression is the scope that is used for the temporary variable that holds the result of that expression when used in a place context。(摘自文献 3)

注意这里的三个关键词:temporary scopetemporary variableplace context

处于位置上下文(place context)的表达式就是位置表达式,它代表了一个内存位置(Paths)。这些表达式是局部变量、静态变量、解引用(*expr)表达式、数组索引表达式(expr[expr])、字段引用(expr.f)和括号内的位置表达式(parenthesized place expressions

突然感觉这里举一个例子会比较好:

1
let t = &foo();

实际上是:

1
2
let tmp;
let t = { tmp = foo(); &tmp };

也就是说,&foo()是一个表达式,现在它处于一个位置上下文之中(&需要一个位置),所以现在需要一个 temporary variable(简称 temporary)来存储这个foo()表达式的结果,也就是上面的tmp,这个tmp的生命周期就是 temporary scope。

而且在这里还做了一个 lifetime-extension:如果没有 let 的话,实际上 temporary 在语句的末尾(遇到;)就会被销毁:

1
&foo();

实际上是

1
2
3
4
{
let tmp;
tmp = &foo();
}

这个立即失效的局部变量显然不符合用户的意图。因此,我们延长 temporary 的生命周期,使其在最内层块的末尾被删除(the end of the innermost block)。

实际上就是把原来局限于语句的生命周期提升了一个一个层次,提升到块了。

怎么样,是不是原来好像很理所当然的一段代码都变得不那么简单了?更多有关这部分概念的解释请参考文献 1,我在这里就不赘述了。


回到在本小节一开始提出的问题,为啥会报错?其实看报错信息就知道了,按照我们上面的思路来思考:

首先,里面这个表达式处于一个位置上下文(place context)之中,因为&期待一个位置,所以得有一个 temporary 来保存这个位置啊:

1
2
3
4
5
6
7
8
9
{
let tmp;
tmp = Car {
engine: Engine {
version: String::from("混合动力"),
},
};
&tmp
}

但是很不幸的是,这个 temporary 首先作用于 Some,而不是 let,所以不会进行 lifetime-extension,在遇到;之后,temporary 就被 drop 掉了。

出现了,悬垂引用!Rust 就 panic 了。(本节完)

2. 🌿 生命周期和所有权 🌿 - 光的波粒二象性?

“光的波粒二象性”这个比喻是油管的一个博主 ledoo 提出来的。

我觉得讲的真的很不错,所以在这里想提一提。

大概就是说,我们讲到生命周期(lifetime)就是总是从代码层面去分析对不对?去看是不是你变量的第一次声明和最后一次使用跨越了一个 block,但是实际上 Rust 做这个生命周期是为了检查内存的有效性的,你还可以从变量指向内存的位置和保证内存的有效性这两个角度来理解生命周期。(Thinking about lifetime as regions of memory, what can they——the reference point to?)

这就好像是光的波粒二象性一样,有的时候从这个角度去看比较好理解,有的时候从另外一个角度去看则更佳。 我个人觉得用这种角度去理解生命周期标注是最好的。

就拿 lifetimes1.rs 为例吧:

实际上他视频里面就有讲这个很经典的生命周期标注问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is '{}'", result);
}

这道题的正确答案应该是这样:

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

加上生命周期标注,生命标注在我初学 Rust 的时候把我虐的死去活来的,你比如说这里的'a,为什么大家都是标'a?是不是意味着所有传入的 reference 的生命周期都要是相同的?什么?返回值的生命周期也是'a?三个都要相同?啊?这限制也太严格了把?

当然,如果你读过像是 Rust 圣经的文档话,文档里面就会告诉你:实际上呢,这里的意思是以生命周期最小的那个 Reference 为准,不过呢……总感觉怪怪的。

如果你使用指向有效内存的角度来进行分析的话,我感觉就会比较好一点:

x 指向一块内存 memory_x,y 也指向一块内存 memory_y,这两块内存当然可以是不一样的,返回值也指向一块内存,那么在这里'a的作用就是说,我要统一这两块内存,返回值指向memory_x“和”memory_y。这里“和”打双引号,因为现实不可能同时指向两部分嘛。

所以 Rust 怎么检查这个返回值是否有效?答案是在这个返回值最后一次使用之前这两块内存memory_xmemory_y都要有效

如果使用生命周期约束的话,差不多也是这个意思,比如说'a: 'b比较官方的说法就是'a的生命周期的范围必须包含或等于'b的生命周期的范围。

假如我们使用“内存说”的话,就刚刚好反过来,'b指向的内存包含了'a指向的内存,所以'b有效的时候'a也一定要有效,所以你可以这样读这个限界:'a in 'b

嗯,大概就是这样。(本节完)

写在最后

首先,感谢你看到这里!

本来还想写一些 unsafe 还有 cargo 的内容的,但是没有时间了!!!!!唉,期中了,我还有 ddl 要赶呢,在这篇博客之中写的生命周期、所有权、移动的概念(话说好像都在讲生命周期)我认为就是 Rust 的核心了,所以对于第一部分的 Rust 编程学习,内容分量应该也刚刚好吧?(小小的虚荣一下,把我在第一部分之中讲的那个车和引擎的问题出成题感觉也 ٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ?)

所以我准备移动一下,在第二阶段的报告里面把 unsafe、cargo 连着操作系统讲一下,如果你觉得从我的这篇报告学到了什么东西的话,那就太好了!

2024 年 04 月 25 日 21:14:31