0%

2024 开源操作系统训练营第二阶段总结-刘梓陆

写在前面

真是一段很长的旅程啊,怎么说呢,这一阶段的任务我基本上都是“挣扎”着完成的,一开始感觉以我对 Rust 和 OS 的理解,应该此阶段的任务不会非常困难。后来发现还得是“纸上得来终觉浅,绝知此事要躬行”,有些东西在课上学习理论知识和实际使用代码进行实现完全是不一样的感觉,而且怎么说呢,Rust 的运用程度实际上对编写操作系统的帮助也不是特别大,就好像你要和亚里士多德探讨哲学问题,重点是这个哲学问题,而不是古希腊语,当然你得先对古希腊语有一个大致的了解,但是古希腊语本身对理解这个哲学问题是没有什么帮助的。

反正总之由于种种原因,我感觉我在这方面的经验根本就是 0,导致本阶段的任务超出截止时间一周差不多才完成。

但是怎么说呢,就好比是 DND 里面的等级系统一样,在等级为 1 的情况下,升到 2 级所需要的经验值是很少的,所以这个训练营给我的提升感觉也特别大(OS 子职等级),了解到了很多之前根本没有机会接触的知识领域,最重要的,更是加深了我对 os 的理解。

我在第一阶段的总结之中也提到了我非常崇拜 Linux,现在感觉我离他好像又进一步了,感到非常开心!

欢迎交流;-)

在写这篇博客时,本人东北大学大三在读,前两年东学西学,Rust 就是之一,说实话,在本学期开始时,我实在找不到什么东西来做,准备先买一本《趣读 Linux 源码》来看,感谢我学弟翊嘉,是他给我推荐了这个操作系统训练营,这样直接上手操作系统的效果比看书要好太多了,真的十分感谢!

我对 Rust 的理解还是比较少,future、unsafe 的内容更是知之甚少,所以大佬求带!看到我这篇总结报告之中有什么不足也请指出!

OS 大概是个什么东西捏?我在完成了这个阶段的任务之后的理解

最近在看 Linus 的自传,他在里面讲了一段关于 OS 的话感觉说得特别好:

计算机上的所有功能要起作用,都得以操作系统为基础。于是,创造一个操作系统就成了终极挑战。你创造操作系统的时候,相当于给所有在这部电脑上跑的程序创造了一个全新的生存环境—从根本上说,其实就是在制定这个世界的规则:什么事可以接受、可以做,什么事不可以接受、不可以做。其实所有的程序都是在制定规则,只不过操作系统制定的是最根本的规则。创造一个操作系统,就相当于自己创造的一方土地制定宪法,而在电脑上跑的程序则相当于各式各样的普通法律。

这段话形象地表达了操作系统的功能、作用、以及它对上层应用程序强大的控制能力,但是实际上感觉在实现了一些东西之后,好像也不完全是这样。

os 下面还有一个 sbi?

原来就是认为是下面这个层级关系:

  • 计算机硬件 -> os -> user application

但是在使用 RISC-V 实际上应该是这样的!

  • 计算机硬件 -> sbi -> os -> user application

实际上操作系统也要向下请求 sbi 提供的功能!在本次训练营之中一个比较常用的功能——往屏幕上输出字符就是通过 sbi 完成的。实际上完全不止于此,SBI 还会在计算机启动的时候进行它所负责的环境初始化工作,并将计算机控制权移交给内核,操作系统的关闭也是由 SBI 控制的。

从 RISC-V 特权级架构的视角来看,我们编写的 OS 内核位于 Supervisor 特权级,而 RustSBI 位于 Machine 特权级,也是最高的特权级。

说实话,我对 SBI 的理解就到此为止了,在代码之中,很贴心地向我们提供了 SBI 服务调用的接口,我们直接使用就可以了。


这部分我感觉还不是很清楚的内容:
SBI 的内容我感觉真的后续还有必要继续学习一下。


毫无疑问就是一个应用程序

在本阶段的相关代码项目之中,除了一些关键的处理 Trap 和上下文切换的逻辑是使用汇编进行编写的之外,其他都是使用 Rust 编写的、和普通的用户应用程序并没有什么不同。

在实现了地址空间之后,基本上就完全停留在逻辑上的开发了,不再关心操作系统的实现细节,实际上我在实现了进程和文件系统的相关功能之后回头一看都感觉地址空间之中的一些实现细节都开始遗忘了。


这部分我感觉还不是很清楚的内容:
Rust 编译器是如何将这个汇编代码和 Rust 代码链接在一起的?
而且在 Rust 之中是如何调用在汇编代码之中定义的过程的?


通过寄存器和 pc 的指令流

而且我发现真的一个应用程序实际上非常简单,就是一堆指令,pc 拿到之后一条条执行,然后会有一个跳转指令跳转到另外一条指令进行执行,然后指令的操作数都是从寄存器拿出来的,线程或者进程实际上就是一个包装了指令流的上下文,在里面进行了一些簿记工作,记录了当前指令进行到了哪里,然后一些相关寄存器的状态。

狂赶一周,二阶段也算是终于做完了,这里就说说实验中令我印象比较深的部分吧。

地址空间

操作系统为每个应用程序制造了一种假象,仿佛它们能够独占并随意的使用巨大的内存空间,但实际上,这是硬件分页机制和操作系统维护页表共同营造的幻觉,在加载ELF文件时,操作系统会把程序加载到物理地址中一段空闲的区间,但会让ELF文件中指定的加载地址对应的页表指向真正的物理地址。

我觉得这个实验需要注意很多细节,比如,页表切换是一条指令完成的,如何保证切换完之后依然能够按照我们所期望的执行流继续执行呢?我们开辟了一个“跳板页”,让所有地址空间的最高一页都指向一段相同的物理地址,这样就可以保证页表切换是“平滑的”。

还有,之前想了很久为什么用户态的上下文是存在用户的地址空间而非内核的,后来看实验指导书才明白页表切换和内核栈切换是两步操作,但只有一个sscratch寄存器,无法在不破坏通用寄存器的条件下切换到内核空间并在内核栈保存上下文。

这章还有很多技术上的优化技巧,比如,如果为每个应用都建立一个能够映射全部空间的页表,会占有过大的物理空间,所以,我们不再对每一个地址都映射,而是根据ELF文件的程序头一段段来映射,并且引入类似“字典树”的数据结构来建立多级页表,有效的节省了空间。

并发

实现互斥锁可以有三种方式,一种是在纯软件形式在用户态通过类Peterson算法

实现锁,还有是在硬件的支持下通过原子指令来实现,还可以通过操作系统支持(假设内核进程不被打断)。

这章给我印象比较深的部分是引入互斥锁时举了一个计数器的例子,根据例子运行的异常提供了3个分析思路:是不是编译优化造成的?是不是OS调度造成的?是不是CPU造成的?并且逐一分析,最终找到根本原因,我认为这给我了一个关于系统编程方法论的启发。并且在分析错误原因时通过状态机和合法状态来分析,也是一个很深刻的想法。

总结

除了上面说的一些底层的特性(页表,地址空间切换等等),操作系统大部分也只不过是个普通的应用程序。在看框架代码时,我也学习到了一些编程的技巧和方法,比如,通过drop trait来实现RAII,通过这个rust机制有效的减少了内存分配相关的错误。

总体来说,不管是对OS的理解,还是对rust的掌握,我都进步了很多。

2024春夏季开源操作系统训练营总结报告

第一阶段总结报告

前言

作为一名大二在读生,在听同学说有一门语言很炫酷的时候,就算是面对着巨大的课业压力,我还是选择来看看rust到底是什么样的一门语言。

Rustlings总结

刚刚接触rust语言时,我并没有意识到rust的强大之处,认为它只是一门新的语言罢了。但是当我跟着文档一步步去接触rust中全新的概念时,我才发现这门语言与其他的编程语言有多大的区别。

从rust变量的所有权与借用规则,再到后面的智能指针和生命周期,每一个特性都是那么晦涩难懂。我甚至在刚刚接触这些特性时,认为这些特性就是在束缚我,让我没法自由自在的编程。我甚至真的想过直接给我的所有rust代码套上一个unsafe。

但是没有束缚的自由也确实不是真正的自由。随着学校课程,尤其是操作系统课程的推进,让我知道了rust的所有权、借用规则以及智能指针,都是为了从语言层面提高整个计算机系统的性能,包括但不限于防止内存泄漏、不让指针乱飞、实现共享区互斥访问等。让我不用像写C语言一样,总是要考虑堆上变量有没有free,指针到底是几级指针,到底指的是什么玩意。不得不承认,经过rustlings110道练习的磨炼,我意识到,rust在通过它的特性,让我更自由的编程

从头开始学习一门语言是不容易的,尤其是学习像rust这样特性贼多的语言。我在这里要感谢一位之前一直被我忽视的一位朋友——编译器。在之前编程时,不论是写C还是写Java,又或者是写Go,我总是认为编译器只不过是在检查我的语句是不是合法罢了。但是在写rust时,我就深深感受到,没有rust编译器,单单靠我一团浆糊的脑子和厚厚的文档,我是学不会rust的,甚至说都难以写出符合rust语法的程序。每次的报错都让我能够更深地了解rust的语言特性,每次的help,都能让我精确无误的修改错误。编译器好像一直在对我说:“嘿,跟着哥,哥带你学rust。”

希望我能够在接下来的阶段乃至之后与rust同行的时间里,跟着rust编译器,不断加深对rust的理解。

嘿,把rust当做母语真的很酷好吗。

第二阶段总结报告

前言

进入到第二阶段的同时,学校其实也开始进入了期末考试的阶段,每天我说的最多的一句话就是:汗流浃背了。但是好在,我还是走完了这一艰难但是有意义的阶段。

1-2 应用程序基本执行环境与批处理系统

虽然第一章和第二章并没有让我动手实践,但是看着文档一步步从搭建最小环境,再到实现自己的print,再到实现riscv特权级的切换。很难想象这是我在两章里面能学到的东西。

3 多道程序与分时多任务

从这里开始,我终于能上手碰碰操作系统了。

这个实验主要是要让我通过rust实现一个任务状态查询的系统调用。我最自然的想法是,任务状态一定是跟任务绑定的,所以我直接在TCB中新增了一个计算系统调用的成员变量。实现下来也是非常自然顺畅的。但是我知道这个方法应该不是最好的方法,毕竟要在TCB中创建一个挺大的数组,而TCB是属于操作系统内核的,并且操作系统中可能有许多的任务,这样就会导致操作系统内核比较臃肿。

4 地址空间

这一章就涉及到了我之前很少接触到的部分——虚拟地址空间。

从sv39多级页表机制到内核与应用的地址空间,这些内容都挺让我头大的。虽然在进入第四章时走了点弯路,编写代码的时候甚至没有涉及到逻辑段。当后来发现其实框架代码中有一些函数接口可以直接调用(如:shrink_to、append_to、insert_framed_area等),我就开始吐槽之前浪费了太多的时间。但是现在想想,这一段小插曲让我对虚拟地址空间有了更深的了解:从分配物理页帧,到建立虚拟地址和物理地址之间的映射,再到用户程序逻辑段的构建。艰难但有意义。

5 进程及进程管理

本章主要实现操作系统中一个重要的系统调用——spawn。此外还涉及到了一种调度算法——stride调度算法

spawn与fork+exec的最大的区别就是,spawn并不需要像fork一样完全复制父进程的地址空间,并依此再创建一个TCB,然后再通过exec将TCB重写为需要执行的用户程序的TCB。它是直接根据新的用户程序创建一个TCB,省去了重写TCB的开销。所以如果要实现一个spawn系统调用,就只需要模仿fork来实现就好了。

而stride调度算法就是在TCB中增加两个成员stride和priority,在不考虑性能的情况下,只需要遍历就绪队列并执行 stride最小的任务即可。但是受于双端队列的限制,就只能从队列中取出一个TCB然后进行比较,如果stride比当前已经选出的TCBstride还大的话就重新将其放回就绪队列,这样可行,但是会导致更大的系统开销。

一些注意点

  • 由于系统终止一个进程的时候是根据TCB的Arc指针强引用数量来判断的,所以很多地方就不能让编译器帮我drop变量,而是需要进行显式的变量drop。

6 文件系统与I/O重定向

在这一章的实验中我对操作系统中的另一个重要部分——文件系统有了基本的认识

从最底层的块设备、缓存,再到上层的文件系统以及操作系统相关的系统调用,学习下来我的感觉就是——多且杂,搞不明白为什么要分这么多层。但是现在回想一下这一切都是有意义的,这些分层让文件系统的不同层的代码高度解耦合,提高了文件系统的可移植性。

实验要求实现建立硬链接、释放硬链接以及查询文件状态系统调用。对于建立硬链接,我就按照文档上的提示按部就班为相同的磁盘索引块再创建了一个目录项。但是对于释放硬链接需要考虑的东西就多了:当一个文件在释放当前硬链接之后还存在硬链接的话,就只需要将磁盘上的某个目录项删除;当一个文件在释放当前硬链接之后没有硬链接了,那就需要将其在磁盘上所有的空间回收(虽然有同学说就算没有回收也能过测试用例)。而对于查询文件状态,我就是将需要查询的字段作为成员放在磁盘索引节点中,这样就能实现就算系统断电,文件的状态也不会丢失(由于是存储在磁盘块上而不是在内存中)

一些注意点

  • 删除目录项的时候按道理来说不能仅仅是将该目录项清零,而是应该将后面的目录项移动到前面来,并且如果移动后刚好空出了一个磁盘块,还需要去回收该磁盘块。但是在我的代码中仅仅实现到了将后面的目录项移动到前面,之后有时间可以尝试去改进一下。
  • 在给磁盘索引节点增加成员的时候一定要减少直接索引的数量以保证一个磁盘块的大小是128字节。

7-8 进程间通信与并发

这两章的实验主要实现了一个重要的算法——死锁检测算法

这个算法其实在学校的课程中学过,是一个非常类似于银行家算法的算法,但是银行家算法是为了避免死锁。

这个算法中我觉得最让我摸不着头脑的就是need矩阵到底应该如何初始化。Available矩阵可以根据剩余的信号量或者锁是否被占用来实现;Allocation可以根据查询当前哪些线程在占据资源来确定。而need呢?显然不能单从信号量和锁的阻塞队列来确定。

从结果来看,其实应该是我的理解错了,我将死锁检测算法和银行家算法混淆了。我之前一直以为死锁检测算法中的need是一个线程在全局角度上对资源的需求量。而实际上need矩阵只是系统在当前状态下各个线程对资源的需求量,在每次需要分配资源时都需要调用死锁检测算法。所以死锁检测算法中的need矩阵是由信号量或者锁的阻塞队列以及当前请求资源的线程决定的。接下来只需要按照算法描述进行编码问题就迎刃而解了。

虽然第三阶段的时间与学校期末考试的时间完美重合,但是从第二阶段的学习中我真正感受到了操作系统的魅力,冲就完了。

第一阶段学习总结-ZhongkaiXu

写在前面

这是第二次写rustlings,两个月前应实习要求第一次学习rust和rcore,这次和上次相比稍微熟练了一些,从最开始的一头雾水,到现在已经对rust的简洁性佩服得五体投地。接下来要一些smmu相关的开发,借着这个机会再熟悉了一下rust。以下是一些学习心得。

学习心得

我认为的rust的核心思想

我觉得rust里最重要的是所有权机制,一个value只能对应一个所有者,即使是函数传参也是一样,虽然刚开始有些不习惯,但是慢慢发现对于锻炼系统软件开发者的思维很有效。

文档

在做题的时候我会同时打开rust文档,因为太多东西记不住了,需要随用随查,抛开水平不够,感觉这还算是一个不错的习惯,开了一个好头,毕竟后面几个阶段的学习必须依赖riscv/arm的手册。

最有挑战性的

我觉得在使用rust开发过程中,最难的地方在于如何把程序写的“rust”,比如对于寄存器的访问,rust中有一些优秀的宏如 registers_struct! ,第一次看到这个东西之前还一直停留在c风格的编程思想,包括写的一些算法题也是没有完全发挥出rust的语言特性,希望在后面学习的过程中多参考一些优秀代码,养成好的习惯。

未来展望

马上进入二阶段的学习,临近6月也要做很多其他的工作,希望能做到WLB吧,把握和优秀的同学们交流的机会,学到更多东西。

第二阶段学习总结-ZhongkaiXu

写在前面

终于在最后一天完成了五道题。

这是第二次写rCore的课后练习题了,之前偷的懒也还回来了。前三个练习之前写过,就还算顺利,后两个卡了蛮长的时间。

学习心得

模仿

其实练习里面需要自己构思代码的部分不多,大部分都是在原有代码的基础上添加一些辅助的功能,只要能设计好思路,剩下的就是模仿已经实现的代码。

比如mmap的实现,我最开始想用现成的 insert_framed_area,然后还写了一大堆判断是否重复映射的函数,结果在munmap的时候又要整个考虑释放area的部分页,写的自己都懵了。后来回头看原有代码的实现,发现其实mmap就是一个 insert_framed_area 的删减版,不需要area那么大的粒度,直接模仿area里面的btreemap设计一个容器,再重用 insert_framed_area 的代码就行了..

再比如发现自己还是没法摆脱c的风格,for能套个好几层,不过好在能想到rust里面应该不用这么麻烦,然后回头翻原有代码,看看其他地方怎么用迭代器的。

大胆写 反正有rust_analyzer 反正可以rollback

现在写rust没有那么提心吊胆怕和编译器同归于尽了,还是自动补全的功劳,很多地方分不清要不要解引用、分不清数据类型,不过rust_analyzer会搞定的。

有些时候乱写一通,最后发现内核挂掉了,只好rollback了,也在群里问过把仓库污染了怎么从头开始,不过大胆写也好过不写,经过n次rollback总算能上手敲代码了。

未来展望

第三阶段hypervisor我来了!

第二阶段 - OS设计实现

0 前言

由于我是二刷了,所以一阶段很快就搞完了,然后提前了一个月左右弄二阶段。最后赶在了五一假期前做完了二阶段的Lab。(PS:不得不说,群友都好强,Orz)

然后我五一假期过后,训练营学习基本上就有些摆了(除了上课就没干啥了)。一方面原因是学校课程忽然要考试了,二是不得不重视英语学习了,三是自制力下降了很多。

融化

虽然是二刷训练营了,但是这次是第一次参与到二阶段的学习中(上次二阶段还没开始就没搞了)。不得不说二阶段真的学到了好多关于OS相关的知识,同时代码的实操非常有用。(PS:Tutorial-Book-V3的step-by-step真的很棒)

我本人是非计算机专业的,对于本次二阶段来说,基本上算是零基础了(没学过计组、操作系统概论、CSAPP、计算机体系结构等)。不过好在的是我对于单片机的开发还是比较熟悉的,在二阶段的学习中我也发现了OS开发和单片机开发的很多相似之处,比如:STM32有个东西叫HAL库,OS中有个东西叫HAL层(也就是SBI),它两都是对硬件的一层抽象。

1 环境与工具软件等

RustRover

还是强推RustRover。毕竟rCore的代码量可不小,用vim在多个文件间切换太麻烦了。而且RR可以方便的查看函数的使用和trait的impl。

RR

另外提一下,我在使用Ubuntu22上的RR2024.3时遇到了闪退问题。换回到RR2023后解决了。

qemu

这个主要是用来模拟一个risc-v64的机器在我们的x86_64的电脑上。

在安装的时候可能会遇到一个坑,就是在执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装编译所需的依赖包
sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \
gawk build-essential bison flex texinfo gperf libtool patchutils bc \
zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev libsdl2-dev libslirp-dev \
git tmux python3 python3-pip ninja-build
# 下载源码包
# 如果下载速度过慢可以使用我们提供的百度网盘链接:https://pan.baidu.com/s/1dykndFzY73nqkPL2QXs32Q
# 提取码:jimc
wget https://download.qemu.org/qemu-7.0.0.tar.xz
# 解压
tar xvJf qemu-7.0.0.tar.xz
# 编译安装并配置 RISC-V 支持
cd qemu-7.0.0
./configure --target-list=riscv64-softmmu,riscv64-linux-user # 在第九章的实验中,可以有图形界面和网络。如果要支持图形界面,可添加 " --enable-sdl" 参数;如果要支持网络,可添加 " --enable-slirp" 参数
make -j$(nproc)

而后,可能会报错说缺少某个东西。这是因为第一步操作可能少了个需要的依赖。按照提示执行

1
sudo apt install <缺少的某个依赖>

2 OS知识

操作系统概述

站在应用程序的角度来看,我们可以发现常见的应用程序其实是运行在由硬件、操作系统内核、运行时库、图形界面支持库等所包起来的一个 执行环境 (Execution Environment) 中,如下图所示。

OS结构

异常控制流

在操作系统中,需要处理三类异常控制流:外设中断 (Device Interrupt) 、陷入 (Trap) 和异常 (Exception,也称Fault Interrupt)。

陷入 (Trap) 是程序在执行过程中由于要通过系统调用请求操作系统服务而有意引发的事件。产生陷入后,操作系统需要执行系统调用服务来响应系统调用请求,这会破坏陷入前应用程序的控制流上下文,所以操作系统要保存与恢复陷入前应用程序的控制流上下文。

异常控制流:陷入

RISC-V 特权级架构

RISC-V 架构中一共定义了 4 种特权级:

特权级 编码 名称 描述
0 00 用户/应用模式 (U) 用于运行普通的用户应用程序。在这个模式下,应用程序不能执行特权指令,也不能直接访问硬件资源。
1 01 监督模式 (S) 通常用于运行操作系统内核。在这个模式下,操作系统可以执行特权指令来管理进程、内存和其他系统资源,但是它不能直接访问所有的硬件资源。
2 10 虚拟监督模式 (H) 用于运行虚拟化管理程序(Hypervisor),它可以在物理硬件上管理多个虚拟机监视器(VMM)。Hypervisor模式可以执行一些特定的管理操作,但不是所有的机器级指令都是可用的。
3 11 机器模式 (M) 用于运行固件和操作系统内核。机器模式可以执行所有的指令,并且可以直接访问所有的硬件资源。通常,机器模式下的代码负责硬件管理和启动时的引导。

其中,级别的数值越大,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。在CPU硬件层面,除了M模式必须存在外,其它模式可以不存在。

从特权级架构的角度,去分析支持应用程序运行的执行环境栈,如下图所示:

特权级

其中,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中操作系统内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 监督模式执行环境 (SEE, Supervisor Execution Environment),如在操作系统运行前负责加载操作系统的 Bootloader – RustSBI。站在运行在 S 模式上的软件视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行,一般情况下 SEE 在 M 模式上运行。

文件系统

在操作系统的管理下,应用程序不用理解持久存储设备的硬件细节,而只需对 文件 这种持久存储数据的抽象进行读写就可以了,由操作系统中的文件系统和存储设备驱动程序一起来完成繁琐的持久存储设备的管理与读写。

文件系统

移植FATFS

之前移植FATFS文件系统到FeatOS(其实就是照着rCore写的,改了一点)中时,学习FATFS结构打的草稿。

FATFS

最后移植成功后,能够读取并运行FAT32文件系统镜像中的elf文件。

3 总结

这次总算是圆满完成第二阶段了,同时也取得了不错的成绩。文件系统和并发让我印象深刻,打算后面完成第三阶段后再回来详细的搞搞ch6后面的东西。

成绩

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

Rust 是一种高级程序设计语言

Rust 设计上的主要目标是保证内存安全,同时追求运行速度和内存利用率

个人认为,想要更好地理解和掌握 Rust,应该在探索 Rust 语法规则的同时思考 Rust 如何达成以上两个目标

抽象

高级程序设计语言的主要特征是高效的代码开发、管理和维护

在这一方面,Rust 提供了丰富的特性:

  • 泛型
  • 强大的宏特性
  • Attribute
  • 函数式
  • trait
  • 强大的包管理器 Cargo
  • 很好的学习资料和活跃的社区

静态分析

为了实现零成本抽象,Rust 选择花更多的时间在编译阶段的静态分析上

Rust 一大核心功能——所有权系统,正是基于静态分析的 RAII 思想实践

RAII 思想的提出主要是针对动态数据类型(位于堆上的大部分数据)的释放问题,其基本思想是在变量获取存储资源时就为其绑定一个生命周期,资源将在生命周期结束后被自动释放

所有权系统保证了资源不会被二次释放

To Be Continue

这篇简短的博客作为2024春夏季开源操作系统训练营第一阶段的总结,我在其中简单概述了我对 Rust 语言的理解

因为我所学尚浅,且没有足够的实践经历,导致以上内容十分简略,甚至可能有不少错误

我并不希望这些内容就这样简短的结束并被掩埋和遗忘,所以我将在我的个人博客上持续更新我对 Rust 的理解

Rust 具有安全和高效的特性,这使得它有希望被用于构建更好的操作系统

操作系统

在计算机的分层体系结构中,操作系统位于软件和硬件的分界处

操作系统的职责是帮助用户程序管理计算机硬件,这基于 CPU 提供 ISA 实现(在本次实验过程中,具体是 RISC-V)

操作系统的职责可以被进一步细分为几个方面——虚拟化、并发、持久性和驱动(和外部设备的通信)

在第二阶段,我们并没有太多的关于驱动的内容。虚拟化考虑如何在有限的硬件资源上为多个软件提供相互隔离的服务,并发考虑如何控制软件对共享资源的使用、持有性主要指文件系统

操作系统称得上是人类所设计的最复杂的一类程序,在这里你几乎能用上计算机中的所有知识

To Be Continue

这篇简短的博客作为2024春夏季开源操作系统训练营第二阶段的总结,我在其中简单概述了我对 Rust 用于构建操作系统的理解

因为我所学尚浅,且没有足够的实践经历,导致以上内容十分简略,甚至可能有不少错误

我并不希望这些内容就这样简短的结束并被掩埋和遗忘,所以我将在我的个人博客上持续更新我对使用 Rust 编写 OS 的理解

第一阶段总结

因为想学习 rust,所以找找有没有用 rust 开发的项目,就找到了 rCore,然后刚好看到有个春季训练营,就参加了。刚刷完官方的 Rustlings,所以第一阶段完成的还挺快的。

第二阶段总结

因为工作上也很忙,所以需要挤出时间来做,在 ddl 的最后一周才开始做,几乎一天一个 lab,也是有点忙坏了。

从课程上来看,因为在 21 年的时候有完成过 xv6-riscv ,所以 rCore 上手的时候主要难度还是在 rust 上面,经常用不明白 rust。又整体复习了一遍操作系统和 rust,希望后面能够继续去写 rust 代码,这感觉很有意思。

感谢清华大学, 感谢 rCore 社区

2024春夏rCore训练营第二阶段总结

参加过2023秋冬,所以ch3-ch5参考了之前的报告。

2023秋冬报告

这里讲讲我之前未接触过的lab。

Lab4-ch6

对文件系统的了解比较浅,之前也从未以代码的形式接触过文件系统,因此花了不少功夫理解easy-fs的设计。再和群友简短交流后,最终胜利完成了。

Lab5-ch8

手册给出的算法类似银行家算法,但不太一样。

我感觉没有理解到位,尤其是我自己打了打草稿后,发现不太能理解负值的semaphore要如何如何在这种方法下表示;同时我觉得用这种方法动态检测死锁,好像需要满足一些条件,比如线程p了一定会在后面v之类的。

然后在如何构建算法的矩阵上我也走了弯路,我首先很呆的在线程创建、mutex/sem创建的syscall里添加自定义struct的方法,来动态维护资源矩阵。这使得代码很乱,且static的struct导致运行多个程序时会出问题,需要特殊处理。这导致后面维护和debug很复杂。最后我选择推导重来,在进程这一层添加功能,并在pcb的mutex/sem_list获取各资源的数目,从task获取线程信息。这样功能几乎就集中在了少数地方,比之前轻松很多。

另外,文档中没有提示ch8需要实现时间的syscall,这导致即使死锁检测的逻辑出错,测试也必然出错。初见必然要debug一轮,感觉这个就有点坑了。

总结

想必于上一期此次要求500分全写完,要求提高了。不过这两个lab由于其内容我接触不多所以感觉还挺有意思的。

然后就是之前也提过的跨章代码合并,还有测试问题。我后来想了想,这好像也没什么办法;跨章合并是由rcore的风格导致的,上下两章代码可能是冲突的,不太好随着章节渐进更新,我见过的一些lab可以做到直接fetch + merge进入下一章;然后用测试框架每次都要恢复文件的问题,麻烦当然是有点麻烦的,但也许受制于某些因素只能这么设计的,如果这个能改了的话就很舒服了。

总的来说训练营还是很不错的,也有在更新维护;我想我们很多学生都应该多一些实际的代码经历,训练营就是这样一个机会。

RUST语言学习和Rcore实验感想

写在前面

​ 这篇博客可能长度有点长(),因为之前没get到要求,以为blog是从二阶段才开始要写hhhh,所以从来没写过就是了hhh~~

​ 这篇blog的主要内容是将我在学习RUST期间所做的笔记做一个整理(主要是来自于RUST语言圣经的节选)以便于自己后续回顾,以及将RUSTLINGS习题集中一些值得标注的语法问题做了记录。

​ :cry:二阶段得好好写了hhhh

RUST语言圣经笔记

数据类型

  1. 数据类型

    • 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
      • Nan表示未被定义的结果
      • debug模式检查整数溢出,release不会管
      • 浮点数不支持判等(eq操作未实现)
    • 字符串:字符串字面量和字符串切片 &str
    • 布尔类型: truefalse,1个字节
    • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
    • 单元类型: 即 () ,其唯一的值也是 ()

    一般来说不用显式声明,RUST编译器有变量推导

    比较逆天的话就不行了……

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let guess = "42".parse().expect("Not a number!");//推导不了

    //确定类型的三种方式
    // 编译器会进行自动推导,给予twenty i32的类型
    let twenty = 20;
    // 类型标注
    let twenty_one: i32 = 21;
    // 通过类型后缀的方式进行类型标注:22是i32类型
    let twenty_two = 22i32;
  2. 序列

    生成连续值,只允许用于数字和字符类型(编译器可在编译期确定类型和判空)

    1
    2
    3
    4
    5
    6
    7
    for i in 1..=5 {
    println!("{}",i);
    }

    for i in 'a'..='z' {
    println!("{}",i);
    }
  3. 函数

1
2
3
fn add(i: i32, j: i32) -> i32 {
i + j
}
  • 特殊返回类型

    • 无返回值

      • 函数没有返回值,那么返回一个 ()
      • 通过 ; 结尾的语句返回一个 ()
    • 发散函数:永不返回

      !作为函数的返回类型

所有权和借用

  1. C和RUST的内存管理差别

    1
    2
    3
    4
    5
    6
    7
    8
    int* foo() {
    int a; // 变量a的作用域开始
    a = 100;
    char *c = "xyz"; // 变量c的作用域开始
    return &a;
    } // 变量a和c的作用域结束
    //a是常数,被放在栈里,函数返回时出栈,a被回收,&a是悬空指针
    //c是字符串常量,在常量区,整个程序结束之后才会回收常量区
  2. 所有权规则

    • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者

    • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者

    • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let x = 5;
    let y = x;
    //浅拷贝,两个变量都依然有效

    let s1 = String::from("hello");
    let s2 = s1;
    //变量移动,默认是只copy指针,不会复制其实际内容
    //s1失效,s2接管那片内存空间

    let s1 = String::from("hello");
    let s2 = s1.clone();
    //你真想赋值的时候复制其内容,用clone()方法

    let x: &str = "hello, world";
    let y = x;
    //浅拷贝,"hello, world"是字符串字面量

    Copy特征:一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy

    • 所有整数类型,比如 u32
    • 布尔类型,bool,它的值是 truefalse
    • 所有浮点数类型,比如 f64
    • 字符类型,char
    • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是
    • 不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy的
  3. 函数传值和返回——所有权的不断变化

    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
    fn main() {
    let s1 = gives_ownership(); // gives_ownership 将返回值
    // 移给 s1

    let s2 = String::from("hello"); // s2 进入作用域

    let s3 = takes_and_gives_back(s2); // s2 被移动到
    // takes_and_gives_back 中,
    // 它也将返回值移给 s3
    } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
    // 所以什么也不会发生。s1 移出作用域并被丢弃

    fn gives_ownership() -> String { // gives_ownership 将返回值移动给
    // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string // 返回 some_string 并移出给调用的函数
    }

    // takes_and_gives_back 将传入字符串并返回该值
    fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string // 返回 a_string 并移出给调用的函数
    }
  4. 引用

    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
    fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);//传入的是引用而不是所有权

    println!("The length of '{}' is {}.", s1, len);
    }

    fn calculate_length(s: &String) -> usize {
    s.len()//拿到的是引用,因此函数结束的时候不会释放所有权
    }

    ————————————————
    //引用默认不可变(就是你不能动你借用的东西的值)
    fn main() {
    let mut s = String::from("hello");//可变引用(可以修改借用的东西)

    change(&mut s);
    }

    fn change(some_string: &mut String) {
    some_string.push_str(", world");
    }

    ————————————————
    //在同一个作用域只可以存在一个可变引用(互斥锁懂我意思吧……)
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;//r1的作用域还没寄,你怎么也搞个可变

    println!("{}, {}", r1, r2);


    ————————————————
    //可变和不可变引用不能同时存在
    let mut s = String::from("hello");

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题

    println!("{}, {}, and {}", r1, r2, r3);

    引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号结束。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
    } // 老编译器中,r1、r2、r3作用域在这里结束
    // 新编译器中,r3作用域在这里结束
    //Non-Lexical Lifetimes(NLL)特性:用于寻找到某个引用在`}`之前就不再被使用的位置

    悬垂引用在Rust是不会存在的,因为当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let reference_to_nothing = dangle();
    }

    fn dangle() -> &String {
    let s = String::from("hello");

    &s//悬垂引用,会报错
    //解决办法是直接返回s,也就是交出其所有权
    }

复合类型

字符串和切片

  1. Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)

  2. 为啥 String 可变,而字符串字面值 str 却不可以?

    就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行的过程中动态生成的。

  3. String和&str的转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //从&str生成String
    String::from("hello,world")
    "hello,world".to_string()

    //String到&str 取切片即可
    fn main() {
    let s = String::from("hello,world!");
    say_hello(&s);
    say_hello(&s[..]);
    say_hello(s.as_str());
    }

    fn say_hello(s: &str) {
    println!("{}",s);
    }
  4. 字符串索引(Rust不支持

    1
    2
    3
    4
    5
    let s1 = String::from("hello");
    let hello = String::from("中国人");
    let h = s1[0];
    let h = hello[0];
    //不同字符的编码长度是不一样的,英文是1byte,中文是3byte,对特定单元的索引不一定有意义

    还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

    字符串的区间切片Rust是支持的,但是必须谨慎

    1
    2
    let hello = "中国人";
    let s = &hello[0..2];
  5. 常见字符串操作

    • 追加和插入

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      //追加
      fn main() {
      let mut s = String::from("Hello ");//mut!

      s.push_str("rust");//追加字符串

      s.push('!');//追加单字符
      }

      //插入 insert需要插入位置和内容 位置越界会报错
      fn main() {
      let mut s = String::from("Hello rust!");//mut!
      s.insert(5, ',');
      s.insert_str(6, " I like");
      }
    • 替换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      //返回一个新的字符串,而不是操作原来的字符串!!!
      //replace 参数是:被替换内容,用来替换的内容
      fn main() {
      let string_replace = String::from("I like rust. Learning rust is my favorite!");
      let new_string_replace = string_replace.replace("rust", "RUST");
      }

      //replacen 和前面差不多,不过是替换n个匹配到的
      fn main() {
      let string_replace = "I like rust. Learning rust is my favorite!";
      let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
      dbg!(new_string_replacen);
      }

      //方法是直接操作原来的字符串,不会返回新的字符串!!!
      //replace_range 替换特定范围
      fn main() {
      let mut string_replace_range = String::from("I like rust!");//mut!!!
      string_replace_range.replace_range(7..8, "R");
      }
    • 删除

      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
      //直接操作原来的字符串  mut!!!
      //pop 删除并返回最后一个字符 由于不确保存在,返回的是Option()类型 需要具体考察
      fn main() {
      let mut string_pop = String::from("rust pop 中文!");
      let p1 = string_pop.pop();
      let p2 = string_pop.pop();
      }

      //remove 删除指定位置的一个字符 注意给的索引要合法(表示字符的起始位置)
      fn main() {
      let mut string_remove = String::from("测试remove方法");
      println!(
      "string_remove 占 {} 个字节",
      std::mem::size_of_val(string_remove.as_str())
      );
      // 删除第一个汉字
      string_remove.remove(0);
      // 下面代码会发生错误
      // string_remove.remove(1);
      // 直接删除第二个汉字
      // string_remove.remove(3);
      dbg!(string_remove);
      }

      //truncate 从当前位置直接删除到结尾 注意给的索引
      fn main() {
      let mut string_truncate = String::from("测试truncate");
      string_truncate.truncate(3);
      }

      //clear 清空
      fn main() {
      let mut string_clear = String::from("string clear");
      string_clear.clear();
      dbg!(string_clear);
      }
    • 连接

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      //+或+=   +右边的必须是切片引用类型
      //返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰
      fn main() {
      let string_append = String::from("hello ");
      let string_rust = String::from("rust");
      // &string_rust会自动解引用为&str
      let result = string_append + &string_rust;
      let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
      result += "!!!";

      println!("连接字符串 + -> {}", result);
      }

      //format!() 格式化输出
      fn main() {
      let s1 = "hello";
      let s2 = String::from("rust");
      let s = format!("{} {}!", s1, s2);
      println!("{}", s);
      }

元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//模式匹配解构元组
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("The value of y is: {}", y);
}

//用.访问元组
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

结构体

  1. 结构体语法

    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
    //创建
    struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
    }

    //初始化 每个字段都要初始化
    let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };

    //通过.来访问结构体内部字段
    let mut user1 = User { //要改的话还是要mut
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");

    //当函数参数和结构体字段名称一样时,可以简写
    fn build_user(email: String, username: String) -> User {
    User {
    email,
    username,//缩略的初始化
    active: true,
    sign_in_count: 1,
    }
    }

    //更新
    let user2 = User {
    email: String::from("another@example.com"),
    ..user1 //未显式声明的字段都会从user1中获取 不过..user1只可以写在末尾
    };//也就是说你要赋值的写在前面

    //更新过程可能会有某些字段发生了所有权的转移,不会影响其他字段的访问
    let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };
    let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("another@example.com"),
    sign_in_count: user1.sign_in_count,
    };
    println!("{}", user1.active);
    // 下面这行会报错
    println!("{:?}", user1);
  2. 元组结构体

    为整个结构体提供名称,而字段不需要

    1
    2
    3
    4
    5
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
  3. 单元结构体:没有任何字段和属性的结构体

枚举

  1. 任何数据类型都可以放到枚举中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum PokerCard {
    Clubs(u8),
    Spades(u8),
    Diamonds(char),//定义枚举成员时关联数据
    Hearts(char),
    }

    fn main() {
    let c1 = PokerCard::Spades(5);
    let c2 = PokerCard::Diamonds('A');
    }
  2. 枚举和结构体的对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //使用枚举来定义这些消息
    enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
    }

    fn main() {
    let m1 = Message::Quit;
    let m2 = Message::Move{x:1,y:1};
    let m3 = Message::ChangeColor(255,255,0);
    }

    //使用结构体来定义这些消息
    struct QuitMessage; // 单元结构体
    struct MoveMessage {
    x: i32,
    y: i32,
    }
    struct WriteMessage(String); // 元组结构体
    struct ChangeColorMessage(i32, i32, i32); // 元组结构体

    //由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。
  3. 取代NULL的方式——Option()枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //Option()枚举定义
    enum Option<T> {
    Some(T), //T可以是任何类型
    None,
    }

    //示例
    ——————————————

    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;
    //当有个None值时,你需要告诉编译器T的类型,因为编译器无法通过None来推断本来应该是什么
  4. Option()枚举的好处

    1
    2
    3
    4
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;//报错!Option(i8)和i8并不是同一种类型

    当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值,我们可以放心使用而无需做空值检查。只有当使用 Option<i8>(或者任何用到的类型)的时候才需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。

    换句话说,在对 Option<T> 进行 T 的运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:期望某值不为空但实际上为空的情况。

  5. match表达式可以用于处理枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
    None => None,
    Some(i) => Some(i + 1),
    }//如果接收到Some(i)类型,将其中的变量绑定到i上,计算i+1,再将其用Some()包裹
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

数组

  1. 创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //RUST的数组是定长的,被存储在栈上
    //变长的动态数组被存储在堆上
    //数组的长度也是类型的一部分
    let a: [i32; 5] = [1, 2, 3, 4, 5];

    //声明多个重复值
    let a = [3; 5];

    //非基础类型数组的创建

    //这样子写会报错,本质还是因为string不能浅拷贝
    let array = [String::from("rust is good!"); 8];

    //这样子写可以,但是很难看
    let array = [String::from("rust is good!"),String::from("rust is good!"),String::from("rust is good!")];

    //遇到非基本类型数组 调用std::array::from_fn
    let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));
  2. 支持索引访问,如果越界会崩溃

流程控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
for i in 1..=5 {
println!("{}", i);
}
}

//如果想在循环中获取元素的索引,使用.iter()方法获得迭代器
fn main() {
let a = [4, 3, 2, 1];
// `.iter()` 方法把 `a` 数组变成一个迭代器
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
}
使用方法 等价使用方式 所有权
for item in collection for item in IntoIterator::into_iter(collection) 转移所有权
for item in &collection for item in collection.iter() 不可变借用
for item in &mut collection for item in collection.iter_mut() 可变借用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 第一种
let collection = [1, 2, 3, 4, 5];
for i in 0..collection.len() {
let item = collection[i];
// ...
}

// 第二种
for item in collection {

}


//while循环
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;

while index < 5 {
println!("the value is: {}", a[index]);

index = index + 1;
}
}//用while循环来实现和第一种for循环是一样的

第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环集合中的元素,优劣如下:

  • 性能:第一种使用方式中 collection[index] 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust 会检查并确认 index 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的
  • 安全:第一种方式里对 collection 的索引访问是非连续的,存在一定可能性在两次访问之间,collection 发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险( 由于所有权限制,在访问过程中,数据并不会发生变化)。

loop:简单的无限循环,需要搭配break跳出

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;//break可以单独使用直接跳出,也可以带一个值返回(类似return)
}
};

println!("The result is {}", result);
}

模式匹配

match和if let

  1. 匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => {
    println!("Lucky penny!");
    1
    },
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
    }
    }
    //match匹配需要穷尽所有的可能,用_表示没有列出的其他可能性(如果没有穷尽可能性的话会报错)
    //match的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  2. 模式绑定

    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
    #[derive(Debug)]
    enum UsState {
    Alabama,
    Alaska,
    // --snip--
    }

    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState), // 25美分硬币
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter(state) => {//这里将枚举类别Coin中的UsState值绑定给state变量
    println!("State quarter from {:?}!", state);
    25
    },
    }
    }
  3. if let匹配

    当我们只关注某个特定的值的匹配情况时,可以使用if let匹配代替match

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let v = Some(3u8);
    match v {
    Some(3) => println!("three"),
    _ => (),
    }

    //if let匹配
    if let Some(3) = v {
    println!("three");
    }
  4. matches!()宏

    将表达式和模式进行匹配,返回True或者False

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    enum MyEnum {
    Foo,
    Bar
    }

    fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
    }
    //对v进行过滤,只保留类型为MyEnum::Foo的元素
    v.iter().filter(|x| matches!(x, MyEnum::Foo));

    //更多例子
    let foo = 'f';
    assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

    let bar = Some(4);
    assert!(matches!(bar, Some(x) if x > 2));
  5. match和if let匹配导致的变量遮蔽

    ​ 尽量不要使用同名变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    fn main() {
    let age = Some(30);
    println!("在匹配前,age是{:?}",age);
    if let Some(age) = age {
    println!("匹配出来的age是{}",age);
    }

    println!("在匹配后,age是{:?}",age);
    }

    fn main() {
    let age = Some(30);
    println!("在匹配前,age是{:?}",age);
    match age {
    Some(age) => println!("匹配出来的age是{}",age),
    _ => ()
    }
    println!("在匹配后,age是{:?}",age);
    }

一些模式适用场景

  1. while let 只要匹配就会一直循环下去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Vec是动态数组
    let mut stack = Vec::new();

    // 向数组尾部插入元素
    stack.push(1);
    stack.push(2);
    stack.push(3);

    // stack.pop从数组尾部弹出元素
    while let Some(top) = stack.pop() {
    println!("{}", top);
    }
  2. let和if let

    1
    2
    3
    4
    5
    6
    let Some(x) = some_option_value;//报错,有可能是None
    //let,match,for都需要完全匹配(不可驳匹配)

    if let Some(x) = some_option_value {
    println!("{}", x);
    }//通过,只要有值的情况,其余情况忽略(可驳模式匹配)

全模式列表

  1. 用序列语法..=匹配区间内的值(还是只能用于数字和字符)

    1
    2
    3
    4
    5
    6
    let x = 5;

    match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
    }
  2. 使用模式忽略值

    1
    2
    3
    4
    5
    6
    7
    8
    //忽略函数变量
    fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
    }

    fn main() {
    foo(3, 4);
    }

    _忽略值和用_s的区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
    println!("found a string");
    }

    println!("{:?}", s);//会报错,因为s的所有权已经转移给_s了

    ——————————————————————————

    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
    println!("found a string");
    }

    println!("{:?}", s);//使用下划线本身是不会绑定值的
  3. 使用..忽略多个值需要保证没有歧义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
    (.., second, ..) => {
    println!("Some numbers: {}", second)
    },
    }
    }//报错,编译器无法理解second具体指哪个
  4. 匹配守卫——为匹配提供额外条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn main() {
    let x = Some(5);
    let y = 10;

    match x {
    Some(50) => println!("Got 50"),
    Some(n) if n == y => println!("Matched, n = {}", n),
    //通过匹配守卫,使得在匹配中也可以正常的使用外部变量,而不用担心变量遮蔽的问题
    _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {}", x, y);
    }

    ——————————————————
    //匹配守卫的优先级:会作用于所有的匹配项
    let x = 4;
    let y = false;

    match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
    }
  5. @绑定——提供在限定范围条件下,在分支代码内部使用变量的能力

    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
    enum Message {
    Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
    Message::Hello { id: id_variable @ 3..=7 } => {
    println!("Found an id in range: {}", id_variable)
    },//@变量绑定,限定范围且绑定变量
    Message::Hello { id: 10..=12 } => {
    println!("Found an id in another range")
    },//限定了范围,但是这样子只会匹配,而id这个量用不了
    Message::Hello { id } => {
    println!("Found some other id: {}", id)
    },//可以匹配并绑定到id上,但是这样子限制不了范围
    }


    ————————————————
    //绑定的同时对变量结构
    #[derive(Debug)]
    struct Point {
    x: i32,
    y: i32,
    }

    fn main() {
    // 绑定新变量 `p`,同时对 `Point` 进行解构
    let p @ Point {x: px, y: py } = Point {x: 10, y: 23};
    println!("x: {}, y: {}", px, py);
    println!("{:?}", p);

    let point = Point {x: 10, y: 5};
    if let p @ Point {x: 10, y} = point {
    println!("x is 10 and y is {} in {:?}", y, p);
    } else {
    println!("x was not 10 :(");
    }
    }

方法Method

  1. 定义和初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct Circle {
    x: f64,
    y: f64,
    radius: f64,
    }

    impl Circle {
    // new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
    // 这种方法往往用于初始化当前结构体的实例
    fn new(x: f64, y: f64, radius: f64) -> Circle {
    Circle {
    x: x,
    y: y,
    radius: radius,
    }
    }

    // Circle的方法,&self表示借用当前的Circle结构体
    fn area(&self) -> f64 {
    std::f64::consts::PI * (self.radius * self.radius)
    }
    }

    ​ 这种定义在 impl 中且没有 self 的函数被称之为关联函数: 因为它没有 self,不能用 f.read() 的形式调用,因此它是一个函数而不是方法,它又在 impl 中,与结构体紧密关联,因此称为关联函数。

    ​ 因为是函数,所以不能用 . 的方式来调用,我们需要用 :: 来调用,例如 let sq = Rectangle::new(3, 3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。

    其他的语言往往将类型和方法一起定义,而Rust对这两者的定义是分开的。

  2. self和被实例化类型的关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #[derive(Debug)]
    struct Rectangle {
    width: u32,
    height: u32,
    }

    impl Rectangle {//方法名称可以和结构体的名称相同
    fn area(&self) -> u32 {
    self.width * self.height
    }
    //self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
    //&self 表示该方法对 Rectangle 的不可变借用
    //&mut self 表示可变借用

    }

    fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
    "The area of the rectangle is {} square pixels.",
    rect1.area()
    );
    }
  3. 方法和字段同名的好处

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    pub struct Rectangle {
    width: u32,
    height: u32,
    }

    impl Rectangle {
    pub fn new(width: u32, height: u32) -> Self {
    Rectangle { width, height }
    }
    pub fn width(&self) -> u32 {
    return self.width;
    }
    }

    fn main() {
    let rect1 = Rectangle::new(30, 50);

    println!("{}", rect1.width());
    }

    ​ 方法和字段同名有助于我们实现访问器,我们可以将widthheight设置为私有属性,而通过pub关键字将Rectangle结构体对应的new方法和width方法设置为公有方法,这样子用户可以通过rect1.width()方法访问到宽度的数据,却无法直接使用rect1.width来访问。

  4. Rust中用自动引用/解引用机制代替了C/C++的->运算符

    ​ 在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用指针。换句话说,如果 object 是一个指针,那么 object->something()(*object).something() 是一样的。

    ​ Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

    ​ 他是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut* 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:

    1
    2
    p1.distance(&p2);
    (&p1).distance(&p2);

    ​ 第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。

泛型和特征

泛型

  1. 代替值的泛型,而不是针对类型的泛型

    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
    //这段代码会报错,因为不同长度的数组在Rust中是不同的类型
    fn display_array(arr: [i32; 3]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
    }

    //用切片的方式打印任意长度的数组,同时用泛型指代不同的类型
    fn display_array<T: std::fmt::Debug>(arr: &[T]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(&arr);

    let arr: [i32; 2] = [1, 2];
    display_array(&arr);
    }

    //切片是一种引用,但是有的场景不允许我们使用引用,此时通过const泛型指代不同的长度
    fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
    }
  2. 泛型的性能

    编译器完成单态化的过程,增加了编译的繁琐程度,也让编译后的文件更大

    会对每一个具体用到的类型都生成一份代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //程序编写
    let integer = Some(5);
    let float = Some(5.0);

    //编译后
    enum Option_i32 {
    Some(i32),
    None,
    }

    enum Option_f64 {
    Some(f64),
    None,
    }

    fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
    }

特征

​ 一组可以被共享的行为,只要满足了特征,就可以做以下的行为。

  1. 定义特征

    ​ 只管定义,而往往不会提供具体的实现

    ​ 谁满足这个特征,谁来实现具体的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    pub trait Summary {
    fn summarize(&self) -> String;//以;结尾 只提供定义
    }

    pub trait Summary {
    fn summarize(&self) -> String { //也可以给一个默认实现
    String::from("(Read more...)")
    }//可以调用,也可以重载
    }

    默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如,我们可以定义 Summary 特征,使其具有一个需要实现的 summarize_author 方法,然后定义一个 summarize 方法,此方法的默认实现调用 summarize_author 方法:

    1
    2
    3
    4
    5
    6
    7
    pub trait Summary {
    fn summarize_author(&self) -> String;//让实现Summary特征的类型具体实现吧

    fn summarize(&self) -> String {
    format!("(Read more from {}...)", self.summarize_author())
    }
    }
  2. 实现特征

    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
    pub trait Summary {
    fn summarize(&self) -> String;
    }
    pub struct Post {
    pub title: String, // 标题
    pub author: String, // 作者
    pub content: String, // 内容
    }

    impl Summary for Post {//为Post实现Summary特征
    fn summarize(&self) -> String {
    format!("文章{}, 作者是{}", self.title, self.author)
    }
    }

    pub struct Weibo {
    pub username: String,
    pub content: String
    }

    impl Summary for Weibo {
    fn summarize(&self) -> String {
    format!("{}发表了微博{}", self.username, self.content)
    }
    }
  3. 孤儿规则——特征定义和实现的位置关系

    ​ 关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的! 例如我们可以为上面的 Post 类型实现标准库中的 Display 特征,这是因为 Post 类型定义在当前的作用域中。同时,我们也可以在当前包中为 String 类型实现 Summary 特征,因为 Summary 定义在当前作用域中。

    ​ 但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域,跟你半毛钱关系都没有,看看就行了。

  4. 使用特征作为函数的参数

    1
    2
    3
    pub fn notify(item: &impl Summary) {//实现了特征Summary的item参数
    println!("Breaking news! {}", item.summarize());//可以调用特征对应的方法
    }
  5. 特征约束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //接收两个实现了Summary特征的参数,但是不能保证这两个参数的类型相同
    pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

    //用泛型T指代
    //T:Summary要求其实现了特征Summary
    pub fn notify<T: Summary>(item1: &T, item2: &T) {}

    //多重约束
    //这里T被要求同时实现两个特征才行
    pub fn notify<T: Summary + Display>(item: &T) {}

    //Where约束,主要是用于简化函数的签名,将特征约束写在别处
    fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
    U: Clone + Debug
    {}
  6. 函数返回值中的impl Trait

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn returns_summarizable() -> impl Summary {
    //返回一个实现了Summary特征的类型,具体是什么类型不知道
    Weibo {
    username: String::from("sunface"),
    content: String::from(
    "m1 max太厉害了,电脑再也不会卡",
    )
    }
    }

    ​ 这种 impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回。

  7. derive派生特征

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

    ​ 例如 Debug 特征,它有一套自动实现的默认代码,当你给一个结构体标记后,就可以使用 println!("{:?}", s) 的形式打印该结构体的对象。

    ​ 再如 Copy 特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现 Copy 特征,进而可以调用 copy 方法,进行自我复制。

    ​ 总之,derive 派生出来的是 Rust 默认给我们提供的特征,在开发过程中极大的简化了自己手动实现相应特征的需求,当然,如果你有特殊的需求,还可以自己手动重载该实现。

特征对象

​ 指向了所有实现了某特征的对象,二者之间存在映射关系,可以通过特征对象找到该对象具体的实现方法。

  1. 可以通过 & 引用或者 Box<T> 智能指针的方式来创建特征对象

    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
    trait Draw {
    fn draw(&self) -> String;
    }

    impl Draw for u8 {
    fn draw(&self) -> String {
    format!("u8: {}", *self)
    }
    }

    impl Draw for f64 {
    fn draw(&self) -> String {
    format!("f64: {}", *self)
    }
    }

    // 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
    fn draw1(x: Box<dyn Draw>) {
    // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
    x.draw();
    }

    fn draw2(x: &dyn Draw) {
    x.draw();
    }

    fn main() {
    let x = 1.1f64;
    // do_something(&x);
    let y = 8u8;

    // x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在函数调用时隐式地被转换为特征对象 Box<dyn Draw>
    // 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上
    draw1(Box::new(x));
    // 基于 y 的值创建一个 Box<u8> 类型的智能指针
    draw1(Box::new(y));
    draw2(&x);
    draw2(&y);
    }
    • draw1 函数的参数是 Box<dyn Draw> 形式的特征对象,该特征对象是通过 Box::new(x) 的方式创建的
    • draw2 函数的参数是 &dyn Draw 形式的特征对象,该特征对象是通过 &x 的方式创建的
    • dyn 关键字只用在特征对象的类型声明上,在创建时无需使用 dyn

    可以通过特征对象来代表具体的泛型。

  2. 使用泛型的实现和特征对象的对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pub struct Screen<T: Draw> {
    pub components: Vec<T>,
    }

    impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
    for component in self.components.iter() {
    component.draw();
    }
    }
    }

    ​ 上面的 Screen 的列表中,存储了类型为 T 的元素,然后在 Screen 中使用特征约束让 T 实现了 Draw 特征,进而可以调用 draw 方法。

    ​ 但是这种写法限制了 Screen 实例的 Vec<T> 中的每个元素必须是 Button 类型或者全是 SelectBox 类型。如果只需要同质(相同类型)集合,更倾向于采用泛型+特征约束这种写法,因其实现更清晰,且性能更好(特征对象,需要在运行时从 vtable 动态查找需要调用的方法)。

  3. 特征对象的限制

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

    • 方法的返回类型不能是 Self
    • 方法没有任何泛型参数

    对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再需要知道实现该特征的具体类型是什么了。如果特征方法返回了具体的 Self 类型,但是特征对象忘记了其真正的类型,那这个 Self 就非常尴尬,因为没人知道它是谁了。但是对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。而当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么。

    标准库中的 Clone 特征就不符合对象安全的要求:

    1
    2
    3
    pub trait Clone {
    fn clone(&self) -> Self;
    }

    因为它的其中一个方法,返回了 Self 类型,因此它是对象不安全的。

  4. 特征对象的动态分发

    ​ 静态分发:编译器会为每一个泛型参数对应的具体类型生成一份代码

    ​ 动态分发:直到运行时,才能确定需要调用什么方法。编译器无法知晓所有可能用于特征对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。

    img

    • 特征对象大小不固定:这是因为,对于特征 Draw,类型 Button 可以实现特征 Draw,类型 SelectBox 也可以实现特征 Draw,因此特征没有固定大小
    • 几乎总是使用特征对象的引用方式,如&dyn DrawBox<dyn Draw>
      • 虽然特征对象没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成(ptrvptr),因此占用两个指针大小
      • 一个指针 ptr 指向实现了特征 Draw 的具体类型的实例,也就是当作特征 Draw 来用的类型的实例,比如类型 Button 的实例、类型 SelectBox 的实例
      • 另一个指针 vptr 指向一个虚表 vtablevtable 中保存了类型 Button 或类型 SelectBox 的实例对于可以调用的实现于特征 Draw 的方法。当调用方法时,直接从 vtable 中找到方法并调用。之所以要使用一个 vtable 来保存各实例的方法,是因为实现了特征 Draw 的类型有多种,这些类型拥有的方法各不相同,当将这些类型的实例都当作特征 Draw 来使用时(此时,它们全都看作是特征 Draw 类型的实例),有必要区分这些实例各自有哪些方法可调用

    简而言之,当类型 Button 实现了特征 Draw 时,类型 Button 的实例对象 btn 可以当作特征 Draw 的特征对象类型来使用,btn 中保存了作为特征对象的数据指针(指向类型 Button 的实例数据)和行为指针(指向 vtable)。

    一定要注意,此时的 btnDraw 的特征对象的实例,而不再是具体类型 Button 的实例,而且 btnvtable 只包含了实现自特征 Draw 的那些方法(比如 draw),因此 btn 只能调用实现于特征 Drawdraw 方法,而不能调用类型 Button 本身实现的方法和类型 Button 实现于其他特征的方法。也就是说,btn 是哪个特征对象的实例,它的 vtable 中就包含了该特征的方法。

特征进阶内容

  1. 关联类型

    在特征定义的语句块中,声明一个自定义类型,这样就可以在特征中使用这个类型。

    1
    2
    3
    4
    5
    pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
    }

Rustlings习题整理

map()的用法

1
2
3
4
5
6
7
8
fn vec_map(v: &Vec<i32>) -> Vec<i32> {
v.iter().map(|element| {
// TODO: Do the same thing as above - but instead of mutating the
// Vec, you can just return the new number!
*element * 2
}).collect()
}
//map方法由原来的迭代器生成一个新的迭代器,对旧迭代器的每一个方法都调用该闭包

字符串和切片操作

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
fn trim_me(input: &str) -> String {
// TODO: Remove whitespace from both ends of a string!
input.trim().to_string()
}

fn compose_me(input: &str) -> String {
// TODO: Add " world!" to the string! There's multiple ways to do this!
input.to_string() + " world!"
}

fn replace_me(input: &str) -> String {
// TODO: Replace "cars" in the string with "balloons"!
input.replace("cars","balloons").to_string()
}

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

#[test]
fn trim_a_string() {
assert_eq!(trim_me("Hello! "), "Hello!");
assert_eq!(trim_me(" What's up!"), "What's up!");
assert_eq!(trim_me(" Hola! "), "Hola!");
}

#[test]
fn compose_a_string() {
assert_eq!(compose_me("Hello"), "Hello world!");
assert_eq!(compose_me("Goodbye"), "Goodbye world!");
}

#[test]
fn replace_a_string() {
assert_eq!(replace_me("I think cars are cool"), "I think balloons are cool");
assert_eq!(replace_me("I love to look at cars"), "I love to look at balloons");
}
}

判断字符串和字符串切片的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn string_slice(arg: &str) {
println!("{}", arg);
}
fn string(arg: String) {
println!("{}", arg);
}

fn main() {
string_slice("blue");
string("red".to_string());
string(String::from("hi"));
string("rust is fun!".to_owned());
//to_owned()方法用于从借用的数据中创建一个具有所有权的副本
//和clone方法的区别是如果传入的参数是引用类型的,可以通过复制获得其所有权
string_slice("nice weather".into());
string(format!("Interpolation {}", "Station"));
string_slice(&String::from("abc")[0..1]);
string_slice(" hello there ".trim());
string("Happy Monday!".to_string().replace("Mon", "Tues"));
string("mY sHiFt KeY iS sTiCkY".to_lowercase());
//to_lowercase()返回此字符串切片的小写等效项,类型为string
}
clone() to_owned()
T T -> T T -> T
&T &T -> &T &T -> T

模块use

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod delicious_snacks {
// TODO: Fix these use statements
pub use self::fruits::PEAR as fruit;//修改为pub才对外可见
pub use self::veggies::CUCUMBER as veggie;

mod fruits {
pub const PEAR: &'static str = "Pear";
pub const APPLE: &'static str = "Apple";
}

mod veggies {
pub const CUCUMBER: &'static str = "Cucumber";
pub const CARROT: &'static str = "Carrot";
}
}

fn main() {
println!(
"favorite snacks: {} and {}",
delicious_snacks::fruit,
delicious_snacks::veggie
);
}

比赛统计

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
use std::collections::HashMap;

// A structure to store the goal details of a team.
struct Team {
goals_scored: u8,
goals_conceded: u8,
}

fn build_scores_table(results: String) -> HashMap<String, Team> {
// The name of the team is the key and its associated struct is the value.
let mut scores: HashMap<String, Team> = HashMap::new();

for r in results.lines() {
let v: Vec<&str> = r.split(',').collect();
let team_1_name = v[0].to_string();
let team_1_score: u8 = v[2].parse().unwrap();
let team_2_name = v[1].to_string();
let team_2_score: u8 = v[3].parse().unwrap();
//注意Team是没有实现可加性的,只可以按照Team内部的元素来操作
let team1 = scores.entry(team_1_name).or_insert(Team{
goals_scored:0,
goals_conceded:0
});
team1.goals_scored += team_1_score;
team1.goals_conceded += team_2_score;

let team2 = scores.entry(team_2_name).or_insert(Team{
goals_scored:0,
goals_conceded:0
});
team2.goals_scored += team_2_score;
team2.goals_conceded += team_1_score;
}
scores
}

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

fn get_results() -> String {
let results = "".to_string()
+ "England,France,4,2\n"
+ "France,Italy,3,1\n"
+ "Poland,Spain,2,0\n"
+ "Germany,England,2,1\n";
results
}

#[test]
fn build_scores() {
let scores = build_scores_table(get_results());

let mut keys: Vec<&String> = scores.keys().collect();
keys.sort();
assert_eq!(
keys,
vec!["England", "France", "Germany", "Italy", "Poland", "Spain"]
);
}

#[test]
fn validate_team_score_1() {
let scores = build_scores_table(get_results());
let team = scores.get("England").unwrap();
assert_eq!(team.goals_scored, 5);
assert_eq!(team.goals_conceded, 4);
}

#[test]
fn validate_team_score_2() {
let scores = build_scores_table(get_results());
let team = scores.get("Spain").unwrap();
assert_eq!(team.goals_scored, 0);
assert_eq!(team.goals_conceded, 2);
}
}

quiz2

首先要观察代码判断其类型,随后用match表达式匹配枚举类型,做出相应的处理

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
pub enum Command {
Uppercase,
Trim,
Append(usize),
}

mod my_module {
use super::Command;

// TODO: Complete the function signature!
pub fn transformer(input: Vec<(String,Command)>) -> Vec<String> {
// TODO: Complete the output declaration!
let mut output: Vec<String> = vec![];
for (string, command) in input.iter() {
// TODO: Complete the function body. You can do it!
let applied_string:String = match command{
Command::Uppercase => string.to_uppercase(),
Command::Trim => string.trim().to_string(),
Command::Append(n) => format!("{}{}",string,"bar".repeat(*n)),
};
output.push(applied_string);
}
output
}
}

#[cfg(test)]
mod tests {
// TODO: What do we need to import to have `transformer` in scope?
use crate::my_module::transformer;
use super::Command;

#[test]
fn it_works() {
let output = transformer(vec![
("hello".into(), Command::Uppercase),
(" all roads lead to rome! ".into(), Command::Trim),
("foo".into(), Command::Append(1)),
("bar".into(), Command::Append(5)),
]);
assert_eq!(output[0], "HELLO");
assert_eq!(output[1], "all roads lead to rome!");
assert_eq!(output[2], "foobar");
assert_eq!(output[3], "barbarbarbarbarbar");
}
}

从Option中取出值

1
2
3
4
5
6
7
8
9
10
11
12
//如果你确定Option中是有值的,可以使用unwrap()方法直接取出来
let my_option: Option<i32> = Some(5); // 一个Option<i32>类型的变量

let value = my_option.unwrap();
println!("The value is: {}", value);

//如果要处理可能有None的情况,可以使用unwrap_or(初始值)
//为None的情况设置一个初始值
let my_option: Option<i32> = Some(5); // 一个Option<i32>类型的变量

let value = my_option.unwrap_or(0); // 如果my_option是None,则使用默认值0
println!("The value is: {}", value);

Option的类型问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let range = 10;
let mut optional_integers: Vec<Option<i8>> = vec![None];

for i in 1..(range + 1) {
optional_integers.push(Some(i));
}

let mut cursor = range;

//pop()函数会包着一层Some()在外面
while let Some(Some(integer)) = optional_integers.pop() {
assert_eq!(integer, cursor);
cursor -= 1;
}

所有权的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Point {
x: i32,
y: i32,
}

fn main() {
let y: Option<Point> = Some(Point { x: 100, y: 200 });

match y {
Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
//加ref是为了防止所有权的转移
_ => panic!("no match!"),
}
y; // Fix without deleting this line.
}

? 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl ParsePosNonzeroError {
fn from_creation(err: CreationError) -> ParsePosNonzeroError {
ParsePosNonzeroError::Creation(err)
}
// TODO: add another error conversion function here.
fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError{
ParsePosNonzeroError::ParseInt(err)
}
}

fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> {
// TODO: change this to return an appropriate error instead of panicking
// when `parse()` returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation)
}

为动态数组Vector实现特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait AppendBar {
fn append_bar(self) -> Self;
}

// TODO: Implement trait `AppendBar` for a vector of strings.
impl AppendBar for Vec<String>{
fn append_bar(mut self) -> Self{ //声明可变mut
self.push("Bar".to_string());
self
}
}

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

#[test]
fn is_vec_pop_eq_bar() {
let mut foo = vec![String::from("Foo")].append_bar();
assert_eq!(foo.pop().unwrap(), String::from("Bar"));
assert_eq!(foo.pop().unwrap(), String::from("Foo"));
}
}

特征约束代替类型

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
pub trait Licensed {
fn licensing_info(&self) -> String {
"some information".to_string()
}
}

struct SomeSoftware {}

struct OtherSoftware {}

impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}

// YOU MAY ONLY CHANGE THE NEXT LINE
fn compare_license_types(software:impl Licensed, software_two:impl Licensed) -> bool {
software.licensing_info() == software_two.licensing_info()
}
//下面有SomeSoftware和OtherSoftware两种类型
//用impl Licensed可以指代他们

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

#[test]
fn compare_license_information() {
let some_software = SomeSoftware {};
let other_software = OtherSoftware {};

assert!(compare_license_types(some_software, other_software));
}

#[test]
fn compare_license_information_backwards() {
let some_software = SomeSoftware {};
let other_software = OtherSoftware {};

assert!(compare_license_types(other_software, some_software));
}
}

#[should_panic]

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
struct Rectangle {
width: i32,
height: i32
}

impl Rectangle {
// Only change the test functions themselves
pub fn new(width: i32, height: i32) -> Self {
if width <= 0 || height <= 0 {
panic!("Rectangle width and height cannot be negative!")
}
Rectangle {width, height}
}
}

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

#[test]
fn correct_width_and_height() {
// This test should check if the rectangle is the size that we pass into its constructor
let rect = Rectangle::new(10, 20);
assert_eq!(rect.width, 10); // check width
assert_eq!(rect.height, 20); // check height
}

#[test]
#[should_panic]
fn negative_width() {
// This test should check if program panics when we try to create rectangle with negative width
let _rect = Rectangle::new(-10, 10);

}

#[test]
#[should_panic]
fn negative_height() {
// This test should check if program panics when we try to create rectangle with negative height
let _rect = Rectangle::new(10, -10);
}
}

迭代器方法

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
pub fn capitalize_first(input: &str) -> String {
let mut c = input.chars();
match c.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + c.as_str(),
}
}

// Step 2.
// Apply the `capitalize_first` function to a slice of string slices.
// Return a vector of strings.
// ["hello", "world"] -> ["Hello", "World"]
pub fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
words.iter().map(
|&word| {
capitalize_first(word)
}
).collect()
}

// Step 3.
// Apply the `capitalize_first` function again to a slice of string slices.
// Return a single string.
// ["hello", " ", "world"] -> "Hello World"
pub fn capitalize_words_string(words: &[&str]) -> String {
words.iter().map(
|&word| {
capitalize_first(word)
}
).collect()
}

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

#[test]
fn test_success() {
assert_eq!(capitalize_first("hello"), "Hello");
}

#[test]
fn test_empty() {
assert_eq!(capitalize_first(""), "");
}

#[test]
fn test_iterate_string_vec() {
let words = vec!["hello", "world"];
assert_eq!(capitalize_words_vector(&words), ["Hello", "World"]);
}

#[test]
fn test_iterate_into_string() {
let words = vec!["hello", " ", "world"];
assert_eq!(capitalize_words_string(&words), "Hello World");
}
}

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
//宏的定义要在使用之前
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}

fn main() {
my_macro!();
}

//使用分号区分不同的模式
#[rustfmt::skip]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
}; //使用分号来区分不同的模式
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}

fn main() {
my_macro!();
my_macro!(7777);
}

Rcore实验感想

写在前面

由于实验要求不能够贴代码,因此本报告重点就是日记这种了……

会简单的讲一下过程

Lab3

看了一下自己写的东西……玛德我原本也贴了不少代码

行吧……那就放些记录性的东西

……

报错挺多的,反正就照着编译器一个一个来吧……

类型错误……

image-20240428141340827

missing documentations for functions

不写注释也不行

image-20240428142025031

Lab4

重写sys_get_time和sys_task_info

先看看原本的sys_get_time是如何实现的?

对指针*ts所指向的内存空间赋值时间信息,在引入虚存之前应用空间和内核空间之间不存在隔离,二者都可以直接访问到*ts所在位置。在引入虚存之后,每个应用以及内核本身都有独立的地址空间,没办法访问了。

因此我们需要想办法,使得OS能够访问到应用所在的位置,需要完成二者地址的翻译。

……

和mmap那个题目一样,sys_get_time以及sys_task_info也是需要在当前任务下才行

为什么突然写回来了呢……(因为在线CI测了sys_get_time还有sys_task_info 发现自己写的根本就不对

我知道哪里不行了 我的实现没问题 搞得我还重写了一次

每次执行系统调用的时候忘记调用add_syscall_num

mmap

insert_frame_area函数是比较值得参考的一个函数

不仅仅是函数实现的功能类似,用法也很值得学习

……

随后就是漫长的调试……

这里注意到特殊的一行

[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.

先找到这行输出是哪里来的,发现在Trap_handler方法里面

然后再考虑,发现其实我的程序连mmap0都没正常跑完

但是正确分配了页面,要不然就不会有start_va:0x10000000~end_va:0x10001000 map_perm:0x16输出

这里的0x16完全没问题(之前看成10进制了)

0001 0110 代表U,W,R被置位 而测试用例mmap0给的是3 也就是011 也是对应W,R

image-20240518222104567

妈的 我知道怎么搞了 之所以会不断出现The Page you wanted has been alloced to others的报错信息是因为之前在sys_mmap方法中对MemorySet中mmap()的调用是这样子的

1
2
3
// 获取内核实例 取得所有权完成分配(这样不对 你并没有找到实际你要分配的位置) 实际上你是给内核多次分配了 所以才这样子报错
let num = KERNEL_SPACE.exclusive_access().mmap(start, len, port);
//之所以这样子写主要还是因为参考了前面insert_frame_area的用法 没有具体考虑他使用的上下文

事实上应该找到当前运行的任务,只有当前运行的任务是知道自己的地址空间信息的,具体在TCB里面有一项memory_set

这里也走了点弯路 一开始我的想法是在TaskControlBlock中实现一个get_current_tasks_area(类似于之前get_tasks_start_time一样拿到时间)拿到memory_set的所有权或者引用之后,在sys_mmap()里面再用得到的memory_set来调用(我个人感觉主要还是仿照了前面代码的思路,就非要拿到一个类似于KERNEL_SPACE的地址空间,事实上没必要)

……

实现了mmap之后munmap就比较简单了

这里写一个点 关于munmap的最后一个测试用例

下面给一个比较滑头的办法 检查一下是不是页对齐就行(start硬编码写死了 所以其实你怎么写都差不多

1
2
3
4
5
6
7
8
9
// YOUR JOB: Implement munmap.
pub fn sys_munmap(start: usize, len: usize) -> isize {
trace!("kernel: sys_munmap NOT IMPLEMENTED YET!");
if start % PAGE_SIZE != 0{
return -1;
}
let num = munmap_current_task(start, len);
num
}

按理说应该是实现一个检测解除映射范围和现有的映射区域是否完全一致的方法

明天再想吧……先看看能不能过在线CI

懂了 在线CI看不到报错 我就说为什么lab4的测例全过了assert断言还是不行

image-20240505014117148

这里可以看到比较详细的信息

这里记录一下回退的点 本地执行CI之后需要删除一些未跟踪的文件

1
2
git clean -f  删除未跟踪的文件(不包括目录)
git clean -fd 删除未跟踪的文件和目录

image-20240505171002449

解决了 太sb了

Lab5

之前的测例实现过程

经典内容……

注意一个点 在Lab5里面把TaskManager拆分成了TaskManager和processor两个数据结构

不过他们对进程信息的获取还是通过TaskControlBlock

关于初始化信息补全

忘记记录了……之前的测例实现基本就是cv,注意放到正确的数据结构里面重新实现一次就行

遇到一个新问题

image-20240506003949090

image-20240506004011708

这个Write系统调用莫名其妙多出这么多次数

我决定在增加系统调用的方法中加一行调试,打印一下系统调用编号

1
2
3
4
5
6
/// 添加系统调用
pub fn add_current_syscall_times(&mut self,syscall_id:usize){
let mut current_inner = self.current.as_mut().unwrap().inner_exclusive_access();
current_inner.syscall_times[syscall_id] += 1;
println!("{} + 1\n",syscall_id);
}

image-20240506010214962

出现了奇怪的输出

每次键入一个字符 对应着read waitpid yield write系统调用都+1了

虽然输出流打断了我的输入流 但是功能应该还是正常的 只是我没有键入回车键 所以用户程序没有被正常执行起来

应该是前面的实现有问题(

在父进程通过fork()系统调用创建子进程的时候,子进程不应该继承父进程的系统调用次数和开始时间

系统调用次数应该直接初始化为0才对(重新算

1
2
start_time:get_time_us() / 1000,
syscall_times:[0;MAX_SYSCALL_NUM]

ok 这里可以过了

现在又有新问题了 还是ch3_taskinfo的测例

image-20240506011435160

好像还是过不了 但是断言错误的次数确实是减少了……

好像是用println!()打印调试信息的问题,如果去掉的话Write系统调用的次数就不会增加

image-20240506013300058

还真是 回头看了一下console.rs里面对println!()的实现 很明显是基于Write

不然平白无故你的OS怎么能打印东西的……把这事情给忘记了

先一次性把时间信息都打印下来吧 后面就不看了

image-20240506013635316

我切换分支到ch4重新跑一下这个用例 t1=43 t2=544 t3=544 info.time=501是没有问题的

睡了 明天再说

……睡觉的时候突然想到Lab5的run_tasks()方法应该是没有修改 所以没有把时间信息记录下来

没错 就是在此处补一个记录时间的功能就ok了

Spawn系统调用实现

一遍过 感觉还是比较简单的…… 主要就是fork() new() 还有exec()的仿写

注意对parent字段特殊处理

spawn出来的进程的父进程应该为当前运行的进程

Stride调度

首先是在TCB里面增加进程优先级的字段priority和步长调度的参考数据stride

其实是对初始化信息的补全

一开始我是看到的processor.rsrun_tasks()模块,里面有一个fetch_tasks()的过程,取得目前应该运行的任务。但是fetch_tasks()是局限在Task Manager内部,缺少Processor结构,也就是当下的任务状态拿不到(就是有一种可能性是当下正在运行的进程stride还是最小)

然后继续看源码

感觉这几个函数之间的关系有点懵

……

后面感觉还是得在add位置实现,也就是fetch还是从队头把进程取出来,但是在add增加进程的时候维护所有进程的stride顺序

Lab6

之前的测例

首先就是要通过之前的测例 sys_spawn和之前会有一些区别

主要就是获得程序数据的方式有差异

本实验测试点

image-20240510225304112

差最后一个

???

byd什么勾八

怎么实验还不能复现的 本地跑差一个点 在线ci全过了是吧

image-20240510225444383

Lab8

如果启用死锁检测功能的话,主要的检测就是在上锁相关的操作检测是否合法

这里有个困惑的点,就是一开始没搞懂资源到底是什么

其实就是各类的lock……能够得到锁 就代表得到了某个特定的资源

初始化是一门玄学……

需要自己设置好两个常量MAX_THREADSMAX_RESOURES的数量,代表当前可以获得的资源

……

一些问题

image-20240516200029337

就卡在这里了,也不知道怎么回事

gpt问了一下

……死锁了 我就说为什么寄了