0%

花了大约一周的时间基本看完了rust圣经,两天刷完rustling,发现rust真是把各种安全考虑(内存,数据…)直接暴露给用户了。

😯rust三大支柱:

  • 没有垃圾回收的内存安全(内存安全)
  • 没有数据争用的并发(数据安全)
  • 没有开销的抽象(性能)

[1]😯无GC: 随时回收内存啊,这不c++ RAII么,NLL比RAII更强!

[2]😯所有权、借用:最有特色的一章。这是直接把读写锁加在语言层了是吧。

[3]生命周期:最难理解的一章。NLL还好理解,给参数和返回值手动标生命周期怎么突然魔幻起来了🥵,好在编译器会帮我们做标注。

[4]泛型、trait:最惊艳的一章。泛型?大伙都有啊;trait? 这不Java的接口(c++虚函数)么,但是确实优化掉了继承,难道这就是组合优于继承么🧐;

😯零开销抽象!!

组合:用”has-a”(有什么或用什么)去替代”is-a”(是什么)

零开销抽象:极度强调运行时性能,把所有解释抽象的工作都放在编译时

[5]函数式编程:这不所有权+借用版本的c++迭代器和lambda表达式么

[6]Box智能指针、循环引用:这不所有权+借用版本的c++ unique_ptr,shared_ptr,weak_ptr的么

[7]unsafe:c++裸指针,但仍然无法逃离所有权和借用

[8]多线程与并发:大伙都有,但是更严格了啊,还得时刻考虑数据的位置和所有权转移


1.变量解构

(a, mut b): (bool,bool)
1
2
3
4
5
6
7
8

2.数值

```61_f32```表示f32类型的61

3.as 转类型

例如```'a' as u8

4.range

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

5.```字符串```通过硬编码,而```String类型```在堆上

6.申请堆内存后,通过```栈中的堆指针```进行访问

7.一个值只能拥有一个所有者

> **变量在离开作用域后,就自动释放其占用的内存**

> 可以多个常量引用,但是只能有一个可变引用
>
> 可变引用和常量引用不能同时存在
>
> 引用的作用域在最后一次使用后时结束(编译器的优化)

```rust
let s1 = String::from("hello");
let s2 = s1;
//s1将所有权移交s2

8.字符串String 和 &str

str是内置类型,String是标准库类型

&str字符串切片

中文3个字节

9.尽量使用迭代方式访问数组 使用下标访问每次有越界检查

i in &v{}```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

和 for i in v{} 的区别?

> 如果不使用引用的话,所有权会被转移(move)到 `for` 语句块中,后面就无法再使用这个集合了:(

> 对于实现了 `copy` 特征的数组(例如 [i32; 10] )而言, `for item in arr` 并不会把 `arr` 的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 `arr` 。



10.模式匹配

```rust
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}

() 表示返回单元类型与所有分支返回值的类型相同,所以当匹配到 _ 后,什么也不会发生。

11.函数参数的三种传入方式:转移所有权、可变借用、不可变借用

12.只要闭包捕获的类型都实现了Copy特征的话,这个闭包就会默认实现Copy特征。

1
2
3
4
5
6
7
8
9
10
11
12
// 拿所有权
let s = String::new();
let update_string = move || println!("{}", s);

exec(update_string);
// exec2(update_string); // 不能再用了

// 可变引用
let mut s = String::new();
let mut update_string = || s.push_str("hello");
exec(update_string);
// exec1(update_string); // 不能再用了

😎仅实现 FnOnce 特征的闭包在调用时会转移所有权

13.调用第一个参数为self的成员函数会移交所有权,实例不能再使用

1
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {    // --snip-- }

14.数据内存布局

VecStringHashMap都是固定大小的类型,都是对堆上数据的引用,引用的大小是固定的,即栈上的引用类型是固定大小的

DST:不定长类型,在代码中直接使用 DST 类型,将无法通过编译。

Rust 中常见的 DST 类型有: str[T]dyn Trait它们都无法单独被使用,必须要通过引用或者 Box 来间接使用

dyn Trait表示它所指定的 Trait,确切的原始类型被删除,补全 Trait 对象指针所需的信息是 vtable 指针,被指向的对象的运行时的大小可以从 vtable 中动态地获取。

1
2
3
4
struct MySuperSliceable<T: ?Sized> {
info: u32,
data: T,
}

?Sized 是一个特殊的 trait bound,表示泛型类型 T 可以是非固定大小类型。

T 既可以是普通的固定大小类型(如 i32, u64),也可以是动态大小类型(如 str 或者 dyn Trait

#[repr(c)] 表示字段的顺序、大小和对齐方式与你在 C 或 C++ 中期望的完全一样。

通过 FFI (Foreign Function Interface,不同语言交互)边界的类型都应该有repr(C),因为 C 是编程世界的语言框架。

15.堆栈

  • 小型数据,在栈上的分配性能和读取性能都要比堆上高
  • 中型数据,栈上分配性能高,但是读取性能和堆上并无区别,因为无法利用寄存器或 CPU 高速缓存,最终还是要经过一次内存寻址
  • 大型数据,只建议在堆上分配和使用

栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或 CPU 高速缓存

将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上,由于寄存器、CPU 缓存的原因,它的性能将更好.

16.智能指针往往都实现了 DerefDrop 特征

Box可当智能指针用

17.Deref

  • 把智能指针(比如在库中定义的,Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的 &v
  • 把多重&,例如 &&&&&&&v,归一成 &v
1
2
// 由于 String 实现了 Deref<Target=str>    
let owned = "Hello".to_string(); // str
  • T: Deref<Target=U>,可以将 &T 转换成 &U,也就是我们之前看到的例子
  • T: DerefMut<Target=U>,可以将 &mut T 转换成 &mut U
    • T: Deref<Target=U>,可以将 &mut T 转换成 &U

18.

Rc 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 Arc

Arc 是线程安全的,Atomic Reference Count

Arc并不允许直接修改其中的数据,应该在Arc内部包装一个mutex


RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常

borrow() borrow_mut()

  • RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时

Cell对于实现了Copy的类型。如 Cell 使用.get()获取一个copy后的数据,使用.set()修改原来的数据 => 进而实现了可变借用和不可变借用同时存在。


Weak

使用Weak来解决循环引用导致的内存泄漏问题,也可直接使用unsafe 裸指针解决 裸指针没有所有权转移

通过RC创建Weak,或者直接创建Weak


NonNull [一个非空,协变的裸指针]

19.unsafe

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

unsafe代码块加在解引用裸指针的周围

将引用转化为裸指针是一种😎再借用

一旦开始使用裸指针,就要尝试着只使用它,不然就会突破rust的引用规则和栈借用规则。产生不好的后果

内部可变性

一个不可变引用 &UnsafeCell<T> 指向一个可以改变的数据,这就是内部可变性。

20.再借用
f

21.Pin

自引用最麻烦的就是创建引用的同时,值的所有权会被转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct SelfRef<'a> {
value: String,

// 该引用指向上面的value
pointer_to_value: &'a str,
}

fn main(){
let s = "aaa".to_string();
let v = SelfRef {
value: s, // 所有权转移
pointer_to_value: &s // 所有权已经被转移,不能再借用
};
}

22.线程安全

  • 实现Send的类型可以在线程间安全的传递其所有权
  • 实现Sync的类型可以在线程间安全的共享(通过引用)

23.常量可以在任意作用域进行定义,其生命周期贯穿整个程序的生命周期

Rust 要求必须使用unsafe语句块才能访问和修改static变量

lazy_static懒初始化静态变量,之前的静态变量都是在编译期初始化的,因此无法使用函数调用进行赋值,而lazy_static允许我们在运行期初始化静态变量!但是定义的静态变量都是不可变引用

Rust为我们提供了Box::leak方法,它可以将一个变量从内存中泄漏(听上去怪怪的,竟然做主动内存泄漏),然后将其变为'static生命周期,最终该变量将和程序活得一样久,因此可以赋值给全局静态变量CONFIG

24.unwarp通过unwarp() 取出Result<i32,ParseIntError>中的i32(潜在panic)

使用?方法后接收option或result时,遇到err,None直接返回

25.iterator

三种迭代器类型

IntoIter 类型迭代器的 next 方法会拿走被迭代值的所有权,IterMut 是可变借用, Iter 是不可变借用。

🥵26.链表

不能直接将head的所有权直接移交给新节点的next。take将head偷出,填入None,返回head

加泛型后head为Option<Box<Node>>


不使用as_ref会将head的所有权移入map函数内(map的函数签名为self,转移所有权),这样&node.elem就是返回map函数内的局部变量的引用。

使用as_ref将Option<Box<Node>> 转为 Option<&Box<Node>>解决问题

27.逆变协变

一个variable被拆分成‘读’跟‘写’,其中读的部分是协变,写的部分是逆变,可读可写的var则是不变

协变:需要动物的地方给一个猫

逆变:需要猫的地方给一个动物

不变:不能转换

rust中函数返回的是协变的,参数是逆变的

rust中生命周期和泛型T都有协变逆变性

*mut T 对于T是不变的 &’a mut T 对于T是不变的

引入NonNUll包裹T,这时T是协变的

在构建list时,使用*mut Node 会导致不能使用其协变性。用NonNull引入协变性

例子:

1
2
3
4
5
6
7
8
9
10
11
12
fn assign<T>(input: &mut T, val: T) {
*input = val;
}
fn main() {
let mut hello: &'static str = "hello";
{
let world = String::from("world");
assign(&mut hello, &world);
}
println!("{hello}");
}
// 这里传入assign的T为hello,hello是一个&'static str类型,即T是&'static str,它是不变的,不能用&'world str类型的val(第二个参数)来

NonNull<T> 是关于 T 协变的。这意味着如果你有一个 NonNull<T>,并且 UT 的子类型,那么你可以将 NonNull<T> 转换为 NonNull<U>。这种属性在泛型编程中非常有用,尤其是在处理继承或类型转换时。

子类型:

  1. 引用的子类型关系:在 Rust 中,引用 &T 是其指向的类型 T 的子类型。这意味着,如果有一个函数期望接受类型 T 的参数,你可以传递类型为 &T 的参数给它,而编译器会自动进行引用的解引用操作。这样的设计避免了数据的不必要的拷贝,并且使得代码更加灵活。
  2. 安全的子类型关系:在 Rust 中,如果一个类型 B 实现了 trait A,那么 B 就是 A 的子类型。这种子类型关系通过 trait 来实现。比如,如果类型 String 实现了 trait AsRef<str>,那么 String 就是 str 的子类型。这种关系允许你在期望 AsRef<str> 类型参数的地方传递 String 类型的值。
  3. Enum 的子类型关系:在 Rust 中,Enum 可以有多个变体(variants),这些变体可以拥有不同的数据类型。对于一个枚举类型来说,它的每个变体都可以被认为是它本身的子类型。这种设计在模式匹配和组合类型时非常有用。

28.module

当使用 mod 语句导入模块时,Rust 自动 为它创建一个模块命名空间

mod logging; 可以使用logging::xxx访问该module下的内容

extern

1.调用外部代码

1
2
3
extern "C" {
fn ctanf(z: Complex) -> Complex;
}

2.外部调用rust代码

1
pub extern "C" fn rustfn1() { }

29.多态

  • 泛型是一种 编译期多态 (Static Polymorphism),在编译一个泛型函数的时候,编译器会对于所有可能用到的类型进行实例化并对应生成一个版本的汇编代码,在编译期就能知道选取哪个版本并确定函数地址,这可能会导致生成的二进制文件体积较大;

  • 而 Trait 对象(也即上面提到的 dyn 语法)是一种 运行时多态 (Dynamic Polymorphism),需要在运行时查一种类似于 C++ 中的 虚表 (Virtual Table) 才能找到实际类型对于抽象接口实现的函数地址并进行调用,这样会带来一定的运行时开销,但是更为灵活。

第一阶段前面做Rustlings的时候忘记写了,知道最后第一阶段快结束的这几天才想起来。故没法清晰的记录每天的学习情况。

其实之前就有想学rust的打算了,所以看过一点rust圣经也加了一点群。这个训练营也正是Rust群里看到的项目。

之前是走C++方向的,自从了解了Rust的之后,个人认为这才是modern C++。Rust保留了C++绝大部分的特性的同时,还提供了丰富的标准库、强大的编译器和方便构建的包管理器、强大的纠错机制。C++就像高冷的冰山学姐一样给你Segmental Default,报错还贼难看懂。Rust就像温柔的小学妹一样帮你Debug。

总的来说第一阶段的收获挺大的,之前没有用rustlings学习,知识看了点语法就去leetcode写题。却不知道有rustlings这种好东西。

期待后面训练营的OS课。

前言

當年在學校學操作系統的回憶痛苦而蒼白,還記得剛上了兩堂課就知道這位老師的課上了也沒意義,估計他都不曉得自己在說什麼,整個學期下來,我連操作系統該有什麼樣的功能都很含糊,這個現象在同學之間普遍存在,一間高等學校好幾屆學生的系統知識就被一個毫無素養的老師給毀了,我每每回想起來感到很悲哀。

還很印象很深刻的是,他說操作系統太過複雜,要在課堂上寫一個是很不可能的。但過了幾年,我瞭解到了國外知名的 xv6 項目,明白自己完全被糊弄了。雖然離開校園已經好幾年,在 rust.cc 論壇上看到 rcore 訓練營的消息,我非常興奮地報了名。

第一階段總結

Rust 語言我還算是有些基礎,用它寫過一些簡單的程序。前面幾十道題過得還挺快,但當初學習沒能覆蓋到方方面面,在生命週期及 unsafe 操作裸指針上還是卡了一些關。這實在是 rust 真正獨創的地方,而操作記憶體在 OS 開發相必是重中之重,也期待在接下來的課程能進一步熟悉。

期許

下班後能利用的時間實在有限, rustlings 居然花了一個禮拜才搞定,接下來的第二階段必定挑戰更大,需要做好時間安排,潛心專研。

第二階段總結

第二階段的難度果然遠高於第一階段,我幾乎是壓線才能完成任務,但這堂課真的接近我理想中操作系統課程該有的樣子。rCore-Tutorial-Guide 2024 有些太簡略了,我第三章之後就改看 rCore-Tutorial-Book 第三版 了,內容十分翔實,可以自學,美中不足的是敘述好用長句,有些語言西化,若能再稍微雕琢文句就更好了。

還有三週實在是太短了,我沒辦法逐章逐節的閱讀完教材,只能先看 lab 問題來選讀章節,要寫 lab 其實不需要用到所有教材內容,例如 condvar 我就沒搞懂了。問答題是個很好的補充,迫使我多讀了一些源碼才能作答,對加深理解很有幫助。為了寫 lab 我讀懂了五成代碼,再寫問答題又多懂了一兩成吧,是希望三週能延長為五週六週,再增加 lab 及問答題的廣度。

期許

二階段已經如此艱辛,三階段更接近實戰任務,想必更具挑戰,我打算選擇 hypervisor ,當年學過的一點虛擬機知識要再撿起來了。

前言

作为一个长期依靠自学的科班选手,目前只系统学习过 Berkeley CS61A/61B,参与过炼丹项目,系统编程最近才起步(xv6)。加入训练营主要是借助 ddl 逼迫自己学习 Rust 以及 OS,拓展技术栈,同时也在为明年暑期实习和后续可能的 PL 研究增加知识储备。(能够遇到志同道合的朋友就更好了^^)

心得

之前早已听说 Rust 语言层面的安全性,但在实际上手之后才真正理解了这一点:严格的所有权、类型、生命周期、借用等机制,不断优化的编译时检查和对人类友好的报错信息/修改建议,包管理、配置管理的先进,以及零成本抽象的构建逻辑等,形成了区别于其他主流语言的独特风格。更加难得可贵的是 Rust 高质量的官方教程,讲解深入浅出,配合实际案例,一般路过萌新也能够顺利入门。

在 Rust 语法学习以及 Rustlings 练习中,主要难点有:读写/所有权机制、泛型/生命周期、智能指针、并发,以及一些 Safe Rust 目前难以解决的问题及,例如链表、自引用和循环引用。

Rust 不同于 C++ 等默认 copy semantics(复制语义) 的语言,转而默认对未实现 Copy trait 的类型 move semantics(移动语义),这一点使得很多初学者望而却步:即使编写简单逻辑程序,仍然需要对 Rust 的类型以及内存模型有一定了解,控制引用生命周期,同时明确程序每一处的逻辑,才能在 Safe Rust 的约束下通过编译。Rust 生命周期机制显式实现了对引用的有效期规定,从而有效控制借用范围。Rust 经过多次版本迭代,也引入了一系列语法糖,例如 Deref coersion、? match Result 等,一定程度降低了心智负担,不必成为“语言律师”才能上手开发。

总的来说,Rust 是一门高度现代化,注重内存安全和性能的系统级开源编程语言,同时具有逐渐完善的编译器和第三方库生态,可谓未来可期。

学习目标

第二阶段开始前,通过 Berkeley CS61C 以及 xv6 lab 提升对 RISC-V 指令集以及 OS 基本运行逻辑的理解。

尽可能在预计时间内完成第二阶段任务,同时备战期末(

契机

事情的起因是在威神群里看到的清华操作系统训练营。一个教你用rust写os的训练营!

受到了炸药哥、cjj等多位体系结构大佬的影响,且本身自己就有一个想把cs和ai结合起来的梦想。而且了解到开源操作系统研究组研究方向涉及高性能计算,于是果断报名。

其实我自己也不知道是否对操作系统感冒,对rust的兴趣也不及cpp。我只是喜欢了解计算机科学的底层,加之想找hpc的暑期实习但简历却石沉大海(悲),且也想扎实自己os基础便来了这里。

rust学习曲线真的好陡峭(痛苦面具)

从rust零基础到完成rustlings大概用了两周时间。

看过rust圣经,rust程序设计语言,做过rust最佳实践的练习,在b站看过网课,还尝试做过两个项目: 用rust实现基本数据结构和用rust实现lua解释器。 真的想说一句:”我xxx!” (请读者自行想象)

其实依旧是有很多不明白的东西,比如链表的布局其实我是似懂非懂的,只是照搬了它的布局。但为何这样布局当时并没有太清楚。
这样的地方还有很多,就且留到以后慢慢体会。毕竟我是来学os的(嘿嘿),我始终坚信一点:所有的疑问都会在不断学习的过程中找到答案!

幻想成为英雄

南京大学jyy在他的操作系统课上曾说过一句话:”这个世界需要英雄!”

我真的真的很喜欢这句话,在这个浮躁的时代,越来越多的人选择卷绩点,水科创,水竞赛,学前后端狠赚一笔()…

但这样不酷,真的不酷。
我要打下最扎实的计算机科学基础,我要实现真正顶层和底层的结合,我要做ai system!

那么就从认真完成这次操作系统训练营开始吧,第二阶段见!

我们,一定未来可期!

在第一阶段的学习中,主要对rust的所有权以及指针等方面进行了着重学习,下面对相关知识点进行简要总结

所有权相关规则

一个值只能被一个变量所拥有,这个变量被称为所有者

一个值同一时刻只能有一个所有者,即不能有两个变量拥有相同的值

当所有者离开作用域,其拥有的值被丢弃,内存得到释放。

当出现所有权冲突时,可以通过调用 data.clone() 把 data 复制一份出来给 data1,这样做可以创建 data 的深拷贝,避免出现所有权问题

Move 语义:当一个值被赋值给另一个变量时,它的所有权会转移,原始变量将不再有效,默认情况下,大部分自定义类型都是具有 Move 语义(优点:可以避免使用拷贝操作,提高性能)

Copy 语义:不希望值的所有权被转移,赋值或者传参时,值会自动按位拷贝(浅拷贝),两个变量都拥有独立的内存空间。 Copy 语义通常适用于简单的基本类型,如整数、浮点数、布尔值等

Borrow 语义:不希望值的所有权被转移,又无法使用 Copy 语义,可以“借用”数据,其允许在不转移所有权的情况下借用值的引用,包括不可变引用(&T)和可变引用(&mut T),允许同时存在多个不可变引用,但不允许同时存在可变引用和不可变引用(优点:使得代码更加安全,因为它在编译时进行所有权检查,防止了一些常见的错误,如悬垂指针和数据竞争)

一个值给多个所有者

  • Rc

对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数,当引用计数为0时,内存释放

Arc 与 Rc 类似,但是使用原子操作来保证引用计数的线程安全性,支持线程共享数据。

(如果不用跨线程访问,可以用效率非常高的 Rc。如果要跨线程访问,那么必须用 Arc)

use std::rc::Rc;
  • RefCell
    允许在不可变引用的情况下修改数据,采用borrow_mut(可变)、borrow(不可变)

Mutex 和 RwLock 都用在多线程环境下,当需要多线程时,可直接替换RefCell

use std::cell::RefCell;

智能指针Box

Rust 中,凡是需要做资源回收的数据结构,且实现了 Deref/DerefMut/Drop,都是智能指针。

允许将数据分配在堆上,当 Box 离开作用域时,它指向的堆内存会被自动清理。常用于: 在编译时大小未知的数据;大型数据结构,以避免栈溢出;拥有数据,确保只有一个所有者。

Box<dyn Trait> 表示一个指向实现了指定 trait 的类型的堆上分配的指针。
Trait 可以是任何 trait,它定义了一组行为或方法,而具体的类型则实现了这些方法。
通过 Box 将其放置在堆上,可以在运行时动态确定对象的具体类型,并通过指针进行访问。
运行时动态派发(动态调用)是通过虚函数表来实现的,这意味着在运行时确定调用的具体方法。

trait Animal {
    fn sound(&self);
}

struct Dog;
impl Animal for Dog {
    fn sound(&self) {
        println!("The dog barks!");
    }
}

struct Cat;
impl Animal for Cat {
    fn sound(&self) {
        println!("The cat meows!");
    }
}

fn main() {
    let dog: Box<dyn Animal> = Box::new(Dog);
    let cat: Box<dyn Animal> = Box::new(Cat);

    make_sound(&dog);
    make_sound(&cat);
}

fn make_sound(animal: &Box<dyn Animal>) {
    animal.sound();
}

unsafe代码块

unsafe绕过了 Rust 的安全检查,错误使用可能导致内存不安全、数据竞态等问题。

  • 解引用裸指针
let mut x = 10;
let ptr = &mut x as *mut i32;
unsafe {
    *ptr += 1;
}
  • 访问或修改静态变量:在 Rust 中,修改静态变量是不安全的操作。
static mut COUNT: i32 = 0;
unsafe {
    COUNT += 1;
}
  • 实现不安全的 trait,如 Send 和 Sync

  • 进行内存布局的低级操作:如结构体的字段重叠或内存对齐。

类型之间的相互转换

  • as 运算符:as 运算符用于类型转换,可以用于将一个值从一种类型转换为另一种类型。例如,将一个 u32 转换为 u64。
  • into 和 from 方法:这些方法是 From 和 Into trait 的一部分,用于在不同类型之间进行转换。这些方法通常会涉及类型的所有权转移。
  • try_into 和 try_from 方法:这些方法是 TryFrom 和 TryInto trait 的一部分,用于尝试在不同类型之间进行转换。这些方法在转换失败时会返回一个错误(Err)
  • cast 方法:在特定场景下,尤其是与裸指针相关的操作中,cast 方法可以用于将一个指针转换为另一种类型的指针。
let ptr: *const i32 = &10 as *const i32;
let ptr_void: *const std::ffi::c_void = ptr.cast();
  • transmute 方法:transmute 是一个不安全的操作,它可以在不同类型之间进行任意转换。这是一个高级的转换函数,使用时必须非常小心。
use std::mem::transmute;

let x: u32 = 42;
let y: f32 = unsafe { transmute(x) };

as用于显式转换,所有权不变;into/from用于隐式转换,所有权转移;try_into/try_from和into/from不同的地方是,转换失败会返回Err

以上是一些本阶段学习的知识点总结,通过第一阶段的训练,整体对rust的语法与编写有了初步地认识,不过在代码调试,多线程、链表操作方面还有所欠缺,将在后续空闲时间不断学习。

第一阶段

两年前就有听说过rust了,但是只是简单的看了下the rust book,并没有实际使用rust进行编程。这次打算借着rcore进一步实践rust。

目前的感受是rust的设计给了很多的限制,同时rust似乎也和cpp一样算是多范式的语言?就比如迭代一个列表既可以写成下面的形式

1
2
3
for ... in something.iter() {

}

写可以写成

1
something.iter().sum();

这样的形式。说实话,我不是很喜欢多范式的编程语言。因为选择太多了,对于我来说阅读别人的代码就会很麻烦。所以这里我更喜欢go的设计。

此外我用rust刷了一些leetcode上的简单的算法题,感觉挺头疼的。需要关注特别多的细节,而且leetcode特别喜欢给i32类型数据,而rust中访问容器用的下标都是usize类型的。导致代码写起来一点也不美观。有特别多的xx as usize

此外,感觉rust的标准库看起来是提供了很多方法来代替本来可用指针操作的方法。比如vector的swap,一般情况下其它语言里可能是这样写

1
a,b = b,a

rust对数据类型还特别敏感,只要有越界就会报错,必须得用饱和减法saturating_sub之类的方式来处理。

总之,我感觉rust的限制太多了,这让我对写好一个rust程序来说感觉心智负担很重。对于不依靠编辑器写完整程序没有信心。

希望接下来的rcore能改变我当前对rust的看法。

第二阶段

第二阶段的难度算是渐进式的吧。第二阶段的lab里,我印象最深的部分分别是内存的地址空间,文件系统Inode组织,死锁检测。

因此,下面我就对这三部分进行一些总结好了。

死锁检测

首先聊一下死锁检测吧,因为这个是最近做过的,所以印象比较深。这部分我之前一直只知道有一个银行家算法,所以一开始看到死锁检测这部分的算法跟银行家算法一样记录allocation和need的时候感觉优点懵。因为,在我原来的理解里,银行家算法应该是需要提前知道线程需要的资源的。

后来动手实现后,才发现死锁检测算法和银行家算法是有区别的,银行家算法其实是死锁避免算法。死锁检测算法感觉主要针对的就是信号量上的资源分配,然后在每一次申请的时候检查,是否分配这次资源后,能够有一个序列走得通,如果有,就算无死锁,否则算有死锁。整体上的时间复杂度应该是 $O(nm)$ 的其中 $n$ 是线程数量, $m$ 是资源数量。

平常写其它语言的时候并没有感觉到死锁检测的存在,可能就是因为这个时间复杂度随着资源数量和线程数量线性增长,所以不采用吧?

文件系统

下面来聊一下文件系统,这部分实验的学习中,我感觉复杂的地方主要在于存在很多的抽象。文件系统本身存储diskInode,然后虚拟文件系统上抽象了Inode,OSInode,最后到操作系统中,它使用的是File trait抽象。

抽象层数很多,因此让我挺晕的。这部分代码上面,感觉比较关键的是Inode的组织。超级快->Inode位图->数据位图->Inode区域->数据块区域。

地址空间

这部分我觉得是非常有收获的一部分,让我感觉算是彻底理解了多级页表,段表的区别。在此之前一直不知道页表是怎样存储的,总是想着它是存到一个特殊的内存里。现在算是明白了页表不过是存储在普通的内存里的一个结构,然后CPU依靠token来区分当前的页表。

SV39页表中,偏移为4KB,所以使用了后12位,剩余的 $39-12=27$刚刚好分成3部分,每部分占了 $27/3=9$ 位,也就是512个页表项,每个页表项占8字节,刚刚好可以将一个页存储满。

然后是操作系统内核采用恒等映射,用户程序除了跳板页,其它的采用Framed映射。这样使得操作系统访问的虚拟地址就是物理地址,而用户程序访问的是虚拟地址。这种区分使得操作系统可以访问到用户的地址,用户程序无法访问操作系统的地址。实现了空间上的隔离。

第三阶段

第三阶段我选择的是项目一:ArceOS单内核Unikernel。该实习项目主要分为两部分:熟悉ArceOS阶段与完成实习任务阶段。

熟悉ArceOS阶段

这一阶段主要是在ArceOS仓库下,完成一些简单的任务。分了两周来实现,第一周主要是尝试为ArceOS添加一些简单的功能。第二周则是练习从外部加载应用到arceos中,通过func call的方式使得外部应用可以打印字符。

第一周

  • 练习1:支持彩色打印 println! 通过在输出的时候,在前后添加特殊符号,使得输出的颜色可以改变。
  • 练习2:支持HashMap数据类型 这一步我参考rust的HashMap实现,调用了相同的库完成。
  • 练习3:为内存分配器实现新的内存算法 这一步我主要是利用了现有的算法完成页分配与字节分配
  • 练习4:解析dtb并打印 这一步我觉得主要难点是dtb的解析,因为不清楚dtb长什么样的,学习后,调用一个crate完成了该任务。
  • 练习5:抢占式调度算法 这一步我记得我是参考了现有的抢占式调度算法,然后做了一个奇偶性的判断,每次出队前,如果这次出队是奇数,就放到队头,否则正常放到队尾。

第二周

本周主要是我的收获主要是对qemu的启动参数有了一些了解,然后就是了解了rust中手写asm的方法。

实习任务阶段

第一周 rt_helloworld与rt_memtest与系统调用新增

我主要是在熟悉lkmodel,完成了原来arceos下的rt_helloworld与rt_memtest两个unikernel的实现。这一部分根据要求,必须要有axstd。因此我一开始的实现是将arceos下的axstd库直接拷贝过来,并且把它需要用到的库也进行了靠北,然后修改一些代码,使得可以编译通过。这个的结果在dev_task1_brute_force分支下。不过后来从群里得知,要尽量从lkmodel已经有的库开始,所以就重新实现了这部分,主要的方式是基于axlog2这个库,做了一些包装和宏的重新导出。

本周的问题是

  • 在实现rt_memtest的时候,出现的全局分配器不存在的问题。
    是为了解决这个问题,我发现是需要引用axalloc这个crate,同时还需要进行初始化。因此,出于实现方便,我在axstd文件夹下同时创建了一个axruntime crate,在其中编写了rust_main函数,在其中完成全局分配器的初始化。

  • 同时在周的结尾,我遇到的问题是宏内核在我操作系统上无法成功运行btp下的测试,通过在微信群与老师交流,我得知主要的原因是glibc在不同操作系统下编译C文件的结果,使用到的系统调用是不同的。就比如init.c,在我的操作系统下编译出的结果需要使用到stat系统调用,而该系统调用在当前lkmodel中是未实现的,因此我尝试在lkmodel中添加该系统调用,主要实现在axsyscall crate中添加该系统调用的函数,然后再fileops中添加fstat函数,该函数的实现参考fstatat函数,这个函数的功能是在目录中获取文件信息。

通过对系统调用新增实现,成功让我的宏内核可以在我的操作系统中运行btp下的init.c程序。我的感受就是了解了不同指令集,linux给他们分配的系统调用号码是不一样的。同时不同操作系统版本的下的glibc生成的文件,使用的系统调用也不一样。

第二、三周 rt loader

我在第二、三周完成了lkmodel下运行glibc静态编译的hello world的c应用。

这一部分我主要是参考思路3PPT的实现,首先就是对opensbi与sbi-rt的修改,这一步我是clone了opensbi代码然后按照ppt的方法进行修改。对于sbi-rt的修改则是fork其仓库,修改后提交到自己的仓库下。最后在cargo.toml中添加依赖即可。

剩下的就是修改qemu.mk脚本,向其中增加Pflash参数与bios参数。
如下所示,让其指向payload文件夹以此加载程序内容。

1
2
3
4
5
6
qemu_args-riscv64 := \
-machine virt \
-drive if=pflash,file=$(CURDIR)/payload/apps.bin,format=raw,unit=1 \
-bios ~/opensbi/build/platform/generic/firmware/fw_jump.bin \
-kernel $(OUT_BIN)
# -bios default

此处payload我是复制org_arceos下的payload,运行mk.sh脚本即可生成apps.bin。其中包含了两个hello world程序,不过由于我没有实现多线程,所以我只能跑一个hello world程序,跑完后发出的exit调用会导致我的程序直接退出。如果实现多线程的话就只会线程退出,然后回到rt_loader。

地址空间管理问题

使用rt_loader时,主要加载的是glibc静态编译的的elf程序,elf程序中存在两个数据段,一个是.text段,一个是.data段。程序想要运行就必须将这两个段搬运到对应的虚拟地址上去,否则程序无法运行。

因此为了完成虚拟地址映射,需要进行地址空间的管理。主要是创建SV39页表,目前lkmodel中的paging完成了这个页表的管理,因此我一开始是参照这个crate与org_arceos的实现,在其中创建一个静态变量来存储用户程序的页表。并编写了map_region。

但是这样做不太好,因为这样一次只能运行一个程序了,而且接下来我在运行程序的时候,它进行系统调用时,获取访问页表的手段是通过TCB中mm_struct来获取的。因此我对此进行了修改。

主要是在我的axstd下添加了map_region函数,它的功能是获取当前的task,然后获取一个可变引用,然后在task中我添加了一个map_region方法,这个map_region方法最终会对task的mm成员变量进行map_region操作。大致的调用关系如下

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
/// rt_loader中的程序调用map_region方法
vm::map_region(va, pa, num_pages << PAGE_SHIFT, flags);

/// axstd的vm模块下的map_region
pub fn map_region(va: usize, pa: usize, len: usize, flags: usize) {
let mut task = task::current();
// 向当前任务的mm中进行map_region
task.as_task_mut().map_region(va, pa, len, flags)
}
/// task的map_region方法
pub fn map_region(&mut self,va: usize, pa: usize, len: usize, flags: usize) {
self.mm.as_mut().map(|mm| {
let locked_mm = mm.lock();
locked_mm.map_region(va, pa, len, flags);
use mm::VmAreaStruct;
let vma = VmAreaStruct::new(va, va + len, 0, None, 0);
mm.lock().vmas.insert(va, vma);
});
}

/// mm的map_region方法
pub fn map_region(&self, va: usize, pa: usize, len: usize, _uflags: usize) -> PagingResult {
let flags =
MappingFlags::READ | MappingFlags::WRITE | MappingFlags::EXECUTE ;
self.pgd
.lock()
.map_region(va.into(), pa.into(), len, flags, true)
}

此处我实现的一个额外点在于调用map_region方法的时候,我同时会向mm struct中的vmas添加vma,也就是让程序知道这个地址空间被分段使用了。

task_ctx问题

在我的实现中,因为使用task记录了页表,因此我在我的axruntime库上添加了对axtask的初始化。同时我还初始化了axtrap,用于系统调用。

最后存在的一个问题是,在lkmodel的系统调用处理过程中,它经常需要访问task_ctx,从中获取当前的task,然后进行相关的操作,比如mmap,mprotect,brk系统调用等。但是因为获取task_ctx失败,所以我一直卡在此处。

听了符同学的提醒,我最终使用静态变量的方式将记录task_ctx,如下所示

1
2
3
4
5
6
7
8
9
pub static mut CURRENT_TASKCTX_PTR:Option<* const SchedInfo> = None;

// 然后在所有初始化task_ctx的位置
// 将 axhal::cpu::set_current_task_ptr(ptr);
// 修改为 CURRENT_TASKCTX_PTR = Some(ptr);

// 在所有获取task_ctx的位置
// 将 axhal::cpu::current_task_ptr();
// 修改为if let Some(ptr) = CURRENT_TASKCTX_PTR {...}

brk问题

解决完了上述问题后,我还遇到的问题就是系统调用分配堆空间的问题。我一开始没有设置brk,因此它总是从默认的位置0开始向后分配堆空间,而这部分空间与程序运行的代码段产生了冲突。

即代码段在0x10000-0x7e0000之间,而brk申请了0x1000-0x220000之间的空间。这导致了空间上的冲突,因此我后来在运行linux app之前,首先设置了brk的地址,将它设置为了数据段之后的位置。通过这样设置,就不会产生冲突了。

总结

总体来说,我完成了lkmodel下运行glibc静态编译的hello world的c应用。这让我对elf结构,qemu.log的调试,地址空间与系统调用都有了更进一步的了解。

lkmodel的组件划分让我觉得非常地棘手,因为虽然是将组件进行了分类,但是感觉想要实现特定功能的话,我依然是需要理解每个组件之间的依赖关系,有时还需要理解每个组件的内部实现。这是我认为相比rcore-os来说,主要的难点。

仓库链接是lkmodel

rust的学习

因为之前学过一些c++,有一定的基础,对rust的学习真的帮助挺大的。rust里的所有权,智能指针,移动语义都能在c++里找到对应的东西,rust学起来还是挺轻松的。

相较于c++,rust使用起来真的太舒服了。使用match进行流程控制十分优雅,宏也比c++的宏更加强大,rust泛型编程的类型萃取使用起来也比c++方便,更不用说cargo包管理对c++简直就是降为打击。

代价是rust的unsafe用起来有点丑陋。

rustlings实验

前一百道题通过看 Rust 程序设计rust官方api文档 和问gpt都能够轻松的完成。

后十道数据结构题目就遇到麻烦了,主要是对 rust unsafe 操作的不熟悉,写起来真的折磨。在认真学习了解rust 指针操作后,有算法基础数据结构实现还是比较好实现的。

总结

rust入门可以认真看完 《Rust 程序设计》,不理解的地方问gpt和百度。再写完rustlings, 最后在github上找个练手项目去理解代码尝试自己实现。

前言

在水群的时候,无意间看到这个训练营的宣传,点进去看到是OS训练营还有项目实战,正好自己对OS比较感兴趣并且正想要一个项目来练练手,就报名了该训练营。并且将该训练营转给队友,发现他们也感兴趣,于是乎,我们三人又组起了队。

第一阶段总结

第一阶段主要完成了rustlings的110道题的训练,前面100个主要针对某个小的知识点/语法,一般添加几个关键词,做几个小的移动就过了,难度并不大,收获比较大的还是后面的10道算法题,感觉做第一个的时候是最难的,后面的借鉴第一个很快就能写出来。比较喜欢这10的算法的原因是,因为之前有其他语言的经验,更加喜欢通过比较来学习,看rust跟其他语言编写同样功能的区别(又称用rust重写)。

下一步

最近事情有点多,又要做读研的准备,又要做找工作的准备,希望自己渡劫成功,能够顺利完成吧。

第一阶段 - Rust学习

0 前言

Rust作为rCore操作系统的开发语言,学习并掌握它对于后面学习rCore OS是非常重要的(关于我Rust学了个大概就去硬啃rCore差点自闭这件事)。

这次训练营也是我二刷了,上次23秋季由于被学校抓去打互联网+等比赛了,🤦‍♂️实在是没有时间弄了,进了二阶段就没然后了。

翻车gif

一阶段时间过的好快,感觉还没学多少就过去了(时间都用去debug操作系统大赛的fatfs了🤦‍♂️)。所以总结的内容就比较普普通通了,各位大佬见谅🙏。

1 环境与工具软件等

由于本人并不是CS相关专业,过去主攻的是EE,所以CS里面的很多东西过去都没接触过。从23年秋季第一次接触训练营开始到现在,接触到了很多过去没有用过的工具软件以及一些开发环境,在此简单的列举了一些。

1.1 git和github

是的,你没有看错。我之前真没怎么用过git和github。一方面原因是,上大学后搞的都是嵌入式裸机的开发,远古单片机IDE(Keil)不支持高级的代码版本管理,加上我的学习路线上也没碰到过使用代码版本管理,索性代码直接放本地(别人要的话就u盘拷贝🤦‍♂️),就很少用git去把代码放仓库(之前实习时迫不得已去折腾了下)。另一方面是,git和github有些时候真的太卡了(之前不会挂代理也不敢挂梯子)。

keil

不过,现在熟悉了git的各个命令后,配合上github去做项目的代码版本管理是真爽呀。

胡桃摇

个人过去常用的git命令和个人理解如下:

1
2
3
4
5
6
7
8
9
git clone 			// 将repo(仓库)克隆到本地
git pull // 合并远程repo的更新到本地
git add . // 将更改的文件放入暂存区
git commit -m "<message>" // 创建一个新的提交,-m 后便是提交的消息
git push // 将提交推到远程仓库
git branch // 查看现有的分支
git branch <new_branch> // 创键新的分支
git checkout <branch> // 切换到某分支
git status // 查看本地仓库状态

还有一些比如本地新建的分支需要推到远程、放弃某文件的修改等命令,就没去记了。我是做相关操作的时候,看一下报错建议的命令或在搜一下、问一下gpt,然后改一下。

另外初次使用可能需要配置用户信息:

1
2
git config --global user.name "Your Name"
git config --global user.email your.email@example.com

1.2 Linux操作系统(Ubuntu、CentOS)

虽说之前玩jeston和个人博客的时候也用过,但是基本上就是靠readme+baidu+CtrlCV+GPT(而且用了就忘),这次也算是又学习并加深了了解吧。这里就简单的列一下这期间用到的还记得的命令吧(大佬们清点喷555)

派蒙抱大腿

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
ls                      	# 列出当前目录下的文件和文件夹
cd [目录名] # 改变当前目录
sudo [命令] # 以超级用户身份执行命令
sudo apt install [包名] # 以超级用户身份通过apt安装软件包
apt-get install [包名] # 通过apt-get安装软件包
touch [文件名] # 创建一个空文件或修改文件的时间戳
vim [文件名] # 打开文件进行编辑
rm [文件名/目录名] # 删除文件或目录
make # 用于编译程序的命令
cp [源文件/目录] [目标文件/目录] # 复制文件或目录
mv [源文件/目录] [目标文件/目录] # 移动或重命名文件或目录
mkdir [目录名] # 创建一个新的目录
rmdir [目录名] # 删除一个空目录
pwd # 显示当前工作目录的路径
cat [文件名] # 查看文件内容
file [文件名] # 查看文件信息
tar [选项] [文件名] # 打包或解包文件
gzip [文件名] # 压缩文件
gunzip [文件名] # 解压缩文件
wget [URL] # 从网络上下载文件
curl [URL] # 传输数据的工具,支持多种协议
ssh [用户名]@[IP地址] # 通过SSH协议远程登录到另一台计算机
scp [源文件/目录] [用户名]@[IP地址]:[目标路径] # 通过SSH远程拷贝文件
df -h # 显示磁盘使用情况
du -h [文件名/目录名] # 显示文件或目录的磁盘使用情况
free -m # 显示内存使用情况
top # 显示实时的系统进程信息
ps # 显示当前进程的快照
kill [进程号] # 结束一个进程
logout # 注销当前会话
reboot # 重新启动计算机
shutdown -h now # 立即关机
fdisk # 一个用于磁盘分区的程序,可以创建、删除、修改和重新组织分区表
losetup # 用于设置和控制Loop设备,它可以将一个文件模拟成块设备,常用于挂载文件系统镜像

1.3 RustRover

强烈推荐,我刚开始是用的VSCode+RA插件或Vim+YouCompleteMe。后来换成rr后,感觉开发真的舒服了好多。一是因为二阶段写OS,在ubuntu里我用的VSCode写一会就会崩,比较卡。用Vim的话,代码文件一多切来切去麻烦的很;二是因为他的各个界面和PyCharm、IDEA是一样的,上手很快;三是多了很多有用的功能,比如你use了某个外部creat,rr会自动帮你在cargo.toml中加上依赖。

RustRover下载地址:https://www.jetbrains.com/zh-cn/rust/

Linux环境安装步骤:

1
2
3
4
5
6
7
1. 下载
2. cd到下载的路径
3. 解压下载的安装包:
4. cd到解压的路径
5. cd bin
6. ./rustrover.sh
7. 勾选一些东西确认安装后,等待片刻,便会进入到欢迎界面

在欢迎界面左下角点击设置图标,再点击(Create Desktop Entry)将rustrover添加到桌面目录。之后便可以使用了。

xiao

2 Rust知识

2.1 Rust学习资料

1.Rust 中文圣经

2.Rust 程序设计语言

3.Rust 中文文档汇总

4.Rust 官方文档中文翻译

5.Rust 半小时学习
半小时看不完呀!

派蒙吃惊

2.2 个人觉得Rust比较有意思的知识点

2.2.1 所有权(Ownership)和生命周期(Lifetimes):

  • Rust的所有权和生命周期机制,可以防止诸如空悬指针、缓冲区溢出等内存安全问题。
  • 在Rust中,每个值都有且只有一个所有者。
  • 而在C语言中,并没有所有权概念,内存管理完全由程序员负责,这就可能导致内存泄漏和野指针等问题。
    1
    2
    3
    4
    5
    6
    // Rust
    {
    let s1 = String::from("hello");
    let s2 = s1;
    // 此时 s1 已经失效,不能被使用
    }
    1
    2
    3
    4
    // C
    char *s1 = "hello";
    char *s2 = s1;
    // 此时 s1 和 s2 都指向相同的内存

    2.2.2 借用检查(Borrowing):

  • Rust的数据借用可以允许多个读访问,但只要有一个可变借用,就不能有其他读或写访问。
  • C语言没有这样的机制,因此需要手动管理指针和数据的共享。
    1
    2
    3
    4
    5
    6
    7
    8
    // Rust
    {
    let mut s = String::from("hello");
    {
    let r1 = &mut s;
    // 在此作用域内,不能借用 s 的其他引用
    } // r1 超出作用域,s 可以被再次借用
    }
    1
    2
    3
    4
    // C
    char s[] = "hello";
    char *p1 = s;
    char *p2 = s; // p1 和 p2 都指向相同的内存

    2.2.3 模式匹配(Pattern Matching):

  • Rust提供了强大的模式匹配功能,可以用于枚举、结构体、元组和字面量等。
  • C语言没有内置的模式匹配功能,通常需要使用switch语句和多个if-else链来实现。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Rust
    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
    }
    let coin = Coin::Penny;
    match coin {
    Coin::Penny => println!("Penny"),
    Coin::Nickel => println!("Nickel"),
    Coin::Dime => println!("Dime"),
    Coin::Quarter => println!("Quarter"),
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // C 
    switch (coin) {
    case PENNY:
    printf("Penny");
    break;
    case NICKEL:
    printf("Nickel");
    break;
    case DIME:
    printf("Dime");
    break;
    case QUARTER:
    printf("Quarter");
    break;
    }

    2.2.4 并发(Concurrency):

  • Rust提供了安全并发的工具,如std::threadArc(原子引用计数)和Mutex(互斥锁)。
  • C语言标准库没有内置的并发工具,通常需要使用操作系统提供的线程库(如POSIX线程)和同步原语。
    1
    2
    3
    4
    5
    6
    // Rust
    use std::thread;
    let handle = thread::spawn(|| {
    // 在新线程中运行的代码
    });
    handle.join().unwrap();
    1
    2
    3
    4
    5
    6
    7
    // C 
    pthread_t thread;
    int status = pthread_create(&thread, NULL, &thread_function, NULL);
    if (status != 0) {
    // 错误处理
    }
    pthread_join(thread, NULL);

    2.2.5 错误处理(Error Handling):

  • Rust使用ResultOption类型来处理可能的错误或空值,这鼓励开发人员进行显式的错误处理。
  • C语言通常使用整数错误码和返回值,这可能导致错误被忽略或错误处理。
    1
    2
    3
    4
    // Rust
    fn open_file(file_name: &str) -> Result<File, io::Error> {
    File::open(file_name)
    }
    1
    2
    3
    4
    5
    6
    7
    8
    // C
    int open_file(const char *file_name) {
    FILE *file = fopen(file_name, "r");
    if (!file) {
    return -1; // 返回错误码
    }
    return 0; // 表示成功
    }

    3 总结

    说来惭愧,这次训练营第一阶段大部分时间花在折腾OS大赛上了。本来到rCore的ch7时都挺顺利的,结果尝试去运行OS大赛的FAT32镜像里的测例就碰壁了。先是easyfs和fat32不是一个东西,得把照着rCore写的OS的fs换成支持fat32的,瞎折腾了两周总算是能读取fat32里的文件了。然后又发现大赛OS的elf文件导入OS运行又有问题,瞎折腾也搞不出来了(基础不牢靠,不懂为啥会loadpage Fault),索性打算重新复习。

这次训练营又学到了很多新的知识,不过也能很清楚地感觉到掌握的不牢靠,后面还需不断加强巩固。同时我也知道了如果自己后面想往OS方向发展,还有很多需要去学习的。说实话,刚开始搞rCore的时候感觉还挺好的,而后到了ch4后,计组等知识的欠缺就很致命了,搞的差点放弃了(fat32那也是,不过好在搞出来了)。然后到了运行OS大赛的elf这就真搞不懂了,只能试着去好好复习下Rust、看看计组、CSAPP等后,再来看看了。

感激社区提供了这样一个学习平台,它为我打开了一扇探索操作系统奥秘的大门。希望后续的学习我还能够坚持下去吧!

npn

4 图片记录

1

照着rCore写的FeatOS移植FAT32文件系统成功