0%

整体评价

我参与 rCore 训练营,是为了较深地学习、掌握 os 知识。此前,我学习了 NJU ics pa,对 riscv isa 即配套的简易版的 os 层有一定的理解,这使我能非常高效地理解 rCore 的文档内容。

我个人是比较推荐有点基础的人去阅读、完成 rCore 的。若对计算机组成原理或基本的操作系统知识不了解的话可能无法读懂前两章,导致无法理解内核代码,从而无法很好地理解后面章节的逻辑。有一定的基础之后,rCore 就会从一个比较低的起点开始慢慢构建一个功能强大的 os,稍微花点时间就能看懂全部的代码,而不存在“庞大而不可测的代码块”。此外,Rust 的 no_std 环境也特别友好,很多实用的东西(如 format、RefCell)都会提供;而且也提供了动态分配内存的需求接口,实现之后就能非常方便地使用各个智能指针和容器了,大大简化了 os 中细微逻辑的实现难度。

Rust 编译器的强大也大大改变了实验练习的“画风”,它要求我们必须在很好地理解 API 后,自己使用 unsafe 代码并保证接口是 safe 的,然后要想办法把代码组织起来,不让编译器报错。也就是说,Rust 的特性使得我们不得不深入地理解实验代码、需求才能完成练习。只要编译不报错,就离完成不远了,而且此时也往往说明我们并没有误用已有的代码。

rCore 之旅

前几章最有意思的是各个链接脚本。较好地组织链接脚本,才能不浪费空间同时正确地启动 os,而想要理解后面的章节的许多需求(如加载 app)也需要先较好地理解 boot 过程。

app 都是通过构建脚本自动生成链接脚本来加载到内核中的,这非常有意思,我称之为“交叉构建”(化用“交叉编译”)——要清晰地认识到,内核代码和用户代码是完全分离的,rCore 中依靠 user 来编译出可运行在内核上的代码,pa 则是使用 navy-app。这打破了我们平时思考的习惯。我们能在电脑上写代码、跑起来,其实是因为 windows 有能在自己上面跑的面向 windows 的编译器,它可能是自举的,也可能是用其他 os 交叉编译了一部分的。若是没写好这样的自给自足的编译器,就只能向 rCore 一样通过元编程来加载。

关于特权级切换时的上下文保护,我发起了一个讨论

第三章引入时间片相关逻辑最大的意义是引入了中断,使得 os 的状态切换更复杂,要求更好地处理上下文关系。而第三章里有 lab1,使得我更加理解了 task 的数据结构构造(此时的 task 都被较简单地、笼统地管理,每个 task 并不能很好地自己管理自己,这在第五章(lab3)得到了改善)。

第四章,设计者让每个 task 都维护一个地址空间和一个页表,两者甚至是解耦的,使用一个地址空间时要显式指定究竟使用哪个页表,这大大增强了系统的可拓展性。而物理页帧分配器的抽象也十分令人惊叹,它是让丑陋的物理地址变得简单易用的第一步。此外,我觉得此章对跳板的解释并不是特别具体,因此我也发起了一个讨论

第六章引入进程后的数据结构设计十分精妙,而配套的 lab 也“逼迫”我去理解这种设计,希望同学们也能亲自体会到。另外,我也学到了这一非常巧妙的 Rust unsafe 代码封装技巧:当原始指针作为结果时,可以将其转化为&'static mut,这样就不需要 unsafe 也能使用该数据:

1
2
3
4
5
6
7
8

fn get_current_mem_set(&self) -> &'static mut MemorySet {

    let ptr = &mut result as *mut MemorySet;

    unsafe{&mut *ptr}

}

之后的章节我也在逐步学习,to be continued…

关于开源操作系统训练营

我是在一年前关注到“开源操作系统训练营”这个活动的,但是苦于那时在进行本科毕业设计工作,没有抽出时间来参加。在那时,我就已经对系统软件、 Rust很感兴趣了,因此本科毕业设计做的也是 OS 相关的工作。如今在研一,课业之外有了一部分时间,想用来提升自身对 OS 前沿的认识,同时积累一些工程经验,于是便参加了今年的开源 OS 训练营。

关于 Rust

Rust 是一门比较新的系统级编程语言,我最早听说 Rust 是在一次高中同学聚会上。那时候,我的一位高中同学所在的大学(不是清华)已经使用 Rust 来布置一些课程的大作业,而我所在的大学还没有涉及 Rust 的课程。

我对 Rust 最深刻的印象是“内存安全”和“系统级编程语言”,但之后的很长一段时间里,我都没有去更深入地了解 Rust 了,原因很简单:没有时间、对课程也没什么帮助。

后来,做毕业设计的时候,导师偶尔会提到一些清华大学陈渝老师组的一些 OS 方面的前沿研究工作,比如押宝 Rust、组件化 OS 之类的。因此,我对 Rust 的认识也在发生转变。实际上,OS 发展中一个重要的影响因素就是系统级编程语言,而 Rust 作为一门比较新的系统级编程语言,未来势必影响 OS 的发展。

经过了两个阶段的学习,我对 Rust 的特点有了更深入的认识:Rust 解决的内存安全问题的方法是将相当一部分内存安全检查放到了编译时,同时又提供了 unsafe 块赋予内核设计者操作底层硬件的能力。换言之,我们只需要保证将尽可能少的代码放在 unsafe 块,并保证这一小部分代码是内存安全的,那么经过 rustc 编译后的内核就是内存安全的。

关于 rCore

第一次与 rCore 结缘是在第七届“龙芯杯”大赛中,只不过那时遇到的是 rCore 的前身 uCore。

rCore 是 uCore 的 Rust 重构版本,而 uCore 参考了 XV6 的实现。从指令架构上来看,XV6 支持的是 x86,一种广泛使用的 CISC,但由于其闭源性、复杂性已经逐渐被教学所抛弃。uCore、rCore 面向的是以 RISCV 为代表的 RISC。RISC 最早提出是为了解决 CISC 指令间相关性难以判断的问题,两条 RISC 指令之间很容易判断相关性,从而实现流水线的微架构设计。

此次参加开源 OS 训练营,是我第一次学习 rCore。在学习过程中,我体会到了 rCore 是如何利用 Rust 语言的机制(特点)进行编写的。接下来我将举例总结。

cargo:Rust 的包管理、构建工具

相比于 C 语言等传统的系统级编程语言,Rust 提供了包管理和构建工具 cargo,好处是:

  1. 可以方便地引入一些库,为编写内核提供便利,无需重复造论子

    比如说在 ch3 中出现了一个 heap_alloc.rs 文件,他在里面创建了一个静态的 HEAP_ALLOCATOR,并使用 #[global_allocator] 进行了标记。后来在 ch4 的文档里我才理解它是用来进行(物理)堆内存分配的分配器,这样就能在内核里使用堆上数据了。而该分配器的具体实现则无需我们操心了,因为它是由一个名为 buddy_system_allocator 的 crate 提供的(结果上 crates.io 一查,是由 rcore-os 组织维护的一个库)。

  2. 构建过程更加简单
    理论上来说,构建一个 Rust 项目只需要 cargo build 就够了,但 rCore 实验还有其他需求,因此框架里使用了 Makefile。

所有权、生命周期

rCore 的内存管理系统巧妙地利用了 Rust 的所有权和声明周期机制。当申请了一个物理页时,分配函数 frame_alloc() 直接将物理页描述符 FrameTracker 的所有权返回,而这些页描述符的所有权直接被申请物理页的变量所拥有(如 PageTableMapArea)。当这些变量的声明周期结束时,他们所持有的 FrameTracker 的生命周期也将结束,而 rCore 为 FrameTracker 实现了 Drop trait,将在它们生命周期结束时“自动”回收物理页。

这是我第二次参加这个训练营了。

上一次参加夏令营的总结:
https://rcore-os.cn/blog/2021/09/10/2021%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%A4%8F%E4%BB%A4%E8%90%A5%E6%80%BB%E7%BB%93-%E9%BB%84%E6%96%87%E7%A6%B9/

因为有过参加一次这个训练营的经历,所以第一阶段和第二阶段对我难度不算太大。第一阶段在前两周抽空完成了。第二阶段重新看了 rCore-Tutorial 又有了些不一样的收获。整体来说前两阶段相较于之前更加完善了,比如有完善的评测机制,可以来判断自己是否实现正确。

学习记录:https://github.com/uran0sH/2023a-os-comp-note

因为对 rCore-Tutorial 还有些印象,所以 ch1 ch2 ch3 都很快的复习了一遍。ch3 的实验内容倒是于之前不同,不过难度也不算很大,很快就有了思路并且将它完成了。ch4 是我这次阶段主要学习的内容,因为之前学习的时候对这块内容有点一知半解的:尤其是开启分页后,内核是如何去管理物理页的(因为开启后内核是不能直接访问到物理地址的)。所以这次重点的学习了一下,并且做了笔记。mmap 和 munmap 实验和之前的基本相同,不过这次需要适配 sys_get_time 和 sys_task_info 系统调用。到了 ch5,还是实现一个 spawn,但是我上一次训练营实现的 spawn 感觉有点问题,这次重新实现了一遍。stride 调度算法对这次的代码进行了适配调整。ch6 实现链接和 fstat,因为对 Linux 这块实现原理比较了解,很快就知道如何去实现了。

对第三阶段的期望:之前第三阶段是做了一些文档的工作,这次想参与一些代码的开发。

阶段一 rustlings

因为之前有一点 Rust 基础,直接做 Rustlings 没有太大问题,加上当时比较忙着工作和找租房的事,所以阶段一比较潦草,把题目做完就没有再花时间深究其中一些不熟悉的部分。总的来说,Rustings 确实是非常好的入门学习资料,重做一遍也相当于复习了。

阶段二 rCore

阶段二最开始看2023A的文档,但是感觉讲得有点模糊,后来读到详细的V3文档感觉一下子通顺了很多。虽然要读的内容多一点,但实际可能因为理解更清晰而做的更快一些。

前三章主要讲操作系统内核是如何启动的,以及启动后如何运行用户程序的。其实一切都没有魔法,无非是如何合理运用硬件规则而已。一般来说是硬件决定了第一条指令的起始位置,硬件决定如何输入输出,硬件也提供了分等级的环境让我们可以隔离用户态和内核态…总之,程序的运行最终都是落实在硬件之上的,因此操作系统需要根据硬件功能做出相应的适配。而为了简化这种适配的复杂度,又引出了位于硬件和内核之间的一层抽象,称为SBI。我们可以把SBI简单想象成”内核的操作系统”,通过SBI简化后的接口可以调用硬件某些复杂的功能,比如输入输出。在了解了裸机的基本运行环境后,再看内核其实和我们日常编写的应用软件是非常类似的,只是在某些时候,往往需要使用汇编语言来构造一些更底层的上下文或完成指令跳转。在熟悉这些技巧后,前三章内核的执行过程就比较好懂了。

第四章引入了地址空间的概念,这里再次体现硬件对内核的支持,硬件可以通过设置来开启MMU功能,直接实现了地址转换功能,甚至还提供 TLB 用作缓存。因此内核只需要考虑对内存的管理功能,而不需要在用户程序运行中转换每一个虚拟地址。在rCore的学习中,比较容易迷糊的是到底哪些空间包含了哪些部分,可以通过打印日志的访问查看堆栈的起止位置,感受程序的内存布局。

第五章引入进程的概念,其实更像是第三章的加强版。理解程序执行需要准备哪些上下文,申请哪些资源,程序进入/退出内核需要如何保存/恢复上下文,之后再做实验就不会迷茫。有点被卡住的是一开始不太理解 idle_task_cx 的作用,仔细阅读 __switch 函数才发现 idle_task_cx 保存了run_tasks() 里循环的上下文。阅读汇编代码实际含义确实不如普通代码好懂,这可能也是操作系统的难点之一吧。

第六章引入文件的实现,由于不需要关注硬件细节,最底层只需要调用实现了 BlockDevice trait的块设备结构体即可,总体比较好懂。且rCore是运行在单核处理器上,不需要考虑文件的共享问题,整体难度不大。

第七章进程间通信也主要是在目前内核至少实现一些方便的功能,并没有新硬件的引入。

第八章线程的引入更加细化了之前对进程的管理,而一些并发原语则是利用了硬件的原子指令的功能,才让一些共享变量能被安全访问。但是rCore没有引入多核,因此也比较好懂。

总体来说,通过这次rCore的学习,对操作系统内核的基本原理有了更直观的感受,一切都是硬件的支持和指令的跳转,同时也体会到Rust是如何应用在系统软件领域的,唯一稍有遗憾的是rCore没有引入多核,毕竟日常生活工作都是使用多核CPU。但瑕不掩瑜,rCore仍是非常好的操作系统入门资料,V3文档也非常详实,在此衷心感谢各位老师和同学的付出。

目前虽然完成了规定的几个实验,但深知在框架内添加代码和自己独立实现一个迷你操作系统仍有不小的差距,还需要多多学习。

#rcore学习总结报告

总述

两周学完rust,两周了解rcore操作系统对于我这个大一的学生来说过程十分艰辛,虽说没有通宵打代码,但确确实实是花了几乎所有非学习睡觉时间完成的。
这里非常非常感谢举办这个训练营的所有老师和负责人,资料准备的很全,几乎不需要再找其他资料;热心负责,经常在群里答疑,课上讲得很精炼。

lab1
在这个实验中我实现了一个TaskInfo函数,该函数可以查询某个Task的运行时间、写入次数、挂起次数、退出次数和查询info次数。
fn sys_task_info(ti: *mut TaskInfo) -> isize
我通过在TaskInner结构体中加入积累响应信息的值,并在调用这些系统函数的时候维护这些值。
ps:一开始真的没想到还可以修改源代码部分,以为只是完成函数,哭

lab2
在这个实验中我重新写了sys_get_time 和 sys_task_info函数,因为这两个函数传入的虚拟地址可能被分在了不同的页
同时我实现了mmap 和 munmap两个函数,分别让用户可以申请虚拟地址空间和取消申请虚拟空间
我在重写sys_get_time 和 sys_task_info函数的时候受讲课老师引导,学习详细白皮书的sys_write的写法,知道了如何获取到用户的实际存储内存地址
在实现mmap和munmap函数中,我先判断用户输入是否合法,然后把用户给的地址取整,按每个页申请和取消空间。
ps:用户空间由TaskInner中memory_set变量控制,不能自己新建memoryset变量!!!

lab3
在这个实验中这次我实现了spawn函数的调用,它能新建一个子进程,并执行该子进程
我参考了给出的fork函数和exec函数的实现过程,在把两者结合,实现利用fork新建子进程,再利用类exec函数启动子进程
ps:fork()+exec()≠spawn()

训练营学习记录

这篇文章用来记录我在2023秋冬季开源操作系统训练营的学习过程,之所以会参加本次训练营,是因为我想进一步学习操作系统以及Rust编程语言。

第一阶段(1-2周)

本阶段的主要目的是掌握Rust编程语言,为后续学习打下基础。

第一周

在参加训练营之前,我有学习过Rust,但之后并没有做过有关Rust的项目,只用Rust刷过题,以至于之前学过的知识很快就忘了,所以我并没有马上开始做训练营的题。

本周我将环境配置好后就开始重新过一遍Rust,主要重学了Rust的所有权、借用、泛型、特征、迭代器、生命周期、智能指针、多线程、迭代器。

第二周

本周我开始完成训练营的题,题目不难,但很有针对性,基本上就是各个章节的知识点,这个过程中也学到了一些重要的知识,比如build.rs、#[no_mangle],为后续学习rCore打下基础

第二阶段(3-4周)

本阶段的学习目标是完成rCore-Tutorial中的五个实验。相比于上个阶段,本阶段的学习任务要重得多,学习难度也更大,需要花更多的时间。

第三周

我首先花了一天时间配置环境,我使用的是Docker,项目中有写好的Dockerfile能直接构建镜像,可惜由于网络问题,我总是构建失败(apt-get 下着下着就卡住了,导致无法构建成功),最后只能新建了一个Ubuntu:20.04的镜像,然后在容器中按照Dockerfile的流程将容器的环境配置好,然后提交容器保存为镜像。

配置完环境后我就开始看 rCore-Tutorial 并完成实验

rCore-Tutorial 的第一章从一个应用层的Hello World开始,一点点去掉需要操作系统支持的部分,最后构建一个能在裸机上打印Hello World的程序。

第二章构建了一个批处理系统,能够依次运行导入的程序。由于没有实现文件系统,从第二章到第五章的用户程序都是同操作系统放在一起的,通过脚本生成的汇编代码来标识应用程序的起始地址和结束地址。
这一章讲到了Risc-V的特权级切换,比较重要的就是Trap的上下文切换过程。

第三章通过时钟中断实现了分时多任务系统,比较重要的就是switch过程,切换任务时我们需要保存任务的上下文,以便下次运行时恢复。这与Trap过程是不同的,Trap是用户态与内核态之间的上下文切换,而switch是不同任务在内核态的上下文切换。

第四章实现了Risc-V的三级分页机制,这里采用双页表的实现方式,即一个任务的用户态和内核态使用不同的页表,这使得我们在Trap时还需要完成页表的切换。为了使得页表切换时程序能按正常的顺序执行,内核页表和用户页表都必须映射一段相同的地址空间,这段空间保存的代码就是Trap过程的跳板。

第五章实现了进程管理,调整了一些数据结构,将进程的执行交给各个CPU(不过这里只有一个CPU),将当前运行的进程id交给Processor结构(它是处理器的抽象),然后将进程的调度交给TaskManager。为了支持在用户态输入命令,还实现了一个简单的shell程序以及sys_read系统调用。新建进程则由sys_fork和sys_exec完成。

第四周

待续。。。。

感谢老师和助教们贡献了这么好的课程,可以看到付出的努力和心血,所以我也非常重视这一次的训练营。通过这次的训练营,不仅让我在短时间内快速学习使用了Rust,并乘热打铁学习实践了我一直想掌握的OS知识。在阅读文档的时候详略有当,也给出了很多扩展性的知识和难题,不过个人水平太菜并没有深入。只经过短短两周多的学习,已经基本能在脑海中对操作系统有一个较为清晰的概念了。

Read more »

2023rCore训练营第二阶段总结

起因

因为不满足于学校的课程教学,想写一些稍微硬核一些的作业,在2023年5月左右的时候在国内外各大操作系统公开课中挑选了一番,最终是选择了rCore,大概有下面这些选择的理由。

  • 使用rust语言,对于当时刚学了一点rust感受到其优越性的我很有吸引力。
  • RISC-V架构,能减少一些x64的繁琐带来的困扰。
  • 支持国内老师对于本科课程的改革。

在暑假期间自学了一番并在秋冬季的训练营中靠着多学了两个月的优势在只有两周时间学习的同学们中获得了第一名))

下面简单来说一下rCore的各个实验。

ch3

ch3的任务是实现获取taskinfo的syscall,总体来说还是比较简单的。
但是实现过程中遇到了一些问题,程序运行到最后卡住了无法结束,稍微有点棘手。(时间久远没有截图)
最后发现是由于内存出现了问题,直接使用桶计数的话内存会炸掉。
解决方法稍微有些投机取巧,由于ch3只有5个系统调用就分别映射到一个数组的0,1,2,3,4就好了

1
2
3
4
5
6
7
8
match syscall_id {
64 => curr_task_tcb.syscall_times[0] += 1,
93 => curr_task_tcb.syscall_times[1] += 1,
124 => curr_task_tcb.syscall_times[2] += 1,
169 => curr_task_tcb.syscall_times[3] += 1,
410 => curr_task_tcb.syscall_times[4] += 1,
_ => {}
}

不过秋冬季看起来没有人和我遇到一样的问题,我自己重新再运行也没有什么问题,不知道当时是怎么回事。

ch4

ch4也稍微有些投机取巧了,我单独在MemorySet中添加了一个字段用来实现mmap,像是这样:

1
2
3
4
5
pub struct MemorySet {
page_table: PageTable,
areas: Vec<MapArea>,
mmap_frames: BTreeMap<VirtPageNum, FrameTracker>,
}

阅读代码理解SV39稍稍费了一些功夫,不过收获还是挺大的。
还有重写sys_task_infosys_get_time,选择自己实现了一个函数来进行到物理地址的转换

1
pub fn translate_ptr(token: usize, ptr: *const T) -> *mut T

图简单没有使用官方提供的translated_byte_buffer

ch5

ch5感觉上还是比较简单的,好像测试用例也不是很强的样子,听说有人没有实现都能过,不过应该会在线上测试被拦下来吧。

ch6

感觉按部就班地调用框架提供的函数就行,不算很难。

ch8

据选择做xv6的小伙伴说这是和xv6差别最大的而一个实验,确实也让我稍微费了一番功夫。
回头去看自己写的代码真是又臭又长啊……….
ch8的作业是实现银行家算法,在lock和unlock之前判断是否会造成死锁。
值得一提的点大概就是sys_semaphore_create函数只会在主线程中调用,因为银行家算法需要提前获知线程总的资源申请量,我的实现里面单独处理了线程id为0的情况,其他的还是比较简单的,就是几个向量加加减减的问题。

调试

使用gdb调试貌似很麻烦,源代码貌似不能很好地同步显示,不过也不怎么影响做完,用打log的方式解决了。
但是这让我的代码里面多了很多类似这样的东西:

1
2
3
4
5
6
7
8
9
10
11
debug!(
"kernel:pid[{}] tid[{}] sys_semaphore_create",
current_task().unwrap().process.upgrade().unwrap().getpid(),
current_task()
.unwrap()
.inner_exclusive_access()
.res
.as_ref()
.unwrap()
.tid
);

显得很臃肿。不过能解决问题就好。

总体感受

rCore教程的叙事方式很有意思,从历史的角度来添加一个一个模块,看书的同时阅读源代码好像自己真的是当时的操作系统开发者,面对着一个一个的问题,提出各种解决方案。难度的曲线也比较平缓,作为一本操作系统入门的教材来说是非常非常优秀的。
缺点大概就是有点过于简单了(bushi)。在看ch5的时候还试着去看了一下rcore中关于多核的部分不过因为资料太少也搁置了(摸了)。
rcore的竞品(不知道这个词合适不合适),chcore有关于多核的部分,如果有同学看到这里并对多核感兴趣可以去看看。(虽然我摸了)。
感谢清华的老师和助教为中国计算机本科教学做出的贡献。国内外的计算机本科教育差距很大不过仍有老师和同学在努力弥补着。感谢他们做出的努力。(什么时候我们学校能引进啊)