0%

一阶段

前言

进入这个训练营很意外,因为是在同学建的就业群里面看到的连接,等进来之后从知道是训练营,本着既来之则安之的心态,我满怀期待的开启了第一阶段的rust学习 结果差点被所有权和引用绕晕 ,所有权和引用的学习真是让我又爱又恨,真是应了群友们的那句话:“编译之前,c++是世界上最好的语言!编译之后,rust是世界上最好的语言!”。

有几天我很想尝试用rust写leetcode的算法题,结果别说写了,我用模仿c++的代码都费劲,那几天rust真是给了我重重一击,但通过对rust的不断运用和学习,我也逐渐开始理解了为什么rust的安全性会如此之高 虽然换来的是编译期间的无数BUG ,但这并不妨碍它仍是一门值得学习和认真揣摩的一种语言。

Rust 学习总结

这三个星期主要集中于对于rust基础语法的学习,以及rustlings的110题,书籍方面,只是拜读了一下《RUST圣经》。好像这110题写过来,就没有几道题是改完一次就不报错直接通过的(不过看到群友们也是如此,我便感觉好受了很多)。
在rustlings的110题中,所有权、引用和生命周期的学习与运用几乎贯彻了全部的代码(要问我为什么是几乎,因为生命周期的题开始的晚哈哈哈哈),其中最麻烦的莫过于test的最后几题,虽然我知道群友们说的对,第一次见新东西难免会很懵,但是那几题直接硬控我两天。。。,相较之下,其它的一些题无非是对知识点的巩固和对算法的实现,对我而言反而没有很大的压力,按部就班来,一切都刚刚好。

二阶段(Rcore)

Rcore 学习总结

学了 Rcore 三个星期,把文档从前到后过了一遍,令我印象最深的还是前两章的内容,移除标准库依赖、构建裸机执行环境、对内存进行布局、将应用程序链接到内核以及进行特权级转换时对Trap上下文的数据保存与恢复,通过 __alltraps__restore 初始化应用程序以及执行吓一跳应用程序的过程都令我备感兴趣。

在实现 ch3 - ch8 时,最大的难点还是对代码含义的理解以及能否按需找到相关属性的存放位置,例如在 ch4 中虚拟地址到物理地址的转换方法,ch5 中通过 forkexec 的具体实现来推测 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
排序算法,链表,哈希表

2024春季开源操作系统训练营第一阶段总结报告

一个月之前其实跟着 The Rust Programming Language 学习过 Rust,从同学那里听过 rustlings,但没有做过。这次有机会搓了一下,对理解的帮助还是挺大的。

实验

rustlings 的实验其实挺简单的。基本上跟着编译器和提示都能独立完成。它的 check 也不严格,数据点少、过编译即可。基本上完成质量全靠自觉

Rust 语言的约束,在最后 10 道算法题上还是体现得比较棘手的。一部分数据结构的实现需要套 Arc 甚至 unsafe。这也是所谓“必要的牺牲”吧。此外,生命周期的处理感觉还是不够优雅。有些奇怪的感觉。

除了这 10 题以外,其他部分基本都是语言基本语法、内容的介绍,也包括包管理器的使用,cargo test 的使用。相对来说都是比较基础的内容。只要跟着做就很简单。

总结

rustlings 是个很好的 Rust 学习项目,很适合计划做 rCore Lab 的同学入门 Rust 使用。是这样的。没错。

希望后期也能继续跟下去。

常见概念

类型

  • 类型推导与标注

      1. Rust是静态类型语言
      1. 编译器可以根据变量的值和上下文的使用方式自动推导出变量的类型
      1. 在无法推导出变量类型的情况下需要手动标注类型
  • 基本类型

    • 最小化原子类型(无法被结构)

    • 分类

      • 数值类型

        • 整数类型

          • 整数是没有小数部分的数字

          • 类型定义的形式统一为:有无符号 + 类型大小(位数)。无符号数表示数字只能取正数和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)来处理这些情况

            • 可以使用 is_nan() 等方法,可以用来判断一个数值是否是 NaN
        • 序列

          • 用来生成连续的数值

          • 只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型

        • 运算

          • 数字运算

            • +

            • -

            • *

            • /

            • %

          • 位运算

      • 布尔类型

        • 有两个可能的值:true 和 false

        • 布尔值占用内存的大小为 1 个字节

      • 字符类型

        • Unicode编码 占4Byte
      • 单元类型,即(),唯一的值也是()

        • main 函数返回这个单元类型 ()

        • 没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function )

        • 内存占用为0!

      • 语句

        • 执行一些操作但是不会返回一个值

        • ;结尾

      • 表达式

        • 求值后返回一个值

        • 表达式不能包含分号

        • 表达式如果不返回任何值,会隐式地返回一个()

        • Rust函数就是表达式

      • 函数

        • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}

        • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可

        • 每个函数参数都需要标注类型

          • Rust 是强类型语言,因此需要你为每一个函数参数都标识出它的具体类型
        • 返回

          • 函数的返回值就是函数体最后一条表达式的返回值,也可以使用 return 提前返回
        • 发散函数:返回值为!,表示永不返回

      • 方法

        • 使用impl关键字定义

        • Rust 的对象定义和方法定义是分离的

        • Self

          • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少

          • &self 表示该方法对 Rectangle 的不可变借用

          • &mut self 表示可变借用

          • &self 其实是 self: &Self 的简写

        • 使用方法代替函数的优点

          • 不用在函数签名中重复书写 self 对应的类型

          • 代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大

        • 关联函数

          • 定义在 impl 中且没有 self 的函数被称之为关联函数
  • 复合类型

    • 字符串

      字符串是由字符组成的连续集合
      Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。

      • String

      • &str

      • String&str 的转换

        • String&str

          • &s

          • &s[..]

          • s.as_str()

        • &strString

          • 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
          用法见格式化输出

      • 转义

        • 可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符
      • 操作UTF-8字符串

        • 遍历字符

        • 遍历字节

    • 元组

      • 长度固定

      • 顺序固定

      • 可以使用模式匹配或者 . 操作符来获取元组中的值

        • 模式匹配

        • .运算符

      • 创建

    • 结构体

      • 组成

        • 通过关键字 struct 定义

        • 一个清晰明确的结构体 名称

        • 几个有名字的结构体 字段

      • 创建实例

        • 初始化实例时,每个字段都需要进行初始化

        • 初始化时的字段顺序不需要和结构体定义时的顺序一致

        • 简化创建:当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化

      • 访问字段

        • 通过 . 操作符即可访问结构体实例内部的字段值,也可以修改它们

        • 必须要将结构体实例声明为可变的,才能修改其中的字段

        • 不支持将某个结构体某个字段标记为可变

      • 结构体更新语法..old_val

        • 必须在结构体的尾部使用

      • 内存布局

      • 元组结构体

        • 结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体
      • 单元结构体

        • 没有字段和属性

        • 不关心属性只关心行为

      • 结构体数据的所有权

        • 想要结构体拥有自己的数据而为借用别人的数据

        • 在借用数据时需要声明生命周期

      • 使用 #[derive(Debug)] 来打印结构体的信息

        • 使用格式化输出{:?}需要实现Display trait
      • 使用dbg!

    • 枚举

      • 概念

        • 允许通过列举可能的成员来定义一个枚举类型

        • 枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。

      • 枚举值

        • 通过::操作符来访问枚举类下的具体成员

        • 枚举的成员默认是从 0 开始递增的,每个成员的值都是前一个成员值加 1

      • 数据关联到枚举成员

      • `Option枚举

        • 用于处理空值
    • 数组

      • array

        • 速度很快但是长度固定

        • 三要素

          • 长度固定

          • 元素必须有相同的类型

          • 依次线性排列

        • 声明

          • 数组的元素类型要统一,长度要固定

          • 初始化一个某个值重复出现 N 次的数组

        • 访问数组元素

          • 因为数组是连续存放元素的,因此可以通过索引的方式来访问存放其中的元素

          • 越界访问

            • 会导致panic
        • 数组元素为非基础类型

          • 需要类型实现std::marker::Copy trait

          • 或者使用std::array::from_fn

        • 数组切片

          • 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置

          • 创建切片的代价非常小,因为切片只是针对底层数组的一个引用

          • 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理

      • Vector

        • 可动态增长的但是有性能损耗
  • 集合类型

    • String

    • Vector

      • 创建

        • Vec::new

        • vec![]

        • Vec::from

      • 更新

        • push
      • 读取

        • 通过下标索引访问。

        • 使用 get 方法。

      • 迭代

      • 插入

        • insert

        • append

      • 删除

        • pop

        • remove

        • drain

      • 截断

        • truncate
      • 保留指定条件的元素

        • retain
      • 存储不同类型的元素

        • 通过特征对象或枚举实现
      • 排序

        • 稳定的排序

          • sort

          • sort_by

        • 非稳定排序

          • sort_unstable

          • sort_unstable_by

    • HashMap

      • 导入

        • use std::collections::HashMap;
      • 创建

        • new

        • 使用迭代器和 collect 方法创建

      • 插入

        • insert
      • 查询

        • get
      • 更新

      • 所有权

        • 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 可以直接跳出当前整个循环

      • break 可以单独使用,也可以带一个返回值,有些类似 return

模式匹配

match

  • match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性

  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同

  • X | Y,类似逻辑运算符 或,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可

  • match 本身也是一个表达式,因此可以用它来赋值

if let

  • 只关心某一个值是否存在

  • 只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match

matches

  • 可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true or false

模式绑定

  • 模式匹配的另外一个重要功能是从模式中取出绑定的值

  • @ 操作符可以让我们将一个与模式相匹配的值绑定到新的变量上

穷尽匹配

  • 使用_ 通配符处理不需要匹配的值

解构Option

模式

模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match 表达式联用,以实现强大的模式匹配能力。

  • 组成

    • 字面值

    • 解构的数组、枚举、结构体或者元组

    • 变量

    • 通配符

    • 占位符

  • 使用情景

    • match

    • if let

    • while let

    • for

    • let

    • 函数参数

全模式列表

变量绑定与解构

变量绑定

可变性

  • 变量默认不可变

  • 使用mut关键字声明可变

  • 常量:不仅默认不可变并且自始至终不可变

    • 使用const标注

    • 必须标明类型

使用_可以使编译器忽略未被使用的变量

解构

  • let表达式可以从一个相对复杂的变量中匹配出该变量的一部分内容

解构式赋值

  • 在 Rust 1.59 版本后,可以在赋值语句的左式中使用元组、切片和结构体模式了

变量遮蔽

  • Rust允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的

所有权与借用

所有权

  • 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),因为调用它们会消耗迭代器。

      1. collect()collect() 方法用于消费迭代器并将其转换为一个集合类型,例如 VecHashMapString。它会遍历整个迭代器,并将每个元素收集到一个集合中。
      1. count()count() 方法用于消费迭代器并计算其中元素的数量。它返回一个 usize 类型的值,表示迭代器中元素的个数。
      1. nth()nth() 方法用于消费迭代器并获取其中的第 n 个元素。它返回一个 Option 类型的值,表示可能存在的第 n 个元素。
      1. last()last() 方法用于消费迭代器并获取其中的最后一个元素。它返回一个 Option 类型的值,表示可能存在的最后一个元素。
      1. max() / min()max()min() 方法分别用于消费迭代器并获取其中的最大值和最小值。它们返回一个 Option 类型的值,表示可能存在的最大或最小值。
      1. sum() / product()sum()product() 方法分别用于消费迭代器并计算其中元素的和或乘积。它们返回一个值,表示迭代器中所有元素的和或乘积。
      1. all() / any()all()any() 方法用于消费迭代器并检查其中的元素是否满足某种条件。all() 方法要求所有元素都满足条件,而 any() 方法只要求至少有一个元素满足条件。它们返回一个布尔值,表示是否所有或任意元素满足条件。
      1. find() / position()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 和 RefMut,通过 RefCell 访问。( RefCell 是一个在运行时而不是在编译时执行借用规则的类型)。

    • 有单一所有者

    • 允许在运行时执行不可变或可变借用检查

  • Arc<T> 原子引用计数类型,用于多线程

  • Cow<T> Clone on Write

    写入时克隆的智能指针。

    Cow 类型是一种智能指针,具有写入时克隆的功能:它可以封装借用数据并提供不可变的访问权限,在需要突变或所有权时,可以懒惰地克隆数据。通过 “借用 “特性,该类型可与一般借用数据一起使用。

    Cow 实现了 Deref,这意味着你可以直接在它所包含的数据上调用非变异方法。如果需要进行变异,to_mut 将获取一个可变异的引用,并在必要时克隆该引用。

    如果需要引用计数指针,请注意 Rc::make_mut 和 Arc::make_mut 也可以提供写入时克隆功能。

智能指针的trait

  • Deref trait

    实现 Deref trait 允许我们重载 解引用运算符(dereference operator)*(不要与乘法运算符或通配符相混淆)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
    Deref trait,由标准库提供,要求实现名为 deref 的方法,其借用 self 并返回一个内部数据的引用

  • DerefMut trait

    用于重载可变引用的 * 运算符。

  • Drop trait

并发

Thread

  • 使用spawn创建新线程

    • 返回JoinHandle,JoinHandle 是一个拥有所有权的值,当对其调用 join 方法时,它会等待其线程结束。
  • 使用join阻塞线程

  • 使用move关键字来将闭包捕获的值的所有权传递给另一个线程

线程通信

  • 使用mpsc::channel信道进行通信

    • 发送者tx

      • send

        • 可以clone
    • 接收者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 的默认实现,这类似于子类覆盖从父类继承方法的实现。

  • 多态

    作为一种语言设计的解决方案,继承在许多新的编程语言中逐渐不被青睐,因为它经常有分享过多代码的风险。子类不应总是共享父类的所有特征,但是继承始终如此。它还引入了在子类上调用方法的可能性,这些方法可能没有意义,或因为方法不适用于子类而导致错误。此外,一些语言只允许单一继承(意味着子类只能从一个类继承),进一步限制了程序设计的灵活性。

trait对象

泛型

在函数中使用泛型

在结构体中使用泛型

在枚举中使用泛型

方法中使用泛型

const 泛型 针对值的泛型

默认泛型类型参数

  • 当使用泛型类型参数时,可以为其指定一个默认的具体类型,例如标准库中的 std::ops::Add 特征

  • 减少实现的样板代码

  • 扩展类型但是无需大幅修改现有的代码

Trait

定义

  • 定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为

  • 如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。

定义特征

  • 使用trait关键字

实现特征

默认实现

  • 可以在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法

使用特征作为函数参数

特征约束(trait bound)

Where 约束

函数返回中的 impl Trait

  • 可以通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征

  • 只能有一个具体的类型

通过 derive 派生特征

  • 形如 #[derive(Debug)] 的代码已经出现了很多次,这种是一种特征派生语法,被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能

Trait 对象

  • 特征对象指向实现了 特征的类型的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。

  • 动态分发

    • 当使用特征对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。
  • 特征对象的限制

    • 不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的

      • 方法的返回类型不能是 Self

      • 方法没有任何泛型参数

关联类型

  • 关联类型是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型

生命周期

生命周期是引用的有效作用域

主要作用是避免悬垂指针

为了保证 Rust 的所有权和借用的正确性,Rust 使用了一个借用检查器(Borrow checker),来检查我们程序的借用正确性

在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析

unsafe

作用

  • 解引用裸指针

  • 调用一个 unsafe 或外部的函数

  • 访问或修改一个可变的静态变量

  • 实现一个 unsafe 特征

  • 访问 union 中的字段

raw pointer

  • 可以绕过 Rust 的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针

  • 并不能保证指向合法的内存

  • 可以是 null

  • 没有实现任何自动的回收 (drop)

  • 创建裸指针是安全的行为,而解引用裸指针才是不安全的行为

  • 创建裸指针

    • 基于值的引用

    • 基于地址

    • 基于智能指针创建裸指针

调用 unsafe 函数或方法

用安全抽象包裹 unsafe 代码

使用 extern 函数调用外部代码

  • 原理:Foreign Function Interface (FFI)

访问或修改可变静态变量

  • 在Rust中 全局变量就是静态变量

  • 静态变量的引用的生命周期只能是’static

  • 有绝对地址

实现不安全 trait

访问联合体中的字段

学习背景

本人是一名大二学生。

去年三月自学过 rust,使用 rust 写过两三个练习的小程序,所以对 rust 的语法和所有权机制还是比较熟悉的。且从去年年底起,对操作系统相关的知识非常感兴趣,在刷了南京大学 Jyy 老师的课后对操作系统有了大概的概念(虽然并没有做 lab)。

今年通过同学,了解到这个夏令营。居然是用 rust 来写操作系统!遂叫上两三个队友一起加入了。

程序安全

内存安全一直是程序设计中一个难以解决的问题。从原始的 gets 溢出攻击,到最近的 xz 供应链后门事件。多少都利用了内存天生的不安全的特性。

而 rust 做的事情,是将天生不安全的代码世界,划出一部分 safe 区域。通过在编译器上的工作,保证了程序员只要在 safe 区域内编写代码,程序就可以保证内存安全。

虽然 safe rust 的表达能力有些时候并不能让人满意,并且在写代码的时候经常和编译器搏斗()。但在我看来,这实际上是把后续 debug 所需要的精力,提前到了编写代码的时候。

所有权

这个世界上的编程语言一般分为两种:有 gc 的,和没有 gc 的

在 c/c++中,macllo 或 new 一段内存在 heap 上后,需要程序员自己在不需要这段内存之后,释放这段内存。随后可以让别的代码块使用这段内存。

这样做的问题在于:

  1. 很多时候,程序员可能会忘记释放被分配的内存
  2. 也有些时候,已经被释放的内存会被程序员误操作,再次释放

于是我们有了 gc——

在 java 这样的语言中,gc(内存回收器)会定期暂停程序的一切行为,回收当前没有被引用的对象所占用的内存。这样的语言还有 go。

这样就不需要程序员来负责内存的释放了,也就不存在多次释放和不释放的问题了。(虽然循环依赖依旧会导致内存溢出……,但这并没有那么危险)

但 gc 其实是一个比较“重”的东西,并且会定期“暂停世界”。这在互联网服务(Java/Go)的领域并不关键,但在操作系统/嵌入式这样的领域中,底层并不喜欢 gc 这样的设计。

rust 利用所有权机制,从另一个角度缓解了内存问题。之所以说是缓解,因为 rust 也没有解决内存溢出的问题。

rust 中,一个变量可以拥有一段 heap 上的内存的“所有权”。当这个变量结束其【生命周期】后,会自动调用其 drop() 方法

而变量的“所有权”可以通过函数传递,程序员可以将一个变量的所有权移交给函数内部,然后获得函数移交出来的变量的所有权。

也可以把变量借给函数,通过 mut 关键字。让函数可以在不获得所有权的前提下,访问变量的值。

在函数需要修改变量时,也可以把变量可变借用给函数。

值得一提的是,同一时间可以存在多个借用,或者一个可变借用。这在很大程度上避免了数据竞争

在多线程程序中,也可以通过 Mutex,Arc 等数据结构,进一步避免并行程序中的数据竞争

rust 哲学

不同于 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》经历了几个过程:

  1. 作为一个非科班的 coder,我的知识体系里面没有操作系统的任何具体知识,所以第零章对我来说特别新鲜
  2. 然后,进入第一章后,我对除了 Rust 之外的几乎所有的东西都很陌生,比如指令二进制表示与各种含义。
    所以为了弄清楚它们,需要经常问 AI 和查 riscv 手册。
  3. 第二、三章:依旧是在弄清楚汇编、寄存器这些知识上狂补,总结了一些笔记发布在自己的博客上,然后
    当周问 kimi 的时候,无意发现我的博客链接被作为那个问题的参考资料之一,哈哈哈,有成就感了。
  4. 第四章开始是一个分水岭,对汇编之类的细节可以觉得不必死抠,但马上被抽象的概念绕进去了,然后我不再记
    笔记了,因为主要是理解为主,只关注“是什么、怎么做”这些重点,弄清楚内存映射的基本步骤。
  5. 随后的章节,我开始以精简版的 tutorial 为主,重点看怎么做,从代码出发,然后不能理解的地方才去看书里的解释。

花时间最多的地方是内存映射和银行家算法上:

  • 内存映射:很多概念(物理地址、虚拟地址、页表、页表项)同时出现,所以需要梳理它们,然后才能串起来理解
  • 银行家算法:第八章的编程题花了很久写,首先一开始没理解题目的算法描述,网上搜索一番之后,依然糊里糊涂的。
    然后课件中给了我一些启发,但依然没理解。尤其是,本地测例能跑通,在 CI 上又不能通过,但 CI 没报告哪个测例不通过,
    所以只能修改一点,推送给 CI。最后我在理解测例的基础上,在纸上整理思路,结合日志去思考死锁发生和没发生的情况,
    弄懂了算法描述,最后修改了几次代码,通过了。

最后,还有一些时间花在工具上:比如学习 tmux、gdb,以及 git 合并冲突上。

背景

沉迷 LOL 十年,归来仍是白银 的经常翘课但毕业后有自己复习408专业课的血亏科班老东西,写过 vue,nodejs,python,C

(欢迎找我干活,可以为爱发电)

第一阶段做的事就是 90% 刷 rustlings,加上 10% 复习了下基础知识

rust

一言以蔽之,我永远喜欢 C 🌝🌝🌝🌝🌝🌚

安全诚可贵,自由价更高。但是为了完成训练营还是学习一下

类型太多要匹配,所有权转移,可变引用与不可变引用的限制 是耽误我最长时间的,机制理解但是感觉用起来就是很不方便。

参考资料

圣经也就图一乐,真学rust还得 官方文档,写代码神器:官方手册

官方文档:理解一些机制和基础用法,做 rustlings 里模块分类和调用方式、编译器参数 等题目时用到的

官方手册:各种类型包含的函数和用法,不会做了就翻一翻,在左边列表找一找,经常能有意想不到的收获

具体内容

对我有用的新学到的且值得值得记录的 主要是 Option 的几个方法

  • as_mut() :&mut Option -> Option<&mut T>.
  • as_ref() : &Option -> Option<&T>.
  • as_deref_mut() : Option (or &mut Option) to Option<&mut T::Target>

前面题目里用到的时候只是按照提示修改,没有细究功能和用法,后面算法题才发现很好用

还有一些用起来不熟练或者还没用但感觉以后可能会有用的,具体用法等实践中再研究了

  • Rc, Arc, Cell, RefCell

数据结构

包括 rustlings 里新加入了一些算法题,小复习了一下涉及到的 BST, heap 的内容,

其他 BFS, DFS, stack,queue,图,都太基本了没啥好说的,DFS 连边的类型都不用区分 =。=
建议下次提升到 912 难度😉(bushi)
加点 最短路径、KMP啥的 可以有🥺

二叉搜索树

  • 特性:中序,任意 parent 不小于左子树,不大于右子树。
    亦即中序遍历序列单调非降

    完全二叉树

  • 平衡因子处处非负:左子树满树

  • 逻辑节点与物理元素依层次遍历次序彼此对应:

Parent(i) = ( i-1 ) >> 1
LChild(i) = ( 1 + ( i << 1 )
RChild(i) = ( 1 + i ) << 1

完全二叉堆

  • 用物理上的向量实现逻辑上的完全二叉树
  • 维持偏序而非全序,即 val [i] <= val [Parent(i)]

操作

  • 插入 + 上滤 O(lgn):只与 Parent 对比并直接互换
  • 删除 + 下滤 O(lgn):用末节点填充根节点(维持结构性)+ swap(e, max(Lchild, Rchild)) (维持堆序性)

操作系统

当年就这门课学得最好,前几个月刷了下 《Three Easy Pieces》,看一半发现还是 rCore指导书 最好,理论与实践相结合,讲解详细清晰,操作切实可行,跟着刷了两章

第一阶段用不上,剩下的等到第二阶段再补啦

第一阶段总结

关于Rustlings

  • rustlings的110道题全部完成
  • 有关rustlings中tests7和tests8中关于build脚本那块儿仍然不是很清楚
  • 其他个人感觉掌握的还行

关于Rust的学习

在第一阶段,我对RUST的学习主要参考了以下开源书目:RUST圣经,RUST程序设计语言,布朗大学的RUST教程。结合上述书目以及训练营课程的讲解可以基本搞明白RUST中的几个关键概念:所有权,生命周期等。

最后总结与展望

虽然已经顺利通过了第一阶段的学习,但是我明显感觉到我对RUST还并不是很熟悉,因此后需还需要补强对RUST的了解。同时也希望自己能坚持下来,在第二阶段和第三阶段学到更多的东西。