0%

Image Description1111

几年前就萌生了用 Rust 制作一个操作系统的想法, 但 x86 的复杂度又有点让人望而却步, 而当时恰好遇到了 riscv, 恰好遇到了 rCore. 到现在, 终于有了一个可以亲身加入训练营的机会, 在此感谢各位老师和学长们为此的付出

第一阶段 Rustlings 总结

几年前将自己学习 Rust 的过程汇总成笔记写成教程放在 个人博客 上了. 这里只再简单总结一下一些重要概念内容

所有权 (Ownership)

所有权是 Rust 的一个核心概念, 用于确保内存安全. 它规定了一个变量 (或数据) 在任何时刻都有且仅有一个所有者. 当所有者离开作用域时, 其所拥有的数据将自动释放, 防止内存泄漏. 通过移动赋值和不可变 / 可变借用, 得以控制数据的访问和生命周期

  1. 移动赋值: 当一个值从一个变量转移到另一个变量时, 原变量将不再拥有数据所有权, 不能再被访问
  2. 引用与借用:
    • 不可变引用: 允许安全读取但不修改原始数据
    • 可变引用: 独占访问, 允许修改数据, 同一作用域内不可存在多个可变引用指向同一数据

泛型 (Generics) 与生命周期 (Lifetimes)

泛型允许创建适用于多种类型的数据结构和函数, 无需重复实现. 生命周期可被视为一种特殊的泛型. 生命周期注解是用来描述引用的有效期的一种机制. 在函数签名或结构体定义中, 通过类似 'a 这样的符号来表示生命周期参数, 并通过上下文推断或显式声明来确保引用不会超过其底层数据的有效期

Traits (特性)

特质是 Rust 中的一种接口抽象, 它定义了一组方法签名, 供实现特质的类型遵守. 通过 traits, Rust 实现了面向对象编程中的多态性.

智能指针

Rust 提供了一系列智能指针类型, 如 Box<T> Rc<T> Arc<T> RefCell<T> 等, 它们分别用于管理堆上的唯一所有权, 引用计数共享所有权, 线程安全的引用计数共享所有权以及在编译时静态检查之外增加运行时借用检查等场景

多线程与异步编程

  • 多线程: Rust 支持原生线程, 通过 std::thread 模块相关API. Rust 的 Ownership 和借用规则有助于防止数据竞争和死锁等问题,Mutex, RwLock 等同步原语进一步加强了线程间的安全通信
  • 异步编程: Rust 异步编程模型基于 futures 和 async/await 关键字,提供了高效非阻塞IO的能力. tokioasync-std 等库提供了异步编程的基础设施, 包括任务调度, TCP / HTTP 网络编程等

除了这些基本语法, 在练习中也实践了手动实现链表, 双向链表, 堆栈, 深度优先和广度优先算法, 排序算法等. 通过 Rustlings 练习, 可以逐步掌握以上各个概念的实际应用, 通过解决实际问题加深对 Rust 编程特性的理解, 并学会如何在实践中遵循 Rust 的安全性和性能原则, 为接下来的第二阶段打下基础, 以构建安全高效的 OS 内核

此外也感谢群内的大佬们, 没事翻翻大佬的聊天记录总能学到新东西 :)

2024春夏季开源操作系统训练营Blog-第一阶段-罗健峰

一些学习链接:

前言

在21年下半年的时候,当时想着给前端的某个项目赋能,由于前端项目过于臃肿、代码质量不高,导致了不单单是开发还是维护都提出了巨大的工程量,提升前端的编译速度和开发效率的情况迫在眉睫,无意中发现Rust在webassembly领域是有一地之席的、在前端基建方面具有与生俱来的优势。

所以很单纯的目的就是想利用Rust这门高性能、安全性的语言来提升前端打包的效率。

目前Rust社区里已经有使用Rust为基础开发的前端打包工具Turbopack

非常自然的我进入到了Rust这门语言的学习。

第一阶段总结

由于Rust,我是有基础的。所以在第一阶段我可能更加注重的是Rust基础的巩固,同时加深使用Rust语言的算法描述。

学一门语言,除了学习各类语言相同的编程概念外,其中,学习该语言自身的特性和适用场景也是非常重要的。

Rust作为一门系统级编程语言,采百家之长,自身形成一些Rust语言所特有的语言特性,其中变量的所有权和生命周期就是Rust自身语言特性中最璀璨的一块。

👇👇👇现在给大家总结和分享一下我在开源操作系统训练营第一阶段的总结。👇👇👇

  1. 思维转换
  2. 所有权和生命周期

一、思维转换

在学习Rust的过程中,如果你想从其他语言迁移到Rust,必须要经过一段时期的思维转换(Paradigm Shift)

从命令式(imperative)编程语言转换到函数式(functional)编程语言、从变量的可变性(mutable)迁移到不可变性(immutable)、从弱类型语言迁移到强类型语言,以及从手工或者自动内存管理到通过生命周期来管理内存,难度是多重叠加。

而 Rust 中最大的思维转换就是变量的所有权和生命周期,这是几乎所有编程语言都未曾涉及的领域。

二、掌握Rust基本概念

只列出我认为学习Rust这门语言的内容和我入手其他语言的经验之谈。

先把握大体后追踪语言细节

  1. 数据类型
  2. 所有权和借用
  3. 流程控制
  4. 模式匹配
  5. 函数
    • 泛型
    • 特征
  6. 包和模块、注释等
  7. cargo及常用库(tokio、serde等)
  8. rust项目的工程化(代码规范化、commit规则等)

三、所有权和生命周期

Rust这门语言最耀眼的设计就在于变量的所有权机制和生命周期,这也是Rust这门语言我认为最具代表性的特征。

它带给了Rust语言最本质或者说最核心的点、就是内存的管理。致使使用这门语言的人要有强烈的内存分布和管理内存的能力,所有权和生命周期是 Rust 和其它编程语言的主要区别,也是 Rust 其它知识点的基础。

本质上来说所有权和生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

fn main() {
let data = vec![10, 42, 9, 8];
let v = 42;
if let Some(pos) = find_pos(data, v) {
println!("Found {} at {}", v, pos);
}
}

fn find_pos(data: Vec<u32>, v: u32) -> Option<usize> {
for (pos, item) in data.iter().enumerate() {
if *item == v {
return Some(pos);
}
}

None
}

动态数组因为大小在编译期无法确定,所以放在堆上,并且在栈上有一个包含了长度和容量的胖指针指向堆上的内存。

在调用 find_pos() 时,main() 函数中的局部变量 data 和 v 作为参数传递给了 find_pos(),所以它们会被放在 find_pos() 的参数区。

loading error
图1

按照大多数编程语言的做法,现在堆上的内存就有了两个引用。不光如此,我们每把 data 作为参数传递一次,堆上的内存就会多一次引用。

但是,这些引用究竟会做什么操作,我们不得而知,也无从限制;而且堆上的内存究竟什么时候能释放,尤其在多个调用栈引用时,很难厘清,取决于最后一个引用什么时候结束。所以,这样一个看似简单的函数调用,给内存管理带来了极大麻烦。

对于堆内存多次引用的问题,我们先来看大多数语言的方案:

  1. C/C++要求开发者手工处理:非常不便。这需要我们在写代码时高度自律,按照前人总结的最佳实践来操作。但人必然会犯错,一个不慎就会导致内存安全问题,要么内存泄露,要么使用已释放内存,导致程序崩溃。

  2. Java等语言使用追踪式GC:通过定期扫描堆上数据还有没有人引用,来替开发者管理堆内存,不失为一种解决之道,但 GC 带来的 STW 问题让语言的使用场景受限,性能损耗也不小。

  3. ObjC/Swift使用自动引用计数(ARC):在编译时自动添加维护引用计数的代码,减轻开发者维护堆内存的负担。但同样地,它也会有不小的运行时性能损耗。

现存方案都是从管理引用的角度思考的,有各自的弊端。我们回顾刚才梳理的函数调用过程,从源头上看,本质问题是堆上内存会被随意引用,那么换个角度,我们是不是可以限制引用行为本身呢?

这个想法打开了新的大门,Rust 就是这样另辟蹊径的。

所以所有权机制规定了以下3点来限制引用。

  • 一个值只能被一个变量所拥有,这个变量被称为所有者(Each value in Rust has a variable that’s called its owner)。
  • 一个值同一时刻只能有一个所有者(There can only be one owner at a time),也就是说不能有两个变量拥有相同的值。所以对应刚才说的变量赋值、参数传递、函数返回等行为,旧的所有者会把值的所有权转移给新的所有者,以便保证单一所有者的约束。
  • 当所有者离开作用域,其拥有的值被丢弃(When the owner goes out of scope, the value will be dropped),内存得到释放。

在这三条所有权规则的约束下,我们看开头的引用问题是如何解决的:

loading error
图2

原先 main() 函数中的 data,被移动到 find_pos() 后,就失效了,编译器会保证 main() 函数随后的代码无法访问这个变量,这样,就确保了堆上的内存依旧只有唯一的引用。

本质上来讲这就是所有权机制所保证的

其中又牵扯出了移动(Move)和借用(borrow)、引用(Reference)的规则

移动

变量data的所有权被移动给另一个data1了、后续data1的所有权移动到了sum函数中、导致了变量data和data1
都被移动、无法使用。

可以看到,所有权规则,解决了谁真正拥有数据的生杀大权问题,让堆上数据的多重引用不复存在,这是它最大的优势。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let data = vec![1, 2, 3, 4];
let data1 = data;
println!("sum of data1: {}", sum(data1));
println!("data1: {:?}", data1); // error1
println!("sum of data: {}", sum(data)); // error2
}

fn sum(data: Vec<u32>) -> u32 {
data.iter().fold(0, |acc, x| acc + x)
}

但是,这也会让代码变复杂,尤其是一些只存储在栈上的简单数据,如果要避免所有权转移之后不能访问的情况,我们就需要手动复制,会非常麻烦,效率也不高。

Rust 考虑到了这一点,提供了两种方案:

  1. 如果你不希望值的所有权被转移,在 Move 语义外,Rust 提供了 Copy 语义。如果一个数据结构实现了 Copy trait,那么它就会使用 Copy 语义。这样,在你赋值或者传参时,值会自动按位拷贝(浅拷贝)。

  2. 如果你不希望值的所有权被转移,又无法使用 Copy 语义,那你可以“借用”数据。

借用和引用

其实,在 Rust 中,“借用”和“引用”是一个概念,只不过在其他语言中引用的意义和 Rust 不同,所以 Rust 提出了新概念“借用”,便于区分。

本质上来将就是不希望所有权被转移、又无法使用 Copy 语义。

总结

打个比方:我拥有一本书,会有以下情况

我把这本书送给了朋友、那我就丧失了这本书的拥有权(所有权)。

我把这本书借给了朋友、朋友不拥有这本书、但有借用,可以修改内容(可变的引用),当然我也可以提醒他不要在我的书上做标记(不可变的引用)。

其中实现Copy trait的语义的意思就是、朋友将这本书拿去复印了、他拥有了复印这本书的所有权、所以我们俩都拥有了不同的所有权。

当然也会有不同的情况发生,比如说这本书是我们俩一起出钱买的,我们俩都应该拥有这本书的所有权。进而Rust引入Rc、Arc、Cell、RefCell等智能指针来实现一个变量会产生多所有权的情况。

个人总结

第一阶段的基础还是蛮重要、一些使用Rust语言描述的语言细节还是需要多多关注,为我们后面的内容打好基础。

进一步感谢开源操作系统社区的老师和技术人员,感谢你们辛苦的付出🌾🌾🌾。

期待第二阶段的学习、展开新的篇章。

Rust OS 一阶段学习总结

@FinishTime: 2024-04-23 00:35:51

个人背景概述

我是一名软件工程专业的大二本科生,曾参与过2023年秋冬季训练营。
在去年的训练营中,我学习了rust,但因为第一次学习,且当时需要准备期中考试和各科作业,没有按时通过第一阶段。
虽然我后续进入了二阶段群,但没有完成第二阶段的实验部分,今年重新参与,打算认真跟进下去。

今年一阶段学习时间表

  • 2024-03-27 重新报名参与活动,并邀请了一名本校大三学长共同进步
  • 2024-04-07 正式开始参与活动,又邀请了两位同校同学,我开始重新复习rust
  • 2024-04-12 通过rust圣经复习至基础部分完成,开始进行做题
  • 2024-04-13 累计做题3小时,做至43题
  • 2024-04-19 进行到cow指针部分
  • 2024-04-20 完成前100题
  • 2024-04-21 完成110题,开始提交

一阶段学习内容概述

我按照“rust语言圣经”上的讲解顺序进行学习,分别学习了:

  • 变量的绑定与解构
  • 基本类型
  • 所有权和借用
  • 复合类型
  • 流程控制
  • 模式匹配
  • 方法
  • 泛型和特征
  • 集合类型
  • 生命周期
  • 返回值、错误处理
  • 包和模块
  • 注释和文档
  • 格式化输出
  • 智能指针
  • 多线程
  • 闭包与迭代器
  • 宏编程
  • unsafe rust

问题概述

在一阶段过程中,我遇到了一定的问题,并尝试进行解决。以下内容是我认为,不是那么“较为基础”的问题。

2024-04-12 问题:unicode展示

问题描述

在primitive_types2.rs中,注释要求尝试传入unicode字符,来理解rust中的char。
但是,我通过https://emojidb.org/rust-emojis这个emojis网址,拿到了一个emoji:☢️,进行测试,却直接编译出错:

出错代码

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
26
27
28
29
30
31
32
33
34
// primitive_types2.rs
//
// Fill in the rest of the line that has code missing! No hints, there's no
// tricks, just get used to typing these :)
//
// Execute `rustlings hint primitive_types2` or use the `hint` watch subcommand
// for a hint.


fn main() {
// Characters (`char`)

// Note the _single_ quotes, these are different from the double quotes
// you've been seeing around.
let my_first_initial = 'C';
if my_first_initial.is_alphabetic() {
println!("Alphabetical!");
} else if my_first_initial.is_numeric() {
println!("Numerical!");
} else {
println!("Neither alphabetic nor numeric!");
}

let your_character = '☢️'; // Finish this line like the example! What's your favorite character?
// Try a letter, try a number, try a special character, try a character
// from a different language than your own, try an emoji!
if your_character.is_alphabetic() {
println!("Alphabetical!");
} else if your_character.is_numeric() {
println!("Numerical!");
} else {
println!("Neither alphabetic nor numeric!");
}
}

问题分析

我与队伍里的成员们进行了初步的实验和讨论,确定报错的原因是:“☢️”无法被解析成字符,必须被解析成字符串。那么,问题来了,为什么它无法被解析成字符,而其它的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”。

2024-04-12 问题:if内部返回异常

问题描述

在编程的过程中,我意外发现,在if语句内,通过不写分号的方式进行返回,会产生问题,程序无法编译,见下图:

出错代码

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// structs3.rs
//
// Structs contain data, but can also have logic. In this exercise we have
// defined the Package struct and we want to test some logic attached to it.
// Make the code compile and the tests pass!
//
// Execute `rustlings hint structs3` or use the `hint` watch subcommand for a
// hint.

#[derive(Debug)]
struct Package {
sender_country: String,
recipient_country: String,
weight_in_grams: i32,
}

impl Package {
fn new(sender_country: String, recipient_country: String, weight_in_grams: i32) -> Package {
if weight_in_grams <= 0 {
panic!("Can not ship a weightless package.")
} else {
Package {
sender_country,
recipient_country,
weight_in_grams,
}
}
}

fn is_international(&self) -> bool {
// if self.sender_country != self.recipient_country {
// return true;
// // true // error, 'true' return to the func is_international, not outside
// }
// // return false;
// false // it can pass the compile

// if self.sender_country == self.recipient_country {
// false
// } else {
// true
// }

self.sender_country != self.recipient_country // most elegent way
}

fn get_fees(&self, cents_per_gram: i32) -> i32 {
cents_per_gram * self.weight_in_grams
// Something goes here...
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn fail_creating_weightless_package() {
let sender_country = String::from("Spain");
let recipient_country = String::from("Austria");

Package::new(sender_country, recipient_country, -2210);
}

#[test]
fn create_international_package() {
let sender_country = String::from("Spain");
let recipient_country = String::from("Russia");

let package = Package::new(sender_country, recipient_country, 1200);

assert!(package.is_international());
}

#[test]
fn create_local_package() {
let sender_country = String::from("Canada");
let recipient_country = sender_country.clone();

let package = Package::new(sender_country, recipient_country, 1200);

assert!(!package.is_international());
}

#[test]
fn calculate_transport_fees() {
let sender_country = String::from("Spain");
let recipient_country = String::from("Spain");

let cents_per_gram = 3;

let package = Package::new(sender_country, recipient_country, 1500);

assert_eq!(package.get_fees(cents_per_gram), 4500);
assert_eq!(package.get_fees(cents_per_gram * 2), 9000);
}
}

问题分析

在遇到这个问题之后,我自己分析不出来为什么会出现这样的错误。于是,我去找队伍里的学长进行咨询。学长在他的教程书里找到了答案。出现这个问题的原因是,rust的但if语句的返回值必然是返回单元(),因此,我不使用return返回true时,破坏了if语句的语法规则,导致出错。具体教程如下:

额外练习

在第一阶段中,我在我们专业的操作系统实验课程中,使用rust编程,完成了实验内容,具体完成了:

  • 生产者与消费者问题
  • 时间片轮转和优先级队列的调度算法
  • 文件系统的实现

总结

在第一阶段的学习中,我巩固了我们所掌握的rust基础,并开始学习操作系统相关的知识。在这一部分,我还培养了我的思考能力,深入思考了rust的安全与不安全的地方。同时,通过和群友的交流,我也展开了对更多其它知识点的思考,比如“Copy”、“Clone”特征和“Drop”的冲突之处。
最重要的是,我深深意识到:Talk is cheap, show me the code!

展望

目前,我已经开始了第二阶段部分内容的学习,希望可以顺利的完成第二阶段的所有内容。之后参与到第三阶段的项目中,继续提高自己的能力。

Rust rCore 二阶段学习总结

@FinishTime: 2024-06-01 01:39

二阶段学习时间表

  • 2024-04-29 二阶段rCore正式开始
  • 2024-05-06 用了一周时间,复习完学校的期中考试内容,并且做各种学校里的作业
  • 2024-05-10 期中考试完毕,继续写短头课要交的作业
  • 2024-05-16 完成学校内的作业,共计:计算机网络1篇论文3000字,iOS开发1篇伪大学生学位论文1.8万字,操作系统实验代码与报告6000字,web开发作业,算法分析与设计实验与报告6000字,人机交互作业报告3000字,毛概发言稿3800字
  • 2024-05-17 rCore的ch3完成
  • 2024-05-19 官宣二阶段结束,我在大约16:00,完成了ch4,之后一直尝试解决ch5和ch6,最后在晚上23:00,我还没有完成ch6,所以我决定先提交到300分以保证3阶段旁听资格。到23:40左右,我完成了前三部分的提交,获得300分,拿到三阶段旁听资格
  • 2024-05-20 一天时间,完成了老师留的数学建模的作业
  • 2024-05-21 继续写rCore的ch6,完成了大部分的核心功能代码,测试时,崩掉
  • 2024-05-22 两天时间,完成了“工程技术创新方法”的作业报告3000字和汇报ppt
  • 2024-05-25 完成了web开发的大作业
  • 2024-05-30 完成了rCore的第400分,也就是说,ch6卡了我差不多一整周时间,最后在万能的群友的帮助下成功了
  • 2024-05-31 早2:00,完成第500分,剩余一整天时间,完成了前面的所有报告,与本篇记录

    二阶段学习内容概述

  • ch1: 理解rCore的代码的运行环境,以及为什么它能够在裸机上运行起来
  • ch2: 实现“让rCore可以接收一系列程序”,即,使rCore变成了一个简单的批处理系统
  • ch3: 对rCore进行升级,使其变为分时系统
  • ch4: 扩充rCore的工作空间,并且提供内存管理
  • ch5: 提供进程的相关服务,即允许进程进行新建进程等操作
  • ch6: 加入文件系统,使得进程加载与rCore启动进行解耦,并能够增加其它存储设备
  • ch7: 增加进程间通信,为rCore引入管道和命令行参数
  • ch8: 通过增加线程,使进程解耦,实现rCore对并发的支持,同时提供死锁检测的服务

    问题复盘(我的吐槽!)

    通过上面的学习时间表可以看出,我主要花时间的地方在ch4和ch6上。ch4耗时近3天,ch6耗时近7天。

    ch4干了小3天,最后发现想复杂了

    其中,ch4我主要在考虑“是应该为os实现地址的映射,还是为task实现地址的映射”。我最开始想到,mmap和munmap的映射应该在os实现,只有os才直接具有对内存空间的全部可见性;而一个task应该只能看到自己的空间,不应该能够看到真实的映射后的空间。即,不应该由task直接调用mmap和munmap。

我尝试实现了用os跟踪所有的task,记录他们的虚拟空间和真实空间,当task有需要时,由os分配真实的物理空间,并完成映射。但是,这样导致os的复杂度增加,并且我完成的代码无法通过测试。

我尝试进行了调试,但发现难以判断到底出现了什么问题,所以我只好换用另一种破坏可见性但简单易实现的方法,不需要os再去跟踪所有的task的空间,而是让task直接能够看到自己的真实空间,并且允许task自己执行mmap和munmap。我仅仅把之前为os实现的核心逻辑代码复制粘贴过来,并配置了一些接口,就通过了ci测试。

ch6卡1周,真是折磨中的折磨

而在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 代码实现了一个通用的二叉堆,支持最小堆和最大堆的功能。下面是这个实现的核心组成部分的详细解释:

  1. 结构定义 (Heap<T>)

    • T: Default:这个约束确保堆中的元素可以被初始化为默认值。这对于确保堆的向量(items)可以在索引0处有一个占位符,简化索引计算非常有用。
    • count:跟踪堆中的元素数量。
    • items:一个向量,存储堆元素。堆使用基于1的索引以简化父子计算,索引0保持为虚拟。
    • comparator:一个函数指针,用于确定元素的排序,允许灵活定义最小堆或最大堆。
  2. 构造函数 (new(comparator))

    • 使用提供的比较函数初始化一个空堆,用于元素排序。
  3. 堆操作

    • 添加元素 (add):在堆的末尾插入一个新元素,然后将其向上移动以维护堆属性。这是通过比较添加的元素与其父元素并根据比较函数进行必要的交换来完成的。
    • 移除根元素 (next in Iterator implementation):移除根元素(根据堆的类型是最小或最大),将其替换为堆中的最后一个元素,然后将此元素向下移动以维护堆属性。在此过程中,堆的大小减少一个。
  4. 辅助函数

    • parent_idx, children_present, left_child_idx, right_child_idx, smallest_child_idx:这些辅助函数用于计算父和子的索引或检查子节点的存在,这对于在添加和删除操作期间导航堆至关重要。
  5. 类型特定的堆构造器 (MinHeapMaxHeap)

    • 这些是方便的包装器,提供了使用标准比较函数(对于最小堆是 a < b,对于最大堆是 a > b)轻松实例化 Heap<T> 的方法。
  6. 测试 (tests module)

    • 包含测试来验证堆的功能,包括最小堆和最大堆行为的测试。

关键特点:

  • 通用性和可重用性:堆可以处理实现了 Default 的任何类型 T,并可以使用在实例化时传递的任何比较函数,使其可以适应不同的排序需求。
  • 效率:插入和删除操作的时间复杂度为元素数量的对数,这是二叉堆的特点。
  • 迭代器集成:通过实现带有 next 方法的 Iterator 特质,允许堆在标凈 Rust 习惯用法中使用,如循环和其他基于迭代器的操作。

增强建议:

  • 错误处理:考虑可能失败的场景,如尝试从空堆中移除元素,并优雅地处理

前言

因为之前学C++结识的好友群中发了这个训练营的报名链接,想着正好OS还没有学就想着报名参加了:)。参加本次训练营之前只有csapp的计算机体系结构这一相关经验,对于Rust和RISC-V一无所知。编程能力上,主要是C++的服务器开发,做过几个分布式微服务项目,国外的一些公开课lab(CS144/CS106B/CS50等)。

总体来说第一阶段学习过程还是有点痛苦的,毕竟Rust学习曲线比较陡峭,不过也有许多特性能与C++等语言联系起来,帮助理解。在报名之后就开始翻阅先前训练营的资料开始自学Rust了,总体还是顺利的

一些链接🔗

第一阶段总结

第一阶段主要集中于Rust基础语法的学习,并完成rustlings练习题,其次是进行RISC-V指令集的学习

  • Rust因为是第一次上手,编码熟练度还不是很高,对于一些编码更高效的方式应用不熟练,算法和数据结构的实现遇到的磕绊较多
  • RISC-V基本指令比较好理解,实际编写还是差一些,能反应指令的行为,一些寄存器记不住
  • 计组和编译原理知识比较薄弱

Rust

Rust学习过程主要参考阅读了《Rust圣经》、《Rust by Example》、和Rust之旅
之后的时间主要是对《Rust圣经》中的进阶部分和基础部分进行阅读笔记并复习,基础部分难点主要集中在所有权和特征这两个知识点上,对于生命周期以及包和模块的讨论并不是特别深入。进阶部分都难哈哈哈哈,迭代器和智能指针还算比较好理解些,对于迭代器加闭包实现的函数式编程每次都让我觉得很强大,Cell和RefCell提供的内部可变性加上智能指针也给程序提供了很大的灵活性的前提下保证了一定的安全性。生命周期的使用也是一大难点了。之后还有unsafe Rust的使用以及宏编程(对比C/C++的难的多但功能性也更强大)

rustlings的基础语法部分不算特别难,不熟练的查一查就能写得出来。难点集中在test和algorithm章节,主要是test后半部分,一些环境变量的设置,以及rust实现数据结构算法所用到的unsafe rust和智能指针的操作。

之后尝试阅读Learning Rust With Entirely Too Many Linked Lists,和用Rust实现一些算法和数据结构来巩固,多敲一些代码提高熟练度,多查文档。

RISC-V学习

这部分知识学习首先是阅读了《计算机组成于设计:RISC-V版》的前两个章节,对RISC-V的基本指令和指令格式有一个了解,以及RISC-V的设计和计算机体系结构的8个伟大思想之间的联系。之前阅读csapp并完成lab实验,有些x86_64的经验,对于一些基本指令和作用的理解还算轻松。难点集中在原子指令上。之后主要阅读了《RISC-V手册》,主要重点是第十章的特权架构,涉及之后rCore的特权级切换操作以及虚拟内存的实现(SV39多级页表)

学习后可以看出RISC-V指令集的强大和简单,和x86_64不同的理念。六种基本指令格式,等长指令就能搞定一切,精妙的设计(就是有时候脑子转不过来)

查缺补漏

阅读操作系统导论,目前还在虚拟化部分,然后观看导学阶段的OS课录播,补足操作系统的理论知识和概念

前言

经过第一阶段的Rust练习和RISC-V语法的入门阅读,进入第二阶段,完成rCore的五个实验。第二阶段主要是进行操作系统的学习,并通过实验具象操作系统概念的实现。同时也在跟着Writing an OS in Rust写blog_os作为补充。

一些链接🔗

第二阶段总结

整体rCore实验难度集中在最后三个实验中,第一二章节引入简单操作系统的架构,从裸机平台构建libOS,将整个OS作为一个库给用户使用,到加入特权级切换,让操作系统找到任务并运行实现批处理OS,第三章加入yield系统调用支持多道程序,并加入时钟中断实现抢占式多任务构成多道程序与分时多任务系统。第四章开启地址空间,进一步完善了OS的安全性,应用不再有直接访问物理内存的能力,并且只能通过虚拟地址读写自己的地址空间,无法访问破坏其它应用的地址空间,同时也给操作系统提供内存管理的灵活性,通过分页机制更好地管理物理页帧,页表管理虚拟地址到物理页帧的映射转换。第五章进一步强化任务的概念为进程,加入一个简单的shell,使得用户与操作系统之间的交互能力提高,可以动态地执行应用。第六章实现了一个easyfs文件系统,对操作系统提供持久化存储。第七章通过文件系统的支持建立进程间通信,让不同应用组合在一起运行。第八章引入线程和并发的概念,让操作系统从宏观层面实现多个应用的并发执行,实现内核态管理用户态运行的线程,并支持互斥锁、信号量和条件变量这三种资源。

由于提早完成了第一阶段任务,得以提前开始第二阶段,前面通过阅读文档并复现代码熟悉整体架构,第二阶段正式开始后完成实验。

  • lab1就是简单实现获取当前执行任务的信息,为了维护系统调用次数需要为每个任务维护一个数据结构用以记录,在每次调用之前通过syscall_id查询自增。执行时间单位是ms,初始化为任务第一次调度的时间。
  • lab2因为地址空间的引入,首先是对先前的get_time和get_taskinfo接口进行修改以支持虚拟地址,实际就将用户传递的虚拟地址转换为对应的物理地址即可访问修改,不过需要注意可能会被分成两页。之后需要实现mmap和munmap接口,对虚拟内存的申请和释放,主要需要注意判断和处理可能错误,以及释放虚拟内存的步骤实现。
  • lab3首先需要实现spawn接口,实际就是fork+exec,需要注意的是不必像fork一样复制父进程的地址空间。然后需要实现stride调度算法,需要注意调度算法所需状态的初始化和维护,可以选择通过堆或者维护单调队列来实现,不过测例简单也可以直接暴力搜索,还需注意处理溢出情况,避免调度进入死循环,同时优先级的维护还需实现set_priority系统调用。
  • lab4是实现硬链接相关接口linkat和unlinkat,需要注意unlinkat对inode以及对应数据块的回收。
  • lab5是实现死锁检测,需要注意维护检测算法所需的per-thread状态以及互斥量和信号量资源。

额外题目还没实现,后续如果有时间再研究吧:)

我先说一下感觉吧,我感觉rust好难啊,真的,^^
之前是群里面发现了rcore这个文档,跟着看到第二章,发现不懂rust,然后就找教程学rust
后来发现了这个训练营,跟着这个训练营的课程进度,做rustlings练习题
一开始确实不难,到了迭代器,traits,结构体这里,差不多就看不懂了,属于是问了gpt也还是不懂的那种
后面的算法我倒是会,就是不会rust语法,然后问群友,虽然说不是每一题都问吧,也基本上是隔一两个就问
不管怎么说,通过群友们的解答和自己的努力,rustlings是做完了
总结:rust好难,^
^

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

初始rcore

第一次知道rcore来源于一篇知乎的帖子,当时沉迷于操作系统的学习,希望能写出一个自己的操作系统。但当时网络上更多的是对xv6的赞赏,rcore的仅仅是一句简单的提及。受限于当时初学计算机,只知语言C,不识Rust,不希望投入太多精力到语言的学习,因此并未细探rcore究竟。(当然,三分钟热度的我最后也没写完一个操作系统,希望这次能够完善地学完!)

直到去年了解到这个开源操作系统训练营,加上自学了rust的基础语法,对其很有兴趣。可惜秋冬季已经结营,加上网站上模糊的教学视频实在难以学习,便又戛然而止。直至今年四月初至,又闻开营之事(别问,问就是大数据推送),喜从心中来,急急忙忙地就上了船。

第一阶段总结

第一阶段主要是熟悉rust语法,前三十道题进展迅速,仅涉及简单语法;中间几十道题中,所有权和生命周期单列出来较为简单,但是复杂场景中的所有权问题令人头疼;最后几道算法题,除去算法本身的难度,rust对于所有权和借用的限制才是最大的难点,以至于最后纯粹是与编译器斗智斗勇,几乎快忘了实现逻辑和复杂度。

除此之外,有多道题目在完成过程中总是不解其意,仅仅是让其通过检测。知道提交通过之后,看到群友们的讨论才恍然大悟,但又懒于修改,还是默默学习的新的内容去吧。