0%

紧赶慢赶,终于是在交上了最后的实验,带着许多想法,写下了这篇总结。同样作为清朝老兵,我又回来了。

一阶段:Rust 110 道算法题

在第一阶段,主要通过编写 110 道算法题,掌握 Rust 编程的基本和高级特性。Rust 语言强调安全性和高效性,
这在算法编程中尤其重要,是的,单个语法题目还是比较轻松的,查阅资料什么的也都可以直接解决。当真正开始算法部分的
题目的时候,才开始慢慢体会到rust的折磨——借用检查器(Borrow Checker)、所有权(Ownership)以及生命周期(Lifetime)等概念
统统在处理复杂数据结构时痛击我,Rust 的强类型系统帮助我减少了运行时错误,经常是我的逻辑天衣无缝,但还是borrowed error。
没事,慢慢折磨吧,折磨多了就会了。。。

实际上,作为清朝老兵,一阶段倒是没花多少时间,翻出以前的仓库,看看填填也就过去了,但是当时熬夜做rustlings的日子仍然是我挥之不去的记忆。酸爽!

二阶段:实现 rCore 简单内核

是的,一拳打爆rcore。第二阶段则是操作系统开发,说白了就是补全各种系统调用。我也慢慢感受到书上说的种种在我看起来毫不起眼的东西,真正实现起来是真的困难,(这里点名虚拟内存和文件系统,东西是真的很多),每个ch都是一脸懵到嘎嘎乱写,然后对着panic疯狂调,期间甚至一度动摇额我的计算机世界观——计算机是真的有玄学啊。不过还好,总是能在某些神奇的地方调调代码就神奇地通过了测试,也是给我留下了不少的未解之谜。

在二阶段确实收获了不少,我现在对于一些较大的工程项目已经没有感觉了,也确实是慢慢熟悉rust了,好玩,爱玩!期待后面的考核。

祝训练营越办越好!希望能在虚拟内存和文件管理那多讲一点,确实很抽象。

最后,感觉你读完我的碎碎念,不管怎样,还是磕磕绊绊弄完了这两个阶段,也算是给上半年自己中间跑路一个交代吧,行文至此,拜拜。

前言

  • 一个大三学生, 偶然看到这个学习活动就来玩一下了. 之前学校操作系统课学了个寂寞, 正好来学多点补全知识, 并且练练rust.

rustling

  • 这个还好, 以前有点点rust基础. 做着没什么困难, 顺便查漏补缺了一下.

rcore

  • 二阶段开始, 内容一下多了起来, 文档看着也挺枯燥的, 有些地方感觉写得对小菜菜不太友好QAQ.
  • lab1
    • 翻翻文档, 翻到了ch3. 懒得再细看文档了, 直接写代码做实验!看了一下要求, 直接加点东西就一次过了, 开心.
  • lab2
    • ch4这个卡了我两周, 内存虚拟化这块真的很不会, 做着不明不白. 有点想摆烂了, 看了看群里聊天记录(光是看群聊信息都能学很多), 有点思路, 往大方向试了一试, 通过.(可能通过了还是不太明白)
  • lab3
    • ch5不是很难, stride调度算法出了点小错误稍微卡了一下, 很快就过了.
  • lab4
    • 听很多人说ch6最难. 文件系统我也不太会,但我认真看了文档和代码, 试了几次也过了, 没感觉太难.
  • lab5
    • 做得最痛苦的实验. 这个实验主要实现死锁检测, 本身不是很难. 但我遇到了一些玄学问题, 有sleep_blocking的测例会在sleep卡住超时过不了, 找来找去找不到原因, 加上DDL到了, 心态小崩. 最后垂死挣扎, 卸了qemu9, 把它换成qemu7, 不怎么抱期望地跑了一下, 通过了!(据说有一些人用qemu9也可以通过, 但不知道为什么我不行.)

总的来说, 第二阶段学到了很多东西, 以前漏掉没学的操作系统知识被补上很多, 但更感觉有更多的东西要学. 同时有时做实验遇到困难怎么尝试都通过不了, 最后再坚持一下通过了, 很有成就感.(感觉实验测例有点简单..)

专业阶段

这个阶段给我最大的感受是学习,因为很多函数都被事先书写好了,这几个实验基本都是在已有的框架上进行相关的函数调用即可,在明白每个函数的实现逻辑以及每个lab实验要实现的功能即可完成实验。同时这个阶段给我的感受是有许多深层次的知识仍需要学习,有许多一知半解的知识需要实践,看到学习群里面大家对于实验的讨论的诸多见解,受益匪浅,完完全全的拓宽了我的眼界,许多从未听过的名词出现在我的面前,只感到纸上得来终觉浅,绝知此事要躬行。可惜时间所迫,没办法对于每个lab实验进行进一步的深究。
第二阶段我重点理会到了操作系统的一步步发展以及实现的功能,在进行每个实验的时候,感受到学校里面的讲解十分片面,并且浅薄(也是我没有认真学习的缘故,学校里面的课检验的只有期末突击而已)。
非常重要的一点是这次的理论和实践一起进行,让我极大的认识到了抽象和具体之间的联系,有时候理论上很复杂并且难以理解的东西,转化到实践上面竟然可能只是一个数组,一个函数栈而已,这让欠缺实践的我大开眼界。
非常期待第三阶段带来的挑战,也非常希望可以通过第三阶段(哈哈哈)。

Rustlings总结

心路历程

这个训练营我也算是“老兵”了,不断地入门再入门,到终于下定决心,投入大量的时间来完成这个训练营,真的经历了很多很多,有着太多的心路历程,从想着三天写完,慢
慢的查漏补缺,温习之前的知识点,慢慢的开始一道题一道题手写rustlings…
我总是说我基础很差,并拿这句话当做挡箭牌,最终一事无成。未来会更好吗?但是我已经有点厌倦痛惜过去了。
说起来这也是我第三次的写rustlings了,开始越来越熟练了,这当然是开心的,不过后面有这更多的更艰难的挑战,阶段二,阶段三,都是未知的大山,更艰难的挑战。

学习由来

对于这个训练营,我是大二机缘巧合开始接触到的,那时候听学长说,特别有含金量,可以“咸鱼翻身”,那可是清华啊,多么具有神话幻想意味的大学,那时的我突然想着,
我一定能做出一番令人羡慕的成就。
但是那时的我用两个字来概括是“摆烂”,在羡慕同学取得的成就和奖项,和自己什么事也不做只打游戏的情况下还希望着未来自己能有一番大成就,如今看来竟全是一片混
浊,在孤独和幻想中,我度过了大学的大半时光,我的性格是卑劣的。
但是未来的路仍旧一片黑暗,载着家人的希望,我在原地自顾自的打转,跳不出自己的镣铐,做不到的仍然做不到,幻想的事物越发离谱。
慢慢得走来,我只剩了这个训练营了,又是另一个未完待续。
我可以做到吗?我问过了很多人,很多未曾谋面的人,陌生的人对我的态度竟大都是积极的,他们不了解我。我也正在逐步了解自己。忘记了太多的事情,记忆也越来越差,但
是从哪个角度来说,我这次真的想完成这次训练营,即使我每次都这么说…
祝武运隆昌。

第一阶段:

比较简单非常方便新手入门,可以很好的对rust的学习曲线难度经行拟合.

第二阶段:

由于研究生的忙碌确实让我有感觉到力不从心,

但是课程比较有用 我之前是干java的对于设计模式和代码规范有一定的追求所以希望在第二阶段对这方面补充

实操教学真的很好 不仅可以检验知识点也可以检验代码能力两不耽误

感想

在接触这门课程之前,我正处于第 N 次入门 Rust 的途中,在之前其实就一直有过多次学习 Rust 的经历,我博客上关于 Rust 的学习笔记最早甚至是 18 年的,回顾我最早学习 Rust 的理由是我希望掌握不同的编程语言范式拓展自己的思维模型。

「学习过程更多的是知识迁移,如果是学习新的编程范式则会稍有难度,这不仅仅是学习新的概念,还需要在思维模式上做出改变,对于编程语言的学习,应当多去学习不同范式的语言,尽管现代编程语言都支持多范式,但各语言擅长的范式还是不一样的。」

但前几次入门 Rust 多因为工作没能用上而浅尝辄止,最近一次我是深刻意识到不实践是无法学会的,所以我一边用 Anki 制作学习笔记卡片,一边尝试用 Rust 写一些小工具,又在 Rust 微信群里看到了 Rust 操作系统的课程,这一下死去的记忆突然开始攻击我了,这不正是我想要的实践吗?
回想起大学自己自学过王爽的《汇编语言》还有于渊《一个操作系统的实现》,但止于汇编编写内核引导后输出 Hello World 的阶段,我被开篇的复杂 C 语言应用劝退了,上面两本书的作者和名字我时隔十年居然都能清晰回忆起来,这或许与我当时的热情有关吧,毕竟操作系统是三大浪漫之一。
于是我立了个 FLAG 想要弥补过去没学好的内容,另外也是为了真正知道自己在做什么,当我拿起自己习以为常且熟练的编程语言和计算机工具进行各种创作的时候,真正发生了什么,学完二阶段或许我不能说掌握了多少底层的细节,但是我有了一种清晰地感知,当我写下的代码在计算机中流动的时候发生了什么。

控制流

由代码编写的指令在编译成汇编代码后会形成一个执行序列,这个序列就是一个控制流,在正常没有中断和异常情况下,整个控制流都由代码生成的汇编控制,这个控制流是普通控制流。
但是一旦进行系统调用,程序就会脱离原本的控制流,进入到操作系统内核代码的控制流中了,比如接受 IO 中断响应,处理用户输入等。这种突变的控制流称之异常控制流。(处理中断、异常、陷入)
这个过程中伴随着执行环境的变化,以及上下文的切换,在应用程序基于操作系统抽象(进程、线程等)的执行环境中,突然切换到操作系统内核基于硬件的执行环境中,同时上下文发生了变化(执行环境的相关参数)。
操作系统和应用程序需要协同硬件一起来保存和恢复这些上下文,使得程序能够正常运行。

异常控制流的保存和恢复由操作系统和 CPU 负责。(手动编写在栈上保存与恢复寄存器的指令)

对于函数转移这内控制流转移,由编译器负责。(编译器会自动生成栈上保存与恢复上下文的指令)

异常控制流

外设中断(Device Interrput):由外部设备引起的外部 I/O 设备事件。
异常(Exception):程序发生除零错误,内存访问越界等。
陷入(Trap):系统调用进入操作系统工作流。

三种东西都是一回事,都是应用程序的工作流被中断了,跑去执行别的地方的代码了,不过对三种中断的方式做了命名区分。

目标平台与目标三元组

通常一个 C 程序编译器的工作流程如下:

  1. 源代码 -> 预处理器进行宏展开
  2. 宏展开的源代码 -> 编译器编译生成汇编代码
  3. 汇编程序 -> 汇编器编译为目标机器代码
  4. 目标代码 -> 链接器链接为可执行文件

Rust 通过一个三元组来描述软件运行的目标平台,即 CPU、操作系统、运行时库等。

`$ rustc –version –verbose

1
2
3
4
5
6
7
rustc 1.82.0 (f6e511eec 2024-10-15)
binary: rustc
commit-hash: f6e511eec7342f59a25f7c0534f1dbea00d01b14
commit-date: 2024-10-15
host: aarch64-apple-darwin
release: 1.82.0
LLVM version: 19.1.1

从 host 可以看出我们的 CPU 是 aarch64, apple 系统, darwin 运行时。

Rust std 库和 core 库区别?

std 库依赖操作系统,提供线程等能力,core 库不依赖操作系统。

elf 格式分析
rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os

实现第一个裸机程序我们做了什么?

Rust 编程:剥离标准库,使用 core 库提供的 trait 实现 panic_handle

1. 去掉 Rust 的标准库依赖,使得程序能够编译成功。

  1. 在 Rust 项目中使用 #[no_std] 标识不使用标准库,这样一来我们 println!() 也将无法使用需要自己实现。
  2. panic 处理是必须的,但是 panic!宏也标准库实现,因此需要导入 core 库手动实现 panic_handle 方法
  3. 但程序需要一个的启动需要一个_start 语义项,语言的标准库作为程序的执行环境,在启动前也会做一些初始化工作才会跳转到由用户编写的程序入口处,但此时我们已经没有标准库依赖了,因此#[no_main]声明没有通常意义的入口函数。

完成上面三项我们就可以编译通过了,但此时编译出来的是一个空壳应用,我们希望执行点什么。

2. 编译出能对接 Qemu 与 RustSBI 的应用。

源代码编译出的来程序字节码可以分为两部分即数据与代码,实际上我们还可以对这两部分进一步划分为更小的单位段(Section),整个这部分就构成了程序内存布局。

.bss: Block Started By Symbol 存储未初始化的全局变量和静态变量。
.data: 已经初始化的全局变量与静态变量。
.rodata: 存储只读的常量与字符串等。

  1. Qemu 在加电启动后会从 0x80200000 开始执行,因此我们需要调整编译出的程序的内存布局,这将通过为编译器指定 linker.ld 配置文件来实现,但提交给 Qemu 的文件还要剥离元数据才能被 Qemu 正确寻址,这通过 rust-objcopy --strip-all 来实现。

  2. 使用#[no_mangle]自定义的入口函数

  3. 编写 linker.ld 定义内存布局

  4. 编写汇编代码配置栈空间布局,设置栈顶指针然后 call 入口函数

1
2
3
4
5
6
7
8
9
10
11
12
13
 # os/src/entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main

.section .bss.stack
.globl boot_stack
boot_stack:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:

3. 实现关键系统调用,实现 Println! 宏等。

通过系统调用实现退出机制,通过系统调用实现 print 方法以及宏。

如何实现批处理操作系统?

批处理操作系统主要解决几个问题:

  1. 加载所有应用程序
  2. 在执行应用程序前初始化,比如用户态栈的初始化,特权级别切换
  3. 处理应用程序的系统调用
  4. 处理应用程序的错误,在出错时执行下一个程序

因此首先要实现用户态的程序,剥离元数据转为 bin 格式,以静态绑定的方式载入内存,在使用时以动态加载的方式来加载。
具体实现方法是通过代码生成汇编码,将应用程序写入数据段,并标识每个程序的起始结束地址。

1
2
3
4
5
6
7
# 例子
.section .data
.global app_4_start
.global app_4_end
app_4_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/04priv_csr.bin"
app_4_end:

在内核中,实现应用程序的加载器、实现 Trap 的处理和特权级别切换,以便于在用户态程序进行系统调用时从用户态切换到内核态,并且在这种切换过程中实现上下文的保存和恢复。

CSR 是什么?

在计算机体系结构中,CSR 通常指的是 “Control and Status Register”(控制和状态寄存器)。这些寄存器用于存储处理器的控制信息和状态信息,允许操作系统和应用程序控制处理器的行为和获取处理器的状态。

多道任务与分时多任务

在这里我们主要为了隔离 CPU 资源的使用,让每个应用在特定时间内都能获得 CPU 的全部使用权,同时也可以主动让出 CPU 的使用。
为了让内核同时调度多个应用程序的执行,我们需要实现应用程序的加载机制和任务管理系统,实现任务的切换、暂停管理等能力。

协作式多任务执行

通过实现 yield 系统调用 我们可以让任务主动让出 CPU 使用权,从而让内核进行任务切换执行别的任务,这种由任务自己让出 CPU 使用权的系统叫做协作式操作系统,但是任务下次获得 CPU 使用权的时间与内核调度策略以及其它任务相关,所以缺点也很明显,对于需要及时得到响应的任务而言这样协作式的方式会严重影响使用体验,协作式操作系统适用于所有应用都是可信的情况下。

分时多任务执行

通过时钟中断机制,我们可以实现时间片轮转调度机制,让 CPU 在每个时间片段内周而复始的轮转执行任务,这样可以确保每个任务都能公平的得到 CPU 的使用时间,这种机制叫做抢占式调度策略。

地址空间 - 计算机空间的抽象

现在,我们学会了如何将如何将应用运行在裸机上所必要的知识与概念,即特权级别的切换与任务栈的切换,切换过程中伴随的寄存器的存储与恢复,接下来是操作系统最重要的事物,即构造各种抽象,提供环境约束与管理。
第一要构造的抽象是「地址空间」,它给应用程序创造一个拥有全部内存的幻觉,隔离应用以及操作系统之间的内存访问,保证安全。

SV39 多级页表机制

RISC-V 中通过将 Satp 寄存器的高 8 位 Mode 设置为 8 则开启 SV39 页表机制,此时 U/S 模式下的地址访问都会被视作 39 位虚拟地址需要经过 MMU 转换为 56 位的物理地址,SV39 即特权模式下的 39 位虚拟地址,即在 SV39 模式下,64 位宽只有低 39 位是有意义的。

地址格式:

因为我们的页表是 4K 大小,因此需要 12 位来寻址,因此低 12 位为页内偏移,高 27 位则为页码,MMU 的转换就是扎到虚拟页码到物理页的映射,Offset 不变,拼接成 56 位物理地址。

页表格式:

页表存储:
如果按下面线性表的方式存储页表,即知道一个应用页表的 base*addr,对应的虚拟页页表 = base_addr + 8 * 虚拟页码,即可得到页表项从而找到物理地址。
考虑到虚拟地址有 27 位用于页表索引,因此如果完全存储 2^27 页页表需要 2^27 _ 8(64 位) = 1GB 的空间,这显然不现实。

因此我们需要按需分配的机制,即保存真正有效的虚拟页表。

我们可以通过类似前缀树的方式采用三级页表的方式来索引页表项目 PTE,将 39 位虚拟地址的高 27 位分为 3 段,每段 9 位,剩下 12 位为偏移,如下图所示,通过三级索引我们拿到最终的物理页表页码,加上 Offset 就可以得到最终的物理地址。
每一级页表的大小刚好 2^9 = 512 个 PTE,每个 PTE 大小 8Byte 刚好 4K 一个物理页帧,这样一来,12K 就足以存储页表了。

在地址空间下运行的应用

在地址空间下应用的程序与之前最大的区别在于,此时所有地址指令都需要经过多重的转换,期间有 TLB 加速了这个过程。除了之前提到的特权级别切换,我们还要切换相应的地址空间。
为了这种切换能够顺畅,我们需要构造跳板以及陷入上下文等数据结构,在应用的高位地址存储用户态陷入上下文以及映射陷入内核的跳板代码,这部分代码尽管在用户地址空间,但是 S 态才有的权限。

进程 - 计算器力量的抽象

计算是一种强大力量,但是在操作系统上可能同时运行着多个不同的应用程序,每个应用都可能需要持有计算的权柄,前面提到我们可以让程序自己出让计算权利的协作式多任务模式,也有由操作系统根据时间片切换的抢占式模式。
如何更好的管理这些任务的计算,我们需要更高阶的抽象,即进程,可以理解为比任务更高阶的概念,它不光包括了正在运行的任务,还包括了他拥有的资源,如地址空间等,下面这张图很好的描述了进程概念和任务的执行过程。

文件系统 - IO 的抽象

关于 Linux 系统有一句非常著名的话,那就是一切接「文件」,这个概念如此的强大,以至于可以容纳一切外部设备,仔细想想,因为它只包含了最小的内涵,即「读、写」两个操作,只要能读写?那就是文件,包括我们的硬盘。

准确的说,文件系统是建立在块设备上的一种抽象,下面这张图描述了块设备的内部布局,类似于页表,不过用于管理索引的结构叫做位图。

操作系统上的应用不应关注块设备的内部结构,而是需要关心它提供什么能力,如果每次数据的读写都需要从索引一路找到数据块岂不是很麻烦。
因此我们要为其实现目录结构的管理,这是一个内存中的数据对象,它完成了对块设备布局中存储的真正数据内容的映射。

并发 - 万物迸发

死锁检测

算法过程就是判断某一未结束线程 Finish[i] == false
它的需求是否小于操作系统所能提供的 Need[i, j] <= Work[j]

如果小于那就执行到完成, 然后更新操作系统能分配的资源
Work[j] = Work[j] + Allocation[i, j]
Finish[i] = true

如果 Finish[0..n-1] 都为 true 即所有线程都有足够资源,否则就是不够,有死锁。

总的收获:

  1. 通过学习操作系统对rust 编程中的借用和生命周期对保存内存安全有了深刻的理解

  2. 对risc-v 和 arm以及x86 的不同的架构体系有了更深入的理解,理解了ISA 是软硬件接口,深化了接口的理解

  3. 理解了 操作系统实现中软硬件配合方式,包括机制和策略。尤其是实现虚拟内存时 为了达到硬件不过于复杂而且性能的最大化,硬件和操作系统各有折中 需要紧密配合

  4. 对于进程和文件的抽象 ,为什么会这样做 以及演变过程 有了更深刻的了解。

    从单纯执行任务的job进化到拥有 虚拟空间和状态 且可以动态创建和销毁的进程,并不是一开始就是这样的。

    对于文件系统的抽象,我理解了抽象的2个角度.第一个角度是从使用者角度,要越简单越好,第二个角度是从实现者角度,其实现的过程时复杂的,但暴露的接口是简单的,统一的

  5. 对多线程和协程的实现机制,让我了解到了 对于不同的任务io密集和cpu 密集型,为什么协程更有效。对多线程之间的同步互斥机制有了更加具体深刻的认识,能够分辨自旋锁和基于等待的锁 的应用场景。

  6. 通过这次学习填补了 概念和实践上的鸿沟,操作系统不再仅仅存在于书本中的理论中。感谢训练营的各位老师!!

第一阶段

之前在公司工作的时候就了解到了 rust,工作中客户端使用 rust,当时觉得 rust 离后端(服务端)比较远,就错过了 rust 的深入了解与学习。
在这几年的工作与学习中,慢慢接触到rust在操作系统的领域也在使用。于是准备学习一下这门新的语言。自己也想对操作系统相关方面做一些
更加深入的了解。

一次偶然的机会在github 上看到了 rcore,用 rust 写操作系统,这个和自己的需求完美契合了。。。

本课程第一阶段采用 rustlings,在线测评,然后对应的知识点也会有出处。是一个非常好的学习工具,但是整体来说数据结构部分相对较难,需要
一些相关数据结构的基础,以及对 rust 语法的了解。

自己对 rust 相关的一学习的例子与记录: https://github.com/FunCheney/Fos/tree/main/rust-study

整体感觉:

  1. 语言的学习还是要多动手练习,多写代码才能理解
  2. 数据结构与算法相关的章节还需要持续的练习
  3. 后续还要安排并发相关的学习与记录:https://github.com/FunCheney/Fos/tree/main/rust-study/rust-atomics-and-locks

第二阶段

整体来时实验教程偏简单且大多都一笔带过。学习的过程中主要还是参考: rCore-Tutorial-Book 第三版!

因为在训练营开始之前我就已经在通过看: rCore-Tutorial-Book 以及抄 https://github.com/rcore-os/rCore-Tutorial-v3 中每一章节相关的代码,
因此在训练营阶段也在抄其中代码,在自己的仓库里面手写了一遍相关代码。

对 risc-v 精简指令架构有了一些了解:https://github.com/FunCheney/Fos/tree/main/code/asm 主要代码实现。
整体感觉:

  1. risc-v 的指令架构没有 intel x86 指令复杂。
  2. 文件系统,与锁相关的实验部分用时教程,偏难。
  3. rust-sbi 和 qemu 会简化很多汇编的相关的工作。降低了对操作系统启动记载相关的一些理解门槛,之间看过 linux 0.11 相关的启动逻辑,还是比较复杂。
  4. 通过在线测评的方式,通过一些测试用例可以起到检测的收手段。比自己写代码的时候(尤其是还在学习过程中)不知到对错会更有方向。但是还是缺少一些
    最佳的实现知道,因为不知道自己过了是否就是好的编码方式,好的实现思路。

一阶段 Rustlings

​ 机缘巧合之下,对我教育颇深的学长为我介绍了这个训练营,于是一段艰辛的历史就开始了

​ 初学rust,被他严格是语法体系给搞傻了,这也不给那也不给,对于写惯C++ 的我来说简直不可理喻。rust语法体系中不允许隐式类型转换,即使是在C++中的非窄化类型转换也不允许。更要命的一点是,所有变量默认全是按const不可变变量来处理的,这极大地降低了我的愉悦性,在C++中此类const操作时显示的,在这变为隐式。还有一点则是类型的特性不会自动从父类继承(C++是这么称呼的,rust学的不太行),必须我去一 一 写出,也是很难受。

​ 介绍完了令我不愉快的地方,rust的优势也很明显,不允许随意更改变量,不允许直接操作指针,变量的所有权等等,在重重限制下无疑降低了代码出错率,不过我还是喜欢C++。

二阶段rcore

​ 这一阶段别提多痛苦了,一阶段语法就没学好,大半时间都在调语法错误,章节知识点介绍都挺好的,想一想也容易想出来,over,不过我还是喜欢C++,rust使我痛苦 , 重复可变借用我恨你