2024开源操作系统训练营第一阶段总结-HCHogan
半年前开始就一直在写rust语言,写了很多练手的项目,所以第一阶段的收获有限,比较印象比较深刻的就是结构体初始化的那个语法:
1 | User { |
没有用过,这次正好复习到了。
进入这个训练营很意外,因为是在同学建的就业群里面看到的连接,等进来之后从知道是训练营,本着既来之则安之的心态,我满怀期待的开启了第一阶段的rust学习 结果差点被所有权和引用绕晕 ,所有权和引用的学习真是让我又爱又恨,真是应了群友们的那句话:“编译之前,c++是世界上最好的语言!编译之后,rust是世界上最好的语言!”。
有几天我很想尝试用rust写leetcode的算法题,结果别说写了,我用模仿c++的代码都费劲,那几天rust真是给了我重重一击,但通过对rust的不断运用和学习,我也逐渐开始理解了为什么rust的安全性会如此之高 虽然换来的是编译期间的无数BUG ,但这并不妨碍它仍是一门值得学习和认真揣摩的一种语言。
这三个星期主要集中于对于rust基础语法的学习,以及rustlings的110题,书籍方面,只是拜读了一下《RUST圣经》。好像这110题写过来,就没有几道题是改完一次就不报错直接通过的(不过看到群友们也是如此,我便感觉好受了很多)。
在rustlings的110题中,所有权、引用和生命周期的学习与运用几乎贯彻了全部的代码(要问我为什么是几乎,因为生命周期的题开始的晚哈哈哈哈),其中最麻烦的莫过于test的最后几题,虽然我知道群友们说的对,第一次见新东西难免会很懵,但是那几题直接硬控我两天。。。,相较之下,其它的一些题无非是对知识点的巩固和对算法的实现,对我而言反而没有很大的压力,按部就班来,一切都刚刚好。
学了 Rcore 三个星期,把文档从前到后过了一遍,令我印象最深的还是前两章的内容,移除标准库依赖、构建裸机执行环境、对内存进行布局、将应用程序链接到内核以及进行特权级转换时对Trap上下文的数据保存与恢复,通过 __alltraps 和 __restore 初始化应用程序以及执行吓一跳应用程序的过程都令我备感兴趣。
在实现 ch3 - ch8 时,最大的难点还是对代码含义的理解以及能否按需找到相关属性的存放位置,例如在 ch4 中虚拟地址到物理地址的转换方法,ch5 中通过 fork 和 exec 的具体实现来推测 spawn 的实现, ch6中对 sys_stat 的实现必须借助 vfs 的相关属性去进行计算, ch8中在线程创建,线程移除,上锁,解锁时都需要对自定义的检测死锁结构体 ProcessLock 进行相应改变。
通过本次学习,我对 Rcore 的实现原理有了更深刻的了解,同时也认知到自己对计算机底层的相关知识掌握的并不牢固,大有几分囫囵吞枣之态,因此,在后续的学习中,我打算对计算机底层的相关知识进行系统的学习,例如汇编语言、操作系统、计算机组成原理等,以便能够更好的理解 Rcore 的实现原理。
Day1
搭建rust练习的运行环境
Day2
let声明变量(不可改变) mut让变量可改变 例:let x=5;let mut x=5;
1.基本数据类型 i32…u8,16,32…usize…char,bool
2.复合类型 数组:[1,2,3] 元组(1,”s”,true,”double”)
loop循环,变量作用域,返回值
引用和借用,slice,自定义结构体和元组结构体,vector,hashmap
第二天完成的都是基础
Day3
enum创建实例和初始化
option枚举,只有存在some()和不存在none
match控制流,类似其他语言的switch进行匹配, _ => 是math的通用符匹配
模块的声明和创建
泛型 T trait方法
生命周期(我觉得比较难的)
&i32 //引用
&’a i32 &’a mut i32 //带有显式生命周期的引用
通过使用&’a符号,声明声明周期。在函数传值的过程中,形参和实参生命周期要一样
BOX
并发编程:(第二个觉得比较有意思的)
使用spawn创建新线程 let handle = thread::spawn(||{…….})
使用join等待所有线程结束 handle.join().unwrap()
将move闭包和线程一起使用 et handle = thread::spawn(move||{…….})
Day4
第四天完成剩下的练习题
stack,queue
排序算法,链表,哈希表
一个月之前其实跟着 The Rust Programming Language 学习过 Rust,从同学那里听过 rustlings,但没有做过。这次有机会搓了一下,对理解的帮助还是挺大的。
rustlings 的实验其实挺简单的。基本上跟着编译器和提示都能独立完成。它的 check 也不严格,数据点少、过编译即可。基本上完成质量全靠自觉
Rust 语言的约束,在最后 10 道算法题上还是体现得比较棘手的。一部分数据结构的实现需要套 Arc 甚至 unsafe。这也是所谓“必要的牺牲”吧。此外,生命周期的处理感觉还是不够优雅。有些奇怪的感觉。
除了这 10 题以外,其他部分基本都是语言基本语法、内容的介绍,也包括包管理器的使用,cargo test 的使用。相对来说都是比较基础的内容。只要跟着做就很简单。
rustlings 是个很好的 Rust 学习项目,很适合计划做 rCore Lab 的同学入门 Rust 使用。是这样的。没错。
希望后期也能继续跟下去。
类型推导与标注
基本类型
最小化原子类型(无法被结构)
分类
数值类型
整数类型
整数是没有小数部分的数字
类型定义的形式统一为:有无符号 + 类型大小(位数)。无符号数表示数字只能取正数和0,而有符号则表示数字可以取正数、负数还有0。
有符号数以补码形式存储
整形字面量可以用下表的形式书写
默认使用i32
整数溢出
当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)
在当使用 –release 参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add
如果使用 checked_* 方法时发生溢出,则返回 None 值
使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
使用 saturating_* 方法,可以限定计算后的结果不超过目标类型的最大值或低于最小值
浮点类型
默认浮点类型是 f64,在现代的 CPU 中它的速度与 f32 几乎相同,但精度更高
浮点数根据 IEEE-754 标准实现。f32 类型是单精度浮点型,f64 为双精度
避免在浮点数上测试相等性
当结果在数学上可能存在未定义时,需要格外的小心
对于数学上未定义的结果,Rust 的浮点数类型使用 NaN (not a number)来处理这些情况
序列
用来生成连续的数值
只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型
运算
数字运算
+
-
*
/
%
位运算
布尔类型
有两个可能的值:true 和 false
布尔值占用内存的大小为 1 个字节
字符类型
单元类型,即(),唯一的值也是()
main 函数返回这个单元类型 ()
没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function )
内存占用为0!
语句
执行一些操作但是不会返回一个值
以;结尾
表达式
求值后返回一个值
表达式不能包含分号
表达式如果不返回任何值,会隐式地返回一个()
Rust函数就是表达式
函数
函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
每个函数参数都需要标注类型
返回
发散函数:返回值为!,表示永不返回
方法
使用impl关键字定义
Rust 的对象定义和方法定义是分离的
Self
self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
&self 表示该方法对 Rectangle 的不可变借用
&mut self 表示可变借用
&self 其实是 self: &Self 的简写
使用方法代替函数的优点
不用在函数签名中重复书写 self 对应的类型
代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大
关联函数
复合类型
字符串
字符串是由字符组成的连续集合
Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
String
&str
String 与 &str 的转换
String 到 &str
&s
&s[..]
s.as_str()
&str 到 String
String::from(“hello,world”)
“hello,world”.to_string()
底层实现
类型为u8类型的字节数组
使用UTF-8编码,为了防止奇怪的返回值,不允许对字符串使用索引
使用切片时要额外注意,不要切出没有意义的切片,并且会导致程序崩溃
常用方法
push 追加字符char
push_str追加字符串字面量
insert插入单个字符char
insert_str插入字符串字面量
replace_range接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串
pop删除并返回字符串的最后一个字符
存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None
remove 删除并返回字符串中指定位置的字符
存在返回值,其返回值是删除位置的字符串
按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误
truncate删除字符串中从指定位置开始到结尾的全部字符
无返回值
按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误
clear清空字符串
replace适用于 String 和 &str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串
replacen该方法可适用于 String 和 &str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。
使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型
使用 format! 连接字符串
适用于 String 和 &str
用法见格式化输出
转义
操作UTF-8字符串
遍历字符
遍历字节
元组
长度固定
顺序固定
可以使用模式匹配或者 . 操作符来获取元组中的值
模式匹配
.运算符
创建
结构体
组成
通过关键字 struct 定义
一个清晰明确的结构体 名称
几个有名字的结构体 字段
创建实例
初始化实例时,每个字段都需要进行初始化
初始化时的字段顺序不需要和结构体定义时的顺序一致
简化创建:当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化
访问字段
通过 . 操作符即可访问结构体实例内部的字段值,也可以修改它们
必须要将结构体实例声明为可变的,才能修改其中的字段
不支持将某个结构体某个字段标记为可变
结构体更新语法..old_val
必须在结构体的尾部使用
内存布局
元组结构体
单元结构体
没有字段和属性
不关心属性只关心行为
结构体数据的所有权
想要结构体拥有自己的数据而为借用别人的数据
在借用数据时需要声明生命周期
使用 #[derive(Debug)] 来打印结构体的信息
{:?}需要实现Display trait使用dbg!宏
枚举
概念
允许通过列举可能的成员来定义一个枚举类型
枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。
枚举值
通过::操作符来访问枚举类下的具体成员
枚举的成员默认是从 0 开始递增的,每个成员的值都是前一个成员值加 1
数据关联到枚举成员
`Option
数组
array
速度很快但是长度固定
三要素
长度固定
元素必须有相同的类型
依次线性排列
声明
数组的元素类型要统一,长度要固定
初始化一个某个值重复出现 N 次的数组
访问数组元素
因为数组是连续存放元素的,因此可以通过索引的方式来访问存放其中的元素
越界访问
数组元素为非基础类型
需要类型实现std::marker::Copy trait
或者使用std::array::from_fn
数组切片
切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
创建切片的代价非常小,因为切片只是针对底层数组的一个引用
切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理
Vector
集合类型
String
Vector
创建
Vec::new
vec![]
Vec::from
更新
读取
通过下标索引访问。
使用 get 方法。
迭代
插入
insert
append
删除
pop
remove
drain
截断
保留指定条件的元素
存储不同类型的元素
排序
稳定的排序
sort
sort_by
非稳定排序
sort_unstable
sort_unstable_by
HashMap
导入
创建
new
使用迭代器和 collect 方法创建
插入
查询
更新
所有权
HashMap 的所有权规则与其它 Rust 类型没有区别:
若类型实现 Copy 特征,该类型会被复制进 HashMap,因此无所谓所有权
若没实现 Copy 特征,所有权将被转移给 HashMap 中
如果使用引用类型放入 HashMap 中,请确保该引用的生命周期至少跟 HashMap 活得一样久
f32 和 f64 浮点数,没有实现 std::cmp::Eq 特征,因此不可以用作 HashMap 的 Key
允许引用集合中部分连续的元素序列,而不是引用整个集合。
创建切片的语法,使用方括号包括的一个序列:[开始索引..终止索引]
开始索引是切片中第一个元素的索引位置
终止索引是最后一个元素后面的索引位置
分支控制
if else
if语句是表达式 可以通过if表达式的返回值进行变量绑定(赋值
可以使用else if实现更复杂的条件分支判断
match
循环控制
for
往往使用集合的引用形式
不使用引用的话,所有权会被转移(move)到 for 语句块中,后面就无法再使用这个集合
对于实现了copy trait的数组会对元素进行拷贝
如果想在循环中,修改该元素,可以使用 mut 关键字
获取元素索引:使用迭代器
while
loop
简单的无限循环
loop 是一个表达式,因此可以返回一个值
使用 continue 可以跳过当前当次的循环,开始下次的循环
使用 break 可以直接跳出当前整个循环
matchmatch 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性
match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
X | Y,类似逻辑运算符 或,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可
match 本身也是一个表达式,因此可以用它来赋值
if let只关心某一个值是否存在
只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match
matches宏模式匹配的另外一个重要功能是从模式中取出绑定的值
@ 操作符可以让我们将一个与模式相匹配的值绑定到新的变量上
模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match 表达式联用,以实现强大的模式匹配能力。
组成
字面值
解构的数组、枚举、结构体或者元组
变量
通配符
占位符
使用情景
match
if let
while let
for
let
函数参数
变量默认不可变
使用mut关键字声明可变
常量:不仅默认不可变并且自始至终不可变
使用const标注
必须标明类型
_可以使编译器忽略未被使用的变量let表达式可以从一个相对复杂的变量中匹配出该变量的一部分内容
在 Rust 1.59 版本后,可以在赋值语句的左式中使用元组、切片和结构体模式了
Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
当把一个变量赋值(?)给另一个变量时,如果没有实现copy trait,则称为移动(move),会将所有权转移给后一个变量。
Rust 永远也不会自动创建数据的 “深拷贝”
任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的
部分move
当解构一个变量时,可以同时使用 move 和引用模式绑定的方式。当这么做时,部分 move 就会发生:变量中一部分的所有权被转移给其它变量,而另一部分我们获取了它的引用。
在这种情况下,原变量将无法再被使用,但是它没有转移所有权的那一部分依然可以使用,也就是之前被引用的那部分。
引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址。使用&创建引用
使用*解引用
引用的两种类型
可变引用
使用&mut创建引用
只能同时存在一个
不可变引用
使用&创建不可变引用
可变引用与不可变引用不能同时存在
悬垂引用(Dangling References)
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。
获取变量的引用,称之为借用(borrowing)
概念
是可以保存在一个变量中或作为参数传递给其他函数的匿名函数
可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。
不同于函数,闭包允许捕获被定义时所在作用域中的值。
闭包会捕获定义其所处环境中的值并使用
闭包关联于小范围的上下文,通常情况下可以自动推断值的类型,也可以手动标注
捕获引用或者移动所有权
闭包可以通过三种方式捕获其环境,它们直接对应到函数获取参数的三种方式:不可变借用,可变借用和获取所有权。闭包会根据函数体中如何使用被捕获的值决定用哪种方式捕获。
不可变借用
可变借用
获取所有权
即使闭包体不严格需要所有权,如果希望强制闭包获取它用到的环境中值的所有权,可以在参数列表前使用 move 关键字
将被捕获的值移出闭包和 Fn trait
一旦闭包捕获了定义它的环境中一个值的引用或者所有权(也就影响了什么会被移 进 闭包,如有),闭包体中的代码定义了稍后在闭包计算时对引用或值如何操作(也就影响了什么会被移 出 闭包,如有)。闭包体可以做以下任何事:将一个捕获的值移出闭包,修改捕获的值,既不移动也不修改值,或者一开始就不从环境中捕获值。闭包捕获和处理环境中的值的方式影响闭包实现的 trait。Trait 是函数和结构体指定它们能用的闭包的类型的方式。取决于闭包体如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait。
FnOnce 适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值移出闭包体的闭包只实现 FnOnce trait,这是因为它只能被调用一次。
FnMut 适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次。
Fn 适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要。
迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。
在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果。
创建
迭代器都实现了一个叫做 Iterator 的定义于标准库的 trait
next 是 Iterator 实现者被要求定义的唯一方法。next 一次返回迭代器中的一个项,封装在 Some 中,当迭代器结束时,它返回 None。
从 next 调用中得到的值是 vector 的不可变引用。iter 方法生成一个不可变引用的迭代器。如果我们需要一个获取 v1 所有权并返回拥有所有权的迭代器,则可以调用 into_iter 而不是 iter。类似的,如果我们希望迭代可变引用,则可以调用 iter_mut 而不是 iter。
消费迭代器的方法
调用 next 方法的方法被称为 消费适配器(consuming adaptors),因为调用它们会消耗迭代器。
collect() 方法用于消费迭代器并将其转换为一个集合类型,例如 Vec、HashMap 或 String。它会遍历整个迭代器,并将每个元素收集到一个集合中。count() 方法用于消费迭代器并计算其中元素的数量。它返回一个 usize 类型的值,表示迭代器中元素的个数。nth() 方法用于消费迭代器并获取其中的第 n 个元素。它返回一个 Option 类型的值,表示可能存在的第 n 个元素。last() 方法用于消费迭代器并获取其中的最后一个元素。它返回一个 Option 类型的值,表示可能存在的最后一个元素。max() 和 min() 方法分别用于消费迭代器并获取其中的最大值和最小值。它们返回一个 Option 类型的值,表示可能存在的最大或最小值。sum() 和 product() 方法分别用于消费迭代器并计算其中元素的和或乘积。它们返回一个值,表示迭代器中所有元素的和或乘积。all() 和 any() 方法用于消费迭代器并检查其中的元素是否满足某种条件。all() 方法要求所有元素都满足条件,而 any() 方法只要求至少有一个元素满足条件。它们返回一个布尔值,表示是否所有或任意元素满足条件。find() 和 position() 方法用于消费迭代器并查找满足某种条件的元素。find() 方法返回第一个满足条件的元素,而 position() 方法返回满足条件的第一个元素的索引。它们都返回一个 Option 类型的值,表示可能存在的满足条件的元素或索引。产生其他迭代器的方法
Iterator trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),它们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
map():map() 方法用于将迭代器中的每个元素应用一个函数,并将结果转换为另一种类型。它返回一个新的迭代器,其中包含了应用函数后的结果。
filter():filter() 方法用于过滤迭代器中的元素,只保留符合特定条件的元素。它接受一个闭包作为参数,该闭包返回一个布尔值,用于判断元素是否应该被保留。
take():take() 方法用于从迭代器中获取指定数量的元素,并返回一个新的迭代器。它接受一个usize类型的参数,表示要获取的元素数量。
skip():skip() 方法用于跳过迭代器中的前几个元素,并返回一个新的迭代器。它接受一个usize类型的参数,表示要跳过的元素数量。
enumerate():enumerate() 方法用于在迭代器中同时追踪元素的索引和值。它返回一个元组 (index, value) 的迭代器,其中 index 是索引,value 是元素的值。
zip():zip() 方法用于将两个迭代器的元素一一配对,并返回一个新的迭代器。它将两个迭代器的对应位置的元素组合成元组 (value1, value2)。
fold():fold() 方法用于将迭代器中的所有元素折叠(或归约)为一个单一的值。它接受一个初始值和一个闭包作为参数,该闭包将累积器和迭代器中的每个元素作为输入,并返回一个新的累积器。
flat_map():flat_map() 方法用于将每个元素转换为另一个迭代器,并将所有的结果展平成一个单一的迭代器。它的功能类似于 map(),但是它额外地将结果展平。
panic!
todo!
unimplemented
Box
有单一所有者
允许在编译时执行不可变或可变借用检查
Rc
允许相同数据有多个所有者
仅允许在编译时执行不可变借用检查
Ref
有单一所有者
允许在运行时执行不可变或可变借用检查
Arc<T> 原子引用计数类型,用于多线程
Cow<T> Clone on Write
写入时克隆的智能指针。
Cow 类型是一种智能指针,具有写入时克隆的功能:它可以封装借用数据并提供不可变的访问权限,在需要突变或所有权时,可以懒惰地克隆数据。通过 “借用 “特性,该类型可与一般借用数据一起使用。
Cow 实现了 Deref,这意味着你可以直接在它所包含的数据上调用非变异方法。如果需要进行变异,to_mut 将获取一个可变异的引用,并在必要时克隆该引用。
如果需要引用计数指针,请注意 Rc::make_mut 和 Arc::make_mut 也可以提供写入时克隆功能。
Deref trait
实现 Deref trait 允许我们重载 解引用运算符(dereference operator)*(不要与乘法运算符或通配符相混淆)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
Deref trait,由标准库提供,要求实现名为 deref 的方法,其借用 self 并返回一个内部数据的引用
DerefMut trait
用于重载可变引用的 * 运算符。
Drop trait
使用spawn创建新线程
使用join阻塞线程
使用move关键字来将闭包捕获的值的所有权传递给另一个线程
使用mpsc::channel信道进行通信
发送者tx
send
接收者rx
recv
try_recv
mpsc:mutiple producer single consumer
共享内存
mutex并发原语
使用lock获取锁
使用Arc原子引用计数类型
Send trait
Sync
封装
抽象
继承
Rust中不存在继承
选择继承有两个主要的原因。其一是代码复用:您可以为一种类型实现特定的行为,继承可将其复用到不同的类型上。在 Rust 代码中可以使用默认 trait 方法实现来进行有限的代码复用,就像示例 10-14 中在 Summary trait 上增加的 summarize 方法的默认实现。任何实现了 Summary trait 的类型都可以使用 summarize 方法而无须进一步实现。这类似于父类有一个方法的实现,继承的子类也拥有这个方法的实现。当实现 Summary trait 时也可以选择覆盖 summarize 的默认实现,这类似于子类覆盖从父类继承方法的实现。
多态
作为一种语言设计的解决方案,继承在许多新的编程语言中逐渐不被青睐,因为它经常有分享过多代码的风险。子类不应总是共享父类的所有特征,但是继承始终如此。它还引入了在子类上调用方法的可能性,这些方法可能没有意义,或因为方法不适用于子类而导致错误。此外,一些语言只允许单一继承(意味着子类只能从一个类继承),进一步限制了程序设计的灵活性。
当使用泛型类型参数时,可以为其指定一个默认的具体类型,例如标准库中的 std::ops::Add 特征
减少实现的样板代码
扩展类型但是无需大幅修改现有的代码
定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为
如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。
trait关键字可以在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法
可以通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征
只能有一个具体的类型
特征对象指向实现了 特征的类型的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。
动态分发
特征对象的限制
不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的
方法的返回类型不能是 Self
方法没有任何泛型参数
关联类型是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型
解引用裸指针
调用一个 unsafe 或外部的函数
访问或修改一个可变的静态变量
实现一个 unsafe 特征
访问 union 中的字段
可以绕过 Rust 的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
并不能保证指向合法的内存
可以是 null
没有实现任何自动的回收 (drop)
创建裸指针是安全的行为,而解引用裸指针才是不安全的行为
创建裸指针
基于值的引用
基于地址
基于智能指针创建裸指针
原理:Foreign Function Interface (FFI)
在Rust中 全局变量就是静态变量
静态变量的引用的生命周期只能是’static
有绝对地址
本人是一名大二学生。
去年三月自学过 rust,使用 rust 写过两三个练习的小程序,所以对 rust 的语法和所有权机制还是比较熟悉的。且从去年年底起,对操作系统相关的知识非常感兴趣,在刷了南京大学 Jyy 老师的课后对操作系统有了大概的概念(虽然并没有做 lab)。
今年通过同学,了解到这个夏令营。居然是用 rust 来写操作系统!遂叫上两三个队友一起加入了。
内存安全一直是程序设计中一个难以解决的问题。从原始的 gets 溢出攻击,到最近的 xz 供应链后门事件。多少都利用了内存天生的不安全的特性。
而 rust 做的事情,是将天生不安全的代码世界,划出一部分 safe 区域。通过在编译器上的工作,保证了程序员只要在 safe 区域内编写代码,程序就可以保证内存安全。
虽然 safe rust 的表达能力有些时候并不能让人满意,并且在写代码的时候经常和编译器搏斗()。但在我看来,这实际上是把后续 debug 所需要的精力,提前到了编写代码的时候。
这个世界上的编程语言一般分为两种:有 gc 的,和没有 gc 的
在 c/c++中,macllo 或 new 一段内存在 heap 上后,需要程序员自己在不需要这段内存之后,释放这段内存。随后可以让别的代码块使用这段内存。
这样做的问题在于:
于是我们有了 gc——
在 java 这样的语言中,gc(内存回收器)会定期暂停程序的一切行为,回收当前没有被引用的对象所占用的内存。这样的语言还有 go。
这样就不需要程序员来负责内存的释放了,也就不存在多次释放和不释放的问题了。(虽然循环依赖依旧会导致内存溢出……,但这并没有那么危险)
但 gc 其实是一个比较“重”的东西,并且会定期“暂停世界”。这在互联网服务(Java/Go)的领域并不关键,但在操作系统/嵌入式这样的领域中,底层并不喜欢 gc 这样的设计。
rust 利用所有权机制,从另一个角度缓解了内存问题。之所以说是缓解,因为 rust 也没有解决内存溢出的问题。
rust 中,一个变量可以拥有一段 heap 上的内存的“所有权”。当这个变量结束其【生命周期】后,会自动调用其 drop() 方法
而变量的“所有权”可以通过函数传递,程序员可以将一个变量的所有权移交给函数内部,然后获得函数移交出来的变量的所有权。
也可以把变量借给函数,通过 mut 关键字。让函数可以在不获得所有权的前提下,访问变量的值。
在函数需要修改变量时,也可以把变量可变借用给函数。
值得一提的是,同一时间可以存在多个借用,或者一个可变借用。这在很大程度上避免了数据竞争
在多线程程序中,也可以通过 Mutex,Arc 等数据结构,进一步避免并行程序中的数据竞争
不同于 c/c++,也不同于 java/go。
rust 在自动内存管理和性能之间,选择了我全都要。
rust 的 std 遵守着零成本抽象,即意味着同样的功能,使用 rust 和手动编写的代码,性能上是一致的。
这也能让人更放心的使用 rust 自带的各种数据结构和功能。
OS 在我看来,是所有软件工程的基础,也是软件工程中的软件工程。
对硬件: 操作系统只是运行在硬件上的一个程序
对软件: 操作系统为软件创造了一个虚拟的环境,让软件程序有一种自己正掌握着整个硬件的错觉。并且,操作系统为软件提供了一系列系统调用。
jyy 老师的课给我的最大的收获,是将一切程序看作一个状态机。
而操作系统也是一个状态机: 一个管理状态机的状态机。
从这个视角看待软件世界,能很大程度上避免panic(因为看到繁杂的代码中各种函数,很难不让人头疼和恐慌)
如果你想体验这种感觉,可以 git clone 一个kernel(?)
世界上的一切都是魔法
法师只需要喊出法咒/作出动作,世界就会回应他,在现实中展现神迹。
程序员只要敲击键盘,电脑就会回应他,屏幕上显示出对应的字母。
这中间的一切都是被封装好的。就像是魔法一样。
从普通软件的视角看,他在 systemcall 时,对于该程序来说,下一个瞬间,它所在的世界,就根据其所呼唤的 systemcall 回应了它。
编写操作系统,其实就是在编制普通软件的世界。
事实上,操作系统也只是在运用硬件和sbi提供给它的魔法罢了
以上是在我眼中的 rust 和 操作系统 ,作为一个初学者,难免出现错漏和片面。
如果读者发现什么需要指正和交流的,欢迎联系我。oveln@outlook。
以及OvBlog
本篇博客一部分内容复制于我的个人博客,略有修改。
这个训练营几年前就知道了,看过了好几届它的宣传,以前也进入过一次网络教室,但并没有报名。
主要是不知道这个东西是什么,和自身领域隔得很远。再加上平常写软件程序,不知道这些底层知识和算法也一样可以写代码。
今年才报名,也没什么原因,想了解就参与咯。对此没什么学习目标,能学到我想学到的东西就够了,
学到不想继续了解为止。(实话)
这阶段的任务是完成 rustlings。
花了两个多小时完成,完成了 rustlings 前 90 多道题。那些题在几年前就做过了,中途感觉在浪费时间。
第二天完成 rustling 剩余十多道题:算法题主要花在查那些算法知识上,因为平常写代码不会用到算法。
写完算法题,开始觉得没浪费时间了。
提前进入第二阶段去自学 rCore 那本书,除了熟悉的 Rust 部分,其余部分我一无所知,看得非常吃力。
不过这不属于第一阶段的总结了。仍在学习路上。
这阶段对我来说太过于挑战了,围绕《rCore-Tutorial-Book-v3》经历了几个过程:
花时间最多的地方是内存映射和银行家算法上:
最后,还有一些时间花在工具上:比如学习 tmux、gdb,以及 git 合并冲突上。
沉迷 LOL 十年,归来仍是白银 的经常翘课但毕业后有自己复习408专业课的血亏科班老东西,写过 vue,nodejs,python,C
(欢迎找我干活,可以为爱发电)
第一阶段做的事就是 90% 刷 rustlings,加上 10% 复习了下基础知识
一言以蔽之,我永远喜欢 C 🌝🌝🌝🌝🌝🌚
安全诚可贵,自由价更高。但是为了完成训练营还是学习一下
类型太多要匹配,所有权转移,可变引用与不可变引用的限制 是耽误我最长时间的,机制理解但是感觉用起来就是很不方便。
圣经也就图一乐,真学rust还得 官方文档,写代码神器:官方手册
官方文档:理解一些机制和基础用法,做 rustlings 里模块分类和调用方式、编译器参数 等题目时用到的
官方手册:各种类型包含的函数和用法,不会做了就翻一翻,在左边列表找一找,经常能有意想不到的收获
对我有用的新学到的且值得值得记录的 主要是 Option 的几个方法
前面题目里用到的时候只是按照提示修改,没有细究功能和用法,后面算法题才发现很好用
还有一些用起来不熟练或者还没用但感觉以后可能会有用的,具体用法等实践中再研究了
包括 rustlings 里新加入了一些算法题,小复习了一下涉及到的 BST, heap 的内容,
其他 BFS, DFS, stack,queue,图,都太基本了没啥好说的,DFS 连边的类型都不用区分 =。=
建议下次提升到 912 难度😉(bushi)
加点 最短路径、KMP啥的 可以有🥺
Parent(i) = ( i-1 ) >> 1
LChild(i) = ( 1 + ( i << 1 )
RChild(i) = ( 1 + i ) << 1
操作
当年就这门课学得最好,前几个月刷了下 《Three Easy Pieces》,看一半发现还是 rCore指导书 最好,理论与实践相结合,讲解详细清晰,操作切实可行,跟着刷了两章
第一阶段用不上,剩下的等到第二阶段再补啦