0%

首先感谢各位老师和学长能够创造出让大家接触并学习实战编写操作系统的机会,这在国内的大环境下是非常难得的,感谢你们的无私奉献。

第一阶段我们从 Rust 基础语法开始学起,在此阶段我巩固了我的 Rust 基础,理解了使用 Rust 语言作为编写操作系统的语言的优势。

相比于传统的 C 语言,Rust 的语法更加现代化,这对于一个 Java/Kotlin 人非常友好,可以方便地用到现代编程语言的技术,如闭包,迭代器,流式 API 等。而且 Rust 语言也有了出了名的安全性,几乎完美杜绝了垂悬引用和二次释放等危险操作。

但是,奇迹和魔法不是免费的。同样地,Rust 的语法和编码规则也更为复杂,我们编码必须完全遵循 Rust 所谓的借用和生命周期规则,而且也出现了许多新概念,这也是 Rust 比较劝退的地方。但是如果能坚持下来,我们将能享受到 Rust 语言带给我们的便利,享受编写高安全性代码的快感。

第二阶直接进入操作系统的学习,这还是第一次实际上手实现操作系统的内核功能,这注定是一个漫长的学习过程。虽然说是使用 Rust 编写操作系统,但接触了底层,难免会和汇编打交道。

首先要理解文档中的汇编代码才能做好基础准备,虽然在学校课堂上学习了特权级,CPU 中断这些知识,但是把他们体现在代码上还是有些难度。特别是切换特权级别这一过程,他们都直接修改了 pc 寄存器,直接跳转执行地址,我们不能用常规编程的程序流控制思路来理解。而 CPU 级别的直接跳转在 Rust 代码中的体现就是无条件地从这个流跳转到另一个流执行了,对于第一次接触这类操作的我产生了极大的震撼。

在理解了这个之后,接下来就是逐步实现操作系统应有的功能:进程调度,虚拟内存空间,文件系统,并发操作,IPC 以及 IO 设备管理。目前二阶段的实验主要还是基于 RISC-V 架构实现的,没有做到架构无关,而且这些功能也是直接嵌入到内核中,没有做成单独的模块。

在实现这些的过程中,我按照教程文档逐一理解代码,然后再将一些调试信息打印出来,直观感受一个操作系统底层运行起来的样子,然后逐步构建起一个庞大的系统架构,每一个模块都精密运作,这是设计师的浪漫。

这次 rCore 学习让我充分学习了操作系统的底层逻辑,也领会到了 Rust 语言的强大,希望后续可以更进一步学习。

逆境中的学习与成长:我的开源操作系统训练营之旅

自我介绍

今年的经济形势确实不容乐观。没错,我就是那位大龄失业的程序员。在寻找工作的同时,我尝试将自己的经验和心得整理成文章,发布在名为《猿禹宙》的微信公众号上,希望能为广大读者提供一些帮助。如果这些文章对您有所启发,请您不吝啬地关注、转发、点个赞、赏 1 元以示支持。广告部分到此结束,接下来我要分享一次偶然的相遇。

微信公众号

Read more »

  • 在这学习的一个月里,我投入了大量时间和精力来学习Rust编程语言以及操作系统开发。我的目标是掌握这门高效、安全的语言,并借此机会深入了解操作系统的底层原理和实现。

第一阶段 Rust编程语言

  • Rust语言基础:我通过阅读官方文档和相关书籍,进一步掌握了Rust的语言特性,包括内存管理、所有权系统、生命周期等。我还通过编写一些小的程序来巩固这些知识,比如实现简单的数据结构和算法。
  • 在学习过程中,我也遇到了一些挑战。比如,Rust的所有权系统和生命周期机制让我感到有些困惑。通过阅读更多的文档和示例代码,逐渐好像克服了这些困难。

第二阶段 操作系统实现

  • 操作系统原理:为了更好地理解操作系统开发,我深入学习了操作系统的基本原理,例如内核启动过程、地址空间、内存管理、进程管理、文件系统等。我通过阅读经典的操作系统教材(rcore book)和通过实践加深对这部分内容的理解。
  • 在整个学习过程,以动手实践为主,用最简单的方式重新实践一遍,理解整个过程,细节,原理

    实践1

  • 按照教程动手实现最简单的系统,输出一行文字,目标是熟悉整个开发环境,编译,打包,启动环境,调试方式

    实践2

  • 熟悉一些rustsbi调用,riscv汇编指令,特权模式,完善一些日志输出,系统调用方式,了解内核各个段的地址,用途

    实践3

  • 实现系统调用处理,时钟中断处理,s->u及u->s的切换过程,栈的切换,任务的运行环境的保存及恢复,任务进程的调度切换

    实践4

  • 实现以sv39模式的地址空间管理,页表的管理,虚拟地址到物理地址的转换,在这个实践上花费时间比较多,特别是异常处理时入口地址,返回地址,任务空间的页表地址切换,及程序各个段的映射,按照rcore的实现trap_context是放在应用层地址空间的,基本上可以随意修改,不够安全,准备尝试把trap_context放到内核地址空间

    实践5

  • 实现了解进程信号的发送接收过程,在内核层触发应用层的回调函数,实际上是一次任务的切换,必须在当前进程调用系统调用后才能触发,了解各种锁通过任务调度的实现,进程间的通信,管道消息的发送接收

    实践6

  • 实现通过对接调用easy-fs对文件的读写,块设备驱动采用的是virtio接口,这块比较复杂,待进一步理解

    实践7

  • 尝试启动双核,对任务进行调度,主要涉及到公共资源的加锁,当前任务的结构指针存储到tp寄存器(x4),获取当前进程通过读取tp寄存器实现,trap_context和task_context加入对tp寄存器的保存和加载(针对系统调用和任务调度)

    总结

  • 回顾这个学习实践过程,我认为我对操作系统原理上的理解取得了很大的进步。但我也意识到,还需要不断的继续学习和实践。
  • 我希望能够继续深入学习Rust和操作系统开发,参与更多的开源项目,提升我的技能和经验。同时,我也希望能够将我在学习过程中积累的知识和经验分享给更多的人,帮助更多的人一起进步。
  • 总的来说,这个学习过程充满了挑战,但我收获了丰富的知识和技能。我对后面的学习充满了期待。

感谢老师和助教提供的帮助!!!

整体评价

我参与 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 »