开源操作系统训练营第一阶段总结
学习
- 学习了rustlings的课程,并完成了所有练习。
心得
- 学习之前主要还是需要看完官网推荐的书籍,然后再去学习和练习。
- 练习过程中,可以边做边学,遇到问题可以及时查阅文档和群友进行讨论,避免陷入牛角尖。
- 借助ai的帮助,可以更快的学习和掌握知识。
收获
- 通过rustlings的练习,对rust的熟悉程度有了更深入的了解。
- 了解使用rust来实现一些数据结构和算法。
这个阶段主要是要完成几个实验,借助几个实验理解一个具有进程/线程管理、内存管理、文件系统、进程间通信和提供了一定同步机制的内核是如何构成并运作起来的。
比较深刻的有这么几个知识点:
Rust
的汇编嵌入由于我们的内核使用了分离内核空间的设置,所以在Trap
的时候需要切换页表。但在切换页表之后,pc
寄存器还是忠实的在其原来的位置自加到下一条指令,如果内核内存空间
和程序内存空间
对这段代码的映射不是在同一个位置的话,则会表现出来程序跳转到了别的地方执行的效果。因此我们设计了一个跳板页,在虚存中映射到所有内存空间的最高页,确保在切换之后,也能正确运行下一条指令。
1 | # trap.S |
在上面的汇编可以看到,我们给trap.S
分配到了.text.trampoline
段,并在链接脚本中定义了一个strampline
符号来标记他的位置,这样我们可以在Rust
中找到这个跳板页,映射到我们期望的位置。
但将跳板也映射到别的地方带来了新的问题,原来__alltraps
中最后跳转到trap_handler
使用的是call trap_handler
。我们可以通过obj-dump
看看编译得到的指令。
1 | # obj-dump -Dx ... |
可以看到,这里用的是pc相对寻址,也就是基于当前指令的偏移找到trap_handler
所在的位置。但是现在__alltraps
已经被我们映射到内存的最高页去了,也就是说我们实际运行代码的时候是在下面这一段内存中。
1 | # gdb |
很明显如果这里跳转到$pc+offset$的话,并不是跳到位于正常代码段的trap_handler
。所以我们要将这里换成寄存器跳转,将trap_handler
的地址放到寄存器t1
中,这样才能顺利地调用到trap_handler
。
在开始正式报告之前,我想说作为一位三战老兵,三次参加开源操作系统训练营,从一开始的只能勉强完成rustlings,再到现在完全实现第二阶段五个lab的所有功能。一路走来,走到今天实属不易。遥想一年前的我,因为二进制漏洞挖掘与利用(pwn)接触到操作系统,并励志于弄清操作系统的运行原理,但是苦于实在没有学习路径而踌躇不前,是开源操作系统训练营给予我接下来奋斗努力的方向。在此也非常感谢这个项目。
另外这一路上结识的,给予我帮助的伙伴,我也非常的感激并庆幸认识了你们。尤其是23年春夏季的助教徐堃元老师和朱懿老师,没有你们的帮助我可能还卡在一些看上去非常令人费解而又苦恼的地方,你们在rcore与系统能力赛上的指导对我的启发很大,在这里向你们表示感谢。
我身边的朋友曾问我,为什么要做操作系统?操作系统在外人看来晦涩难懂,且在实际业务场景中似乎并不需要我们去细究它的实现与原理。每到这种时候,我就会想到登月前肯尼迪总统所做的讲话:We choose to go to the moon, not because it’s easy, but because it’s hard.
做难而正确的事情,至少目前为止,我都是这样干的。至于前程?但行好事,莫问前程。在我还有精力干的年纪,稍微干点事情吧。
2024.11.7 stone-san于寝室
第一阶段就是rustlings的通关。一开始我对rust语言一无所知,且光看一些书籍的话,学习的效果也不佳,所以使用rustlings来实操是有助于我理解rust语言的。在这个阶段我觉得rust不同于其他高级语言的一些地方一个就是所有权的概念,使用所有权,编译器在编译时会根据一系列规则进行检查,且这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。我理解是说同一个作用域范围内,数据只能被一个变量所使用,如果说发生了赋值的情况,那么除非是这个数据类型本身支持自动拷贝之外,像String这样的类型则必须显式调用clone方法来进行拷贝。这样在内存安全上,rust从根本上杜绝了一些c语言里会有的问题,诸如悬垂指针等。
另外,训练营版本的rustlings本身也加了一些自己的内容。比如今年新增的数据结构与算法章节,就给rustlings通关带来了新的挑战。不过这对于接下来rcore是有好处的,因为rcore里全是数据结构相关的一些东西。
第二阶段是rcore,有五个lab,分别对应着多道程序、地址空间、进程管理、文件系统、并发同步这几个我认为操作系统里最基本的一些实现。
多道程序的实验主要是实现一个系统调用来获取当前任务信息。第一次做的时候并不清楚到底要干什么,但是随着多次的学习了解了,这个TaskInfo结构体的信息其实是要我们在任务结构体里先增加一些对应的字段,然后再通过实现方法来设置这些字段的内容或者是获取这些字段的信息,最后输出给用户。其实绝大多数后面的任务基本也就是对结构体字段的完善与实现方法的撰写。、
在地址空间的实验中,除了上一章的实验之外,还增加了内存分配mmap和munmap的实现。因为开启了虚拟内存,所以现在我们不能直接对任务结构体操作,因为会遇到数据存储在两个页上的情况,我们只能先根据token找到真实地址,然后再获取内容。至于mmap和munmap,那涉及到对页表和memory_set的更改,为此维护了一个BTreeMap,来对应内存地址与具体页的关系。
在进程管理里,先前的任务结构体被重新修改为了进程。并且要实现的系统调用也与进程的产生有关系。此外,对stride算法也有了一定的了解。本质上还是进程里添加了一个字段,然后在每次执行进程时都重新处理一次prio字段并更新记录,以决定下一次执行的进程。
文件系统是比较难啃的一章,这里首先文件系统我不是很了解,而且这一章里也大量使用了闭包,这都是要从零开始学习的地方。由于文件系统大改,sys_spawn需要适配上新的文件系统。这里我原本的写法是将这些操作全部放置于syscall/process.rs下,但是问题在于data会莫名其妙的数据置零。所以只能将这个任务扔进task中了。
而并发同步是我另一个不熟悉的领域,在此之前我对并发这里并没有过多的了解。通过这章的学习也了解了互斥锁和信号量的实现机制。总之还是非常有挑战性以及很有趣的。
至于之后要做什么?我不知道,我手上的活有很多。有很多的任务等待我去完成,比如三阶段的项目制实习,比如明年系统能力赛的内核实现,比如软件所的kernel开发任务。总之一直在路上。我想到了我小学时候读过的一本关于奥数的书,在那里,序言的最后写下一句话:
我想。应该是这样的。
初学这门语言时总觉得是一门很繁琐的语言,需要和编译器,但是经过编译器和 clippy 方便的静态检查(折磨),发现这门语言的确是很严谨的,很多错误都是在编译期就能发现的,到后面就会发现这门语言有一种可以帮你减少错误的感觉。而且 rust 语言的创新性也是很吸引人的,下面是我认为 rust 语言的一些特色:
另外,rust 的迭代 API 也和其他语言不太一样,rust 的迭代器是惰性的,只有在需要的时候才会计算,这样可以避免一些不必要的计算。rust 中的迭代器处理同时满足了效率和代码的简短,是大部分 rust 代码中无处不在的。这些特性使得 rust 语言在系统编程中有着很大的优势。
在 rustling 过程中,有一些题目是编译器告诉我要这么做,然后就过了,虽然体验到了编译器的强大,但是对底层的原理还是似懂非懂。
Option和Result类型的使用感觉有点繁琐,各种.unwarp()。
最后在algo的题目被裸指针的转换弄得很头疼,也许链表Box::into_raw(node)可以用内部可变性和引用计数?
rustlings 只是对 rust 中的一些基本特性做了一个简单的介绍,没有很深入的要求。对于很多特性还可以深挖,而且 rust 中还有很多有意思的特性。以下是我个人再看的一些资料:
rustlings + The Rust Programming Language - The Rust Programming Language
Introduction - Rust By Example
Effective Rust - Effective Rust
Table of Contents - Rust Cookbook
rCore 用比较现代的 RISCV ISA 和 rust 语言介绍并实践了 OS 相关的重要概念和基础功能,如线程调度、地址空间切换、文件系统、IPC和并发。
整体思路:先看看每章的引言和题目,了解下一章将要学到什么。并且带着题目和测例中的疑问点看实现细节和代码。(面向测例编程)
前三章的知识更偏向ISA,riscv 还是比较简单直观的,没有很多复杂的指令和很绕的概念,只需要了解基本的 load/store 和算术运算即可。特权级相关的寄存器比较重要,需要重点记下。
lab2 从内核态复制一个结构体到用户态可以单独用一个函数封装一下,可以参考已经给出的 translated_byte_buffer
的实现,在实现 mmap
的时候要注意 SimpleRange
是左闭右开的区间,然后申请内存前枚举判断区间的相交就很显然了。
lab3 的 spawn 实现的一个坑点就是不能用 TaskControlBlock::new()
,猜测是 stdin
等不能初始化多次,所以最好还是仿照 new 和 fork 在 TaskControlBlock
结构体中写一个 spawn
。stride 算法听起来很吓人,但实现还是很简单的,只需要加入 stride
和 pass
,然后在调度时暴力计算就好了。
前面的代码还算直观,在 lab4 和 lab5 的代码就比较抽象了,为了方便实现参数乱传。
easy-fs 的层级太多了,代码量比较大,做实验时要重点看块管理器和 inode 部分。在 vfs 的 Inode
结构体中只存了 block_id
和 block_offset
,再加一个 inode_id
会对 stat
和 link
的实现比较方便,当然从block_id
和 block_offset
也可以直接反推出 inode_id
。在 unlink
操作时,我获取了一下 nlink
,即枚举这个 inode 有多少个硬链接,如果只有一个,就需要回收inode以及它对应的数据块。这个枚举还存在优化的空间。
ch8 的死锁检测算法需要在每次获取锁和释放锁时更新 Available 和 Need 矩阵,需要注意这两个矩阵的区别,什么时候用到不同的两个矩阵。同时新建线程和锁时要维护矩阵的大小。在 lock 和 down 操作执行前判断死锁。因为要更新矩阵中对应 tid 的一行,所以需要知道task在数组的位置。但是锁里面只能获得 task 对象,没法得到对应的下标。最后我用 get_trap_cx()
获取每个 task 的 trap_cx 比较。现在想想更科学的方法应该是往 TaskControlBlock
里面再加一个元素。
感觉 rCore 整体的代码量比较小,用用户程序测试操作系统的过程特别有意思,用户体验很友好,但是部分测例比较弱。
写完 lab 题目之后还是要尽快写总结,感觉过了几周已经忘了rustling干了啥了。
之前也曾做过os的实验,但是当时所用脚手架代码太多,掩盖了许多底层细节——尤其是内存空间相关的。所以决定做一下rcore,复习os相关知识。
可能因为有相关基础,rcore前四个lab都没怎么费力,只有最后一个lab理解上花了点功夫。究其原因,是因为当时学银行家算法时就没有理解他是怎么运作的。带着先入为主的错误的观点来看实验要求,就感觉百思不得其解了。
我最初以为银行家算法是静态对一个程序进行分析以判断该程序是否会产生死锁的——但实际上银行家算法也可以用于“动态申请🔒时,如果判断可能死锁,就返回错误值”的情况。(实际上对于图灵完备的语言,静态分析反而没办法判断是否会产生死锁)
此外从 rcore 中学到的东西有:
写到这里,第二阶段就要告一段落了。rCore的文档真的写的很棒,作为一名非科班的CS爱好者,终于弥补了自己本科时期的遗憾,系统的学完了操作系统,也对git项目开发和rust有所入门。
学完之后最大的感觉是补全了自己的计算机体系结构,之前一直在学一些语言层面的语法,思考代码主要是从逻辑层面,现在能思考一些比较深刻的东西,debug能力也有所增强。
lab1算是对操作系统的基本入门,了解了用户态程序是如何一步步进入到内核态代码进行处理的。前三章读完之后,感觉分时多任务和上个月学的rtos的工作原理有点像,属于同一类系统。也对如何从0开始构建一个裸机应用,后面想试试用rust编写stm32,应该也会很方便。
lab2加深了我对虚拟地址的一些理解,之前了解过可执行文件的地址空间,当时还在想一个应用有4G的地址空间,真的能存下这么多吗?这一章对页表机制讲得很深刻,解答了我之前所遇到的一些困惑。
lab3的编写,让我对进程管理的一些api及实现有了全新的认识,之前的我只是一个pthread的调包工程师,现在对进程的实现和相关流程有了更为本质的理解。
lab4实现的文件系统,来回看了好几遍,真的挺复杂的。最终完成lab,很有成就感,学习了rust的Trait实现,后面的项目可以参考这个架构。
lab5的各种同步机制,算是对Rust同步机制的巩固,在项目中多次用到了多线程,想想挺可笑的,这些没有锁导致的问题,自己在初学阶段真的都有犯过,后面才有一步步接触到了这些同步机制。
看完整个RCore教程,完成了第二阶段的实验,才发现自己浪费了太多时间在一些开发技术上面了。学完操作系统明白了,这些技术本质上都是相通的,无非是各个语言实现的不同,以后要花重点在这些基础上,做一些更偏底层的学习。
最后,感谢训练营,感谢清华大学能提供这次机会学习操作系统,学到了很多,希望下一个阶段自己能坚持走到最后。
在过去两周,我学习了rCore操作系统。通过阅读实验指导书,我跟着操作系统的发展历程,学习了批处理系统、多道程序与分时多任务、虚拟地址空间、进程管理、文件系统、进程通信和并发等核心概念,并对rCore的实现有了更深入的认识。以下是我对这两周学习过程的总结。
搭建执行环境:
学习了平台与目标三元组,理解了应用程序执行环境。
批处理系统:
学习了批处理系统的基本原理,包括作业调度、作业执行过程。
多道程序与分时多任务:
掌握了多道程序设计的基本概念,以及如何实现分时多任务调度。
虚拟地址空间:
理解了虚拟内存的概念,包括页表、地址映射。
进程管理:
学习了进程的管理、调度,更加深入的理解了fork+exec。
文件系统:
掌握了文件系统的基本结构,包括目录、文件。
进程通信:
学习了父子进程之间的通信机制——管道。
并发:
学习了线程管理机制的设计与实现,理解了同步互斥的实现,包括锁、信号量、条件变量。
Rust语言的安全性和性能在rCore开发中得到了充分体现,经过阅读系统的源码以及作业的编写,对rust语言在内存安全和并发控制方面的优势有了更深的理解,第一阶段学习rust的很多疑问也得到了解答。
通过学习rCore,我对操作系统的原理有了更深入的理解,虽然考研的时候较为系统的学习了操作系统的知识,但是基本上还是停留在理论知识方面。这次rCore学习之旅,我获取PCB对进程进行操作、实现课本上学习过的系统调用、深入汇编代码理解什么是 '陷入' ,让我对操作系统的设计理念、计算机的体系结构有了具象化的认识。
在学习过程中,我也遇到了许多挑战,包括环境搭建遇到报错、Rust基础不够牢固导致代码编写举步维艰等,但通过不断解决这些问题,我的编程能力和问题解决能力得到了显著提升。
两周的rCore学习之旅让我受益匪浅。通过学习rCore,我对操作系统的设计和实现有了更深刻的认识,同时也提升了我的编程技能。我相信,这些知识和经验将对我未来的学习和职业发展产生积极影响。