0%

2023秋冬季rCore训练营报告

ch0 & ch1 & ch2

  • 跟这 ch0 进行了一些环境配置,然后边看 ch1 边找着文档一起移除标准库依赖,构建用户态执行环境,构建裸机执行环境,并用 qemu 和 gdb 模拟和监测程序运行的状态,让我对什么是操作系统,操作系统内核运行的环境的有了一个较为深刻的理解。而阅读 ch2,令我理解了早期的操作系统是如何来处理程序的。

ch3

  • ch3 中我学习了多道程序的放置与加载,任务切换,如何管理多道程序。然后在练习中为了实现 sys_task_info 我在原本 TaskControlBlock 的基础上添加了 task_syscall_times 数组和 first_start_time 字段来记录获取 taskinfo 所需信息。在 syscall 中调用自己封装的 add_cuurent_task_syscall_times 来实现对 task_syscall_times 记录更新。而对于 first_start_time,我在程序第一次运行时更新来记录,使得在调用 sys_task_info 时能够准确获得程序的运行时长。

ch4

  • ch4 中我详细学习了虚拟内存与 SV39 多级页表机制。在练习中我重写了 sys_get_time,我利用 get_mut 封装了一个将当前任务指定地址usize大小的数据修改的函数,然后将Timeval分为两个指向值的指针传入函数进行修改,以修正ti指向的地址为虚拟地址的问题,并避免了两个值在两个不同虚拟地址的情况,而对与 sys_task_info 我借鉴了前面 sys_get_time 的写法,利用已有的 translated_byte_buffer 将结构体分片为 u8 数组进行取址修改。而对于要求实现 mmap 与 munmap,我先检查函数参数是否出错和指定虚拟页是否有映射,然后调用 insert_framed_area / shrink_to 来实现要求的功能(虽然我认为单纯使用 shrink_to 有点小问题)。

ch5

  • ch5 我学习了进程管理机制的实现。在练习中我将前面的功能全部移植了过来(虽然后来发现有些好像不用),对于 spawn 我参考了 fork(exec()),只是新建 TCB 时改为直接 TaskControlBlock::new(elf_data),然后修改其中的 parent。对于 stride 调度算法,我在 TaskControlBlockInner 中添加 stride 与 pass 字段,set_prio 时 pass改为 BIG_STRIDE / prio。

总结

  • 我跟着 rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档进行了第二阶段的实验,感觉收获还是比较大的,就是环境配置等前置我感觉还是有些混乱,没有一个比较好的教程,希望可以添加一个。最后,感谢老师能供提供一个这么好的平台。

感谢老师和主教们提供的这么一次宝贵的学习操作系统的课程,不管是老师们上课时的专业程度,还是文档的详细度,或是练习的代码,都体现出了老师们对于这门课付出的大量的时间和心血。在第一阶段的学习中,我了解和熟悉了Rust语言的使用方法,并在rustlings中尝试解决各种问题。当然,rust语言也并没有那么容易上手,加上时间有限,在学习第一阶段时候我对rust语言本身还是只有一个概念的雏形,有些rust的语法特性由于缺乏实践没有真正掌握。当然,在第二阶段的学习中,我开始真正尝试使用rust实现系统调用,同时努力学习吸收老师们的代码和思路,不仅能够更加熟练的使用rust语言,还对操作系统有了更清晰的概念。

Read more »

Rust语言学习

  • Rust 编程语言是一个专注于安全、并发和实用性的系统编程语言,语法简洁先进

  • 内存安全:无需垃圾回收器即可保证内存安全。

  • 所有权系统:所有权、借用和生命周期是 Rust 独有的特性,用于在编译时管理内存。

  • 类型系统:Rust 拥有强类型系统和类型推导。

  • 并发:Rust 通过所有权模型来避免数据竞争。

  • 构建系统: Rust拥有强大且易用的构建系统和包管理器cargo

  • 第三方库: 强大的生态系统支持,可以找到几乎任何类型的库。

  • 学习资源丰富,做完Rustlings的一系列小练习可以了解rust的各个方面,有官方中文文档

rcore

课程很硬核,实验较简单

实验一

实验一让我了解了rcore基本的进程调度实现,以及RV64汇编语法,寄存器特性,调用约定,RISC-V的指令集很简洁,总共就100多条指令.

实验二

实验二让我了解了以下内容

  • SV39分页机制: 开启SV39分页后所有的内存访问都变成虚拟地址形式,通过MMU将虚拟地址转成物理地址

  • rcore内存管理: rcore为所有物理地址做了一个恒等映射,可以通过虚拟地址访问所有物理地址,先实现了一个分配物理页帧的FrameAllocater,然后实现了一个MemorySet来实现分配地址空间和地址空间对应的物理内存

  • 双页表OS: 系统为用户态专门设计了一张没有内核模式代码的页表,内核模式区域只包含了trap入口代码,用户态发生trap就跳到Trampoline切换到内核态页表,内核态返回用户态的时候又切换回用户态的页表,这样子做每次trap会多出两次切页表导致TLB清空的性能消耗

    为了防范可能出现的类似”幽灵熔断”漏洞的攻击,牺牲一些性能来换安全性还是有必要的.

实验三

实验三学习了rcore的进程管理机制,fork,exec的实现,Elf映射到内存的过程,以及支持进程优先级的stride调度算法

fork: 按照一个父进程创建一个子进程,子进程拷贝父进程的地址空间

exec: 清空进程的地址空间来执行新的可执行文件

stride调度算法: 支持优先级的进程调度算法

原理是给每个进程添加priority和stride的成员,调度器每次都挑选stride最小的进程来执行,进程执行一个时间片就加上一个大常量BIG_STRIDE除以priority的值

priority要求必须>=2,因为如果=0的话除法就会发生异常,=1的话就不支持溢出处理,>=2的情况下STRIDE_MAX – STRIDE_MIN <= BigStride / 2,这样可以做溢出判断

实验四

实验四学习了easyfs文件系统的实现,这个文件系统可以以模块的形式添加进内核里.

这个内核模块的开发流程可以在用户模式做测试,测试稳定之后放到系统内核里面,这样子做有很多好处

  1. 让内核设计更模块化
  2. 可以把这个文件系统模块放到用户态,如果以后要实现一个用户态解析这个OS文件系统的工具,比如ch6中的easy-fs-fuse可以在测试环境的用户态生成一个fs.img给虚拟机内的操作系统使用

easy-fs的实现:

把磁盘的线性空间分块,每个块大小为常量BLOCK_SZ

第一个块的信息是超级块,包含文件系统的标识,大小,inode区域,数据分布信息

inode用来描述文件和目录,根目录的inode编号是0,相当于linux中的/根目录,通过根目录来实现索引文件系统中的所有文件

Rust & RISC-V & Rcore

第一次知道 rcore 这个项目是在一个 CS 的开源交流群里,因为一直是开发小白,想要找一些项目来写,提升自己的实力。

暑假开始学 xv6,但是因为旅游计划搁置了,刚好开学后听闻 rcore 的项目,脑袋一拍就想参与,并也是进入以后才了解到 rust 语言。

起初参加的原因只是希望找人和我一起写课程来督促我成长,因为本人很容易分心。但是这个问题在开始写 lab 以后就眼小云散了,发现自己在写实验的时候还是很专注,能够投入时间的。

在学习了第一阶段的 rustlings 后,我了解到了很多独属于 rust 本身的魅力,比如所有权,又比如unsafe,同时 rust 的编译器比较严格,能够在编译层面上杜绝一大部分难以在代码层面上发现的 BUG,

同样的,riscv 架构是一个新兴架构,指令集简洁且开源,易于掌握和学习。

下面简单总结一下在第二阶段我完成的各个实验情况。

ch3

本次实验在一个分时多任务系统架构的基础上实现了一个当前任务信息查询调用。

从系统调用的层面上(S态)实现了该功能。

sys_task_info调用的实现建立在对TaskManager的修改的基础上。

我在TaskManagerInner中的TaskControlBlock里添加了syscall_times来统计该任务的各个系统调用次数的统计(在Trap_handler中增加贡献),并且在Inner中添加start_time来计算Task开启的时间长度,任务块实现函数中统计。

由于本次实验框架基于数组的实现方式,是建立在系统体量较小的基础上,所以能够理解。但是如果系统逐渐庞大,系统调用和任务数量的增加,会导致TaskManager内存部分乘法增长(因为是数组套数组)。

任务数量应该远小于系统调用的情况下使用HashMap来替代TaskControlBlock内部的统计数组会好一些。

ch4

本次实验重写了ch3中的sys_write和sys_task_info,因为在分页机制启用的基础上原有代码不再可用。

上面两个系统调用涉及到用户空间中的内存使用,所以我设计了一个modify_byte_buffer函数,用于将src(启用分页的内核空间地址)中的len长度的内容拷贝到ptr(启用分页的内核用户地址)相同长度的物理内存中去。

本次实验还实现了sys_mmap和sys_mummap的匿名映射的内存申请和解绑系统调用。

这两个函数都需要对所映射的地址范围进行检查,其中有一个关键点在于框架内所有的区间都使用左闭右开的形式。而且在使用虚拟地址来找到对应的物理地址时,框架(mm::page_table::find_pte)在第0和第1层遇到没有下一层映射的情况时返回None,但是到第2层时会直接返回,所以我们在返回值不是None的情况下也要判断是否Vaild。

上面这个 Vaild 的问题我真的是调了两个小时才发现的,真的需要知道每一块代码的运行方式和功能。

ch5

本次实验继承了ch4中的所有功能。

同时实现了spawn创建进程以及通过设置priority优先级和stride参数来对进程进行调度。

本次实验较为简单,使用 git cherry-pick 能简单的得到前面提交的代码,方便了功能的继承。

同时这次实验的调试也并不容易,其中在 syscall 统计其次数时,我像之前一样获取了TaskManager的所有权后增加,但是这样导致我在 waitpid 中的一个 assert 一直未能通过,后才发现改正。

总结

本次 rcore-os 训练营的第二阶段,让我巩固和学习掌握了很多操作系统的知识,并且亲身实践了功能,受益匪浅。

第二阶段将较难的一些概念和功能拆分,让我们不至于对一个东西很难上手,同时实验循序渐进,能够在原来写好东西的基础上增加对系统的理解修改代码,让我们的思维不断蜕变;而且增加功能的实验方式让我感觉像是在写自己的系统,就会更认真的写,学到更多东西,有强烈的正反馈。

同时第二阶段的实践也让我的 rust 语言熟练读更上一层楼。

如果还有时间我将继续学习 ch6 和 ch8 两个实验。

写在前面

接触到 rust 断断续续有两年的时间了,始终相信它是未来编程语言的方向(虽然我不是程序,只是一只菜鸡)。没有正经用 rust 去做过项目,参加训练营是机缘巧合,满足了我一直以来的需要:一是给自己一个机会训练 rust 的实际运用。二是本身是 linux 爱好者,使用 linux 超过 20 年,想深入了解一下操作系统的组成及运作原理。

一阶段总结

之前对 rust 的关注,还是带了起手的优势,一阶段完成得比较顺利。印象深刻的是clippy3这样,大家踩的坑都踩过。目前看题目的要求是程序能按设计的逻辑正确实现,而不仅仅是设置属性宏让编译能通过。

二阶段总结

二阶段对我而言几乎是全新的知识。以学习为目的,尽量在理解清楚的基础上做题。题目的完成也以通过测试为目的,未考虑错误处理等等细节。

ch1 & ch2

这两章从应用执行环境说起,引入了程序执行环境和特权级切换这两个基础概念。第二章的批处理系统和后续的操作系统,本身的角色就是在硬件和应用直接充当桥梁的角色。用 M 态抽象硬件的能力,用 S 态提供接口,供 U 态的应用调用。

开始阶段研究了一下实验环境在我的 Gentoo Linux 下执行的可能性,做过一些改动来适配。事实证明,直到目前为止( ch5 )都能正常运行。具体过程记录在 我的准备工作【腾讯文档】Qemu常见问题Q&A 的第 2 ~ 4 个问题。

ch3

题目要求:实现 sys_task_info 系统调用,统计任务信息:包括状态、当前任务所用到的系统调用ID及对应的调用次数,任务开始到目前为止的时长(只考虑开始到现在的时长)

思路

  1. task只会以两种方式开始执行,run_first_task 或run_next_task,只在一个地方退出:mark_current_exited。所以在任务开始时保存当时的时间,退出的时候再取一次时间,二者的差就是当前任务的运行时长。
  2. 系统调用最终都是通过 sys_call 进行调用,它的入参就有该次调用的 syscall_id 只需要在这里统计当前任务的系统调用 ID和次数即可。

小结

具体实现时为了不影响正在执行的任务,直接从当前任务的 TaskInfo 中克隆了它的副本来计算目前为止的运行时间,应该可以有更优的方式。

ch4

个人觉得是前三个里面最难的,内存管理的部分非常抽象并有一定复杂度。主要是实现 map 和 unmap 功能,能让应用正常申请释放内存。

思路

  1. map 时需要遍历当前 task 的 MemorySet.areas,从而判定需要分配的虚拟地址范围 start ~ start + len 有没有跟 areas 里面的 某个 area 有重叠,如果没有,才能创建新的 MapArea,并将它推入 MemorySet.areas。 最后这步不能忘记!

  2. 因为 map 的机制,areas 中不会出现部分重叠的区域,所以 unmap 的时候,找到的 area 的 范围和 start ~ start + len 一定是重合的。还有就是记得要将它从 areas 中移除。

  3. map 和 unmap ,start ~ start + len 覆盖的虚拟和物理地址页面都可能不止一个

小结

理解映射关系花了很多时间和心思,最后总结的结果是:map 时,经过一系列检查,先创建了一个 area,此时它的 VPNRange 是 start ~ start + len, 但 data_frames 还是空的,直到我们将它 push 到 MemorySet.areas 时,在 push 函数的实现里,才为每个vpn 分配了一个 ppn ,并将二者的映射插入到 data_frames 中,完成映射。unmap 时我采取的方法是直接遍历 area.vpn_range, 将 vpn 和 ppn 的映射关系断开,然后从 MemorySet.areas 中去掉当前的area,被分配的物理节点会在映射断开时自动释放,area 在从 areas 中移除后,也会自动被释放,从而完成 unmap 操作。

ch5

相对ch4, 这个让人稍微能缓缓。

思路

spawn 的实现,在提示里面已经给出了,可以参考 fork + exec。区别就是建立 MemorySet 时,改成从 path 读取,最后按照 exec 的方式来设置 trap_cx。

stride 调度算法仅仅只是过关。实现了当 priority 变化时,重新 设置当前任务的 stride。 在 task 管理中,添加任务时根据 stride 添加到适当的位置。当 pop_front 取任务时,自然就取到最小的。没有考虑溢出或其他情况。

小结

Stride 考虑溢出的话会更公平,调度队列考虑使用 BtreeMap,利用它的天然有序性,以 stride 为 key,应该表现会更好。

未完待续

第一次参加开源训练营,给我的感觉很不错,感谢老师和助教们的付出。这也是我第一次写rust语言,刚开始写的时候一路想弃坑最后还是坚持下来了。

第一阶段

第一阶段内容还是比较简单文档也很清晰,照着做就能够完成。第一阶段遇到的最大难题是加入训练营时还有五天就开第二阶段了,然后就拼命爆肝rustling。

第二阶段

第二阶段就不得不吐槽一下实验文档,刚开始只知道有guide,后面发现还有一本book v3, 感觉还得优化下实验说明, 很容易一开始就找不到方向。还有就是实验测试样例说明不够,只能靠自己摸索。不过指导书写的很不错,之前有上过操作系统课,学习起来还是比较轻松。

ch1 ch2 ch3

这部分给我开了个好头,os课上学的知识还是比较零碎的。rcore的操作系统发展脉络很清晰,操作系统上的程序怎么运行起来的,如何构建用户态向内核态的系统调用请求,如何构建裸机上的操作系统,都讲述的很清楚。实验部分完成task_info系统调用难度也不大。

ch4

地址空间这章感觉是这几章中最难的一章,之前对虚拟内存没什么概念,v3这里也是好长的几章字。学完了现在感觉也是似懂非懂的,
不过好在这章实验不是很复杂只需要实现4种系统调用,前面两种还是重写上一章的只不过加入了虚拟地址转换。不过这里有一个神奇的地方在于ch4里面get_time_us()和get_time_ms()产生了很大的误差,而在ch3里面却通过了。

ch5

进程这章比较简单。不过让我学习到的地方在于,指导书上面介绍了
为什么要实现进程。这章实验要求在新增进程功能下,不仅让代码能通过之前的测试,还要新实现sys_spawn,以及进程调度算法,不过总的不算困难。

rCore 二阶段总结

第一次看到训练营的通知是在 Rust 中文社区,本着尽可能的抓住所有学习的机会就随手报了训练营,
结果被 Rust 的语法的优雅,还有操作系统的复杂性,功能性深深吸引,奈何我只有一些通识导论课的基础,没有关于 Risc-v 或计算机体系结构等的课程基础,所以
学期二阶段的课程非常的慢,不过还好,赶上了截止日期。

第一第二章是教学演示用的,但是为了能跑起来我还是花了很长时间,在 mac 下配置 docker,在 docker 里配置实验环境也是踩了不少的坑。
rCore 以一种计算机操作系统历史的教学方法,从简单到困难,从底层的基础到高深的抽象,使得我虽然没有什么计算机基础(只有简单的编程基础)也能学的津津有味。

ch1 讲的是一个最小用户态的执行环境操作系统,使得可以在裸机上,不调用任何标准库,通过编写的 kernel 以及实现的 sys_write 系统调用接口,来打印属于 rCore 的 hello world!
这一章给我带来的极大的兴趣,让我对操作系统的认知从之前的一个深不可测的黑盒到现在的“最基本的是一个执行环境”。

ch2 则针对操作系统的任务执行过程来讲,实现了从第一章的只能运行一个应用程序的最小用户态执行环境到可以一次性将多个任务加载到内存,并分别处理多个任务的批处理操作系统,提高了运行效率。

ch3 则更进一步,引入了程序自动放弃cpu的控制权 yeild 和系统在时间片下主动暂停程序执行的时钟中断,更进一步的提高了运行效率。

ch4 引入了内存空间这一概念,个人觉得这一章是我看到现在为止最复杂的一章,多级页表的设计,任务管理器与页表的连接,以及跳板的作用,如何处理陷入,非常复杂,第一次看完还是很蒙,但是头铁直接去做 lab2,边做边看源码,也慢慢的理解了其设计逻辑。

ch5 引入了进程的概念,更加细化了应用执行过程,代码逻辑中将 TaskManager 二分为处理 保存系统中存在的进程的容器 和 cpu 正在执行的进程。在从 ch4 迁移到 ch5 中,对我来说还是有一点难度。

rCore 的文档对于像我这样的新手来说非常好,实验难度也适中,给我的感觉是实验本身要实现的逻辑不是很难,但是实验的要求应该也不是仅仅通过测试用例,还要了解阅读 rCore 的核心代码和逻辑。
才能得到更好的收获。

lab1实验总结和疑难点

ch3的实验相对来说较为简单,由于系统调用任务增加任务各自的系统调用数量syscall_times并且要得到任务的相关属性taskinfo,所以在任务控制块TaskControl那里的inner模块中加入相关的变量记录即可。

lab2实验总结和疑难点

ch4的实验开始就有一些难度,特别是mmap和munmap不知道该怎么实现,于是我使用逆推的办法,mmap和munmap都是要对一部分虚拟地址进行alloc和dealloc操作,所以从这一点出发找到了当前任务的memory_set,然后对该任务的memory_set那部分虚拟地址进行insert_frame和unmap即可,然后再用接口暴露给系统调用即可。

lab3实验总结和疑难点

ch5的实验比ch4相对来说简单一点,sys_spawn只需要复制一遍fork改一些参数即可,轮转调度只需要在fetch_task的时候对每个task的当前stride进行比较,挑选得到相应的task即可。

lab4实验总结和疑难点

ch6的实验很难,因为涉及的结构体很多以及各个结构体之间存在相互有关的关系,于是我根据文档整理了一下接口。

File

  1. readable: 返回可读属性
  2. writable: 返回可写属性
  3. read(mut user_buf): user_buf获取输入信息, 返回1
  4. write(user_buf): 打印user_buf信息,返回长度

TaskControlBlockInner

  1. 新增fd_table: Vec<Option<Arc<dyn File + Send + Sync>>>

Syscall

  1. sys_write(fd, buf, len):fd为程序输入到那个文件,将文件的信息输出出来
  2. sys_read(fd, buf, len):将用户输入放到buf中
  3. sys_openat(dirfd, path, sflags, mode):用各种不同的读写方式打开文件

BlockDevice

  1. read_block(block_id, &mut buf):将blockid对应的块内容读到buf中
  2. write_block(block_id, &buf):将buf写到对应block

一个块512字节,512*8bit

BlockCache

  1. 由512个8bit,block_id, block_device:Arc和modified组成
  2. addr_of_offset(offset):返回offset位置的cache元素。
  3. get_ref(offset):获取offset位置的模板T的对应指针。
  4. get_mut(offset):同上但可以修改。
  5. read(offset): get_ref。
  6. modify(offset): get_mut。
  7. drop:如果修改了将结果写到对应block中。

ClockCacheManager

  1. 由Arc<Mutex>组成的VecDeque
  2. get_block_cache(block_id, block_device):从VecDeque中获取对应的block_cache,如果queue满了则弹出强引用计数为1的cache然后存入queue,就是弹出自己再加入。

easy-fs布局

SuperBlock

  1. 由合法性验证的魔数magic,文件系统的总块数total_blocks,索引节点位图,索引节点区域,数据块位图,数据块区域组成。
  2. initialize():有磁盘块管理器传入初始化参数
  3. is_valid():判断是否合法。

Bitmap

  1. 由start_block_id和blocks组成,起始块编号和块数
  2. alloc(block_device):先得到bits64_pos和inner_pos为第blockid块磁盘中未完全分配,然后发现是这一块的第bits64_pos个64位没完全分配,再找到这个第64位的第一个0处于的比特位赋值为1,并返回这一位在整个Bitmap中从startblock一开始处于第几位。

DiskInode

  1. 由文件字节数size,直接块编号,间接块编号组成。
  2. initialze(),初始化。
  3. is_dir(): 是否为目录。
  4. is_file(): 是否为文件。
  5. get_block_id(inner_id, block_device): 获取inner_id对应的文件存储的blockid。
  6. data_blocks(): 得到一共占了多少数据块,向上取整。
  7. total_blocks(): 数据块与索引块的总和。
  8. blocks_num_needed(new_size): 得到从size扩容到new_size需要的额外数据块。
  9. increase_size(new_size, new_blocks, block_Device): 扩容函数。
  10. clear_size(block_Device): 清空文件内容,返回一个Vec重新送给磁盘块管理器进行调度。
  11. read_At(offset, buf, block_device): 将文件内容从offset字节开始的部分读到buf中。返回读到的字节数。
  12. write_at(): 同上实现但是需要increase_size。

DirEntry

  1. 由目录名和inode_number组成。

EasyFileSystem

  1. 由block_device, inode_bitmap, data_bitmap, inode_area_start_block和data_area_Start_block组成。
  2. create(block_Device, total_blocks, inode_bitmap_blocks): 创建efs实例并且将第0快设置为超级块
  3. open(block_device):将编号位0的超级块读出然后构造efs实例。
  4. get_disk_inode_pos(inode_id): 得到inode_id对应的数据块在第几块以及offset。
  5. get_Data_block_id(data_block_id):同上,得到数据块所在的实际位置
  6. alloc_inode(), alloc_Data(): 返回alloc所在的数据块id。
  7. dealloc_Data(block_id): 将cache中blockid的块清零并且释放block_id-data_Area_Start_id的空间。
  8. root_inode(efs): 获取根目录的inode。

Inode

  1. 由block_id, block_offset, fs文件系统, block_Device组成。
  2. read_disk_inode()
  3. modify_disk_inode()
  4. find(name): 根据名字得到对应存inode的位置并且构造Inode。
  5. find_inode_id(name, disk_inode): 对文件系统进行遍历查找取出每个DirEntry并得到相应的目录名与name进行比对,返回DirEntry的Inodenumber。
  6. ls同上但是把每个dirent的名字记下来。
  7. create(name):先查有没有当前name的inode,然后alloc一个新inode并且得到存储inode需要的数据块id和偏移,然后将新文件目录项插入到根目录内容中。
  8. clear():
  9. read_at(offset, buf: &mut [u8]): disknode.read_at.
  10. write_at同上,但需要扩容
  11. increa_size(new_size, disk_inode, fs): disk_inode.increase_size。

然后继续使用逆推法得到Inode应该是整个的核心代码编写处,关键之处在于要得到inode_id,因为没有直接的name查询的接口,需要使用inode_id进行linkat和unlinkat的维护,疑难点在于接口太多,这一部分我借鉴了以往同学的报告才得以理解。

lab5实验总结和疑难点

ch8的实验相对来说比较简单,在每个mutex和sem里记录分给了哪些任务的tid以及还有哪些任务tid在等待就可以得到allocation和need,available我两种方式都写了一下,一种是在process里直接维护,还有一种是每个mutex和sem各自维护,最后得到这些之后进行银行家算法即可。唯一的难点就是别忘记写sys_gettime,我因为没写这个系统调用卡了一天,期间使用过trace但是忽略了gettime的报错,一直trace的是我银行家算法里的数据allocation和need等。

前言

首先,非常感谢 rCore 训练营提供的学习机会。在训练营里,我收获颇多,不仅巩固了以往学到的一些技法,还把以往仅在书本上了解的操作系统原理转化成了实际的项目代码,一步步看着以往学过的知识点被串联起来,真是一次难忘的经历!

训练阶段

第一阶段 Rust 学习

因为之前我并没有很深入地学过 rust 语言,想要在两周内初步掌握一门静态强类型语言还是稍微有点挑战性的。
幸好,rust 的核心概念所有权和生命周期在以往学习 C++ 的过程中已有涉猎,因此学习的过程并没有预期中那么 “陡峭”,第一周便通关了 rustlings。

虽然说字面上通关了,但是实际上有些细节部分还是没有掌握得很透彻,于是第二周便打算直接在项目代码中学习,半学半仿写地看了一遍 rCore 的代码。在项目中学习到了不少的 rust 编程技巧,例如各种 trait,from/into 的用法,泛型的使用技巧,函数式编程的思维方式等,可谓收获满满。

整体来说,rust 这门语言给我的观感非常不错,他一方面吸收现代语言的灵活性,另一方面也保留了对底层的控制能力,也 “堵” 上了不少 C++ 的坑点,接下来有计划更进一步深入学习。

第二阶段 rCore Labs

在计算机科学领域,操作系统扮演了重要的角色,它们不仅支持着现代计算设备的正常运行,还提供了深入研究和探索计算机内部工作原理的机会。因此,我一直对操作系统有着浓厚的兴趣,并渴望了解它们的内部机制。在做 Labs 的过程中,我有种逐渐拨开迷雾的明朗感,以往看作黑箱的各种操作系统机制,被具体地呈现到眼前,可谓非常地 “过瘾”。

  1. Lab1
    没有什么特别复杂的地方,主要一个坑的地方是,get_time_us 的精度问题,syscall计数很好地巩固系统调用的流程,明确地展示了特权级别切换的具体过程。

  2. Lab2
    页表的具体实现比较抽象,看的过程中理了几次流程图。但是理解之后发现,其中 RAII 方式管理页帧的方法非常的优雅。在理解 mm 模块之后,mmap 和 munmap 的实现就比较直接明了了,只要做好边界检测和合法性检测,实现的过程比较简单。需要注意的是前面写的 syscall 因为内存地址空间的切换,需要把用户空间的地址转换成操作系统访问的物理地址。

  3. Lab3
    spawn 仿照的 fork 和 exec,实现比较简单,stride 算法实现也比较简单,没有特别复杂的地方。

  4. Lab4
    坑的地方来了,Lab3 写的 spawn 在迁移到 Lab4 后无法正常运作了。在排查了一段时间后发现貌似和内存对齐有关(莫非是编译器bug?),似乎超出了我的能力范围,决定暂时不深入探究。最后解决的方案是使用 TaskControlBlock 提供的 fork 和 exec 函数组合。而 link 和 unlink 的实现需要修改 fs 的代码,通过 DiskInode 记录文件信息,透过 OSInode,Inode 层层调用。link 的本质是两个不同名的文件指向同一个,Inode,这让我切切实实地体会到了操作系统对磁盘的抽象。

  5. Lab5
    又一坑位,Semaphore 的测试点依赖 sys_get_time 函数,没实现会直接死循环。由于这次 Lab 不要求合并前面的代码,于是没考虑这个东西,一直在找算法的 bug,卡了一天多的时间。今天晚上通过群友指点才知道问题所在。。。

总结

通过训练营,我初步地了解了操作系统的内部工作原理,学习了如何编写高效且安全的系统级代码。Rust编程语言的高效与抽象让我对代码的可维护性和可靠性有了更深刻的认识。

与训练营的导师和同学合作是一个非常愉快的经历。我有机会与有经验的人合作,学习他们的技能和思考方式。团队合作让我明白了众人拾柴火焰高的道理,锻炼了我的沟通和合作技能。希望通过参加训练营,提高我的编程技能、系统级知识和解决问题的能力。此外,我希望通过与训练营导师和同学的合作,不断进步,并将所学应用于实际工作中,并对 rCore 开源社区作出贡献,未来积极参与 rCore 和 rust 社区,并期待在未来可以贡献于开源操作系统项目。

最后,我想向rCore开源操作系统训练营的所有人表示由衷的感谢!rCore开源操作系统训练营是一次宝贵的学习经历,不仅提高了我的技术水平,还拓宽了我的视野,感谢导师和组织者为我们提供了这个机会!

2023秋冬季rCore训练营报告

Rust

参加这次训练营的初始目的就是因为曾经多次学习 Rust ,但是因为工作和个人项目使用很少,感觉使用不是很熟练,想借用这次机会加深理解。对 Rust 的兴趣来源于网上将其与 C/C++ 的对比以及称其可以替代 C/C++ 的相关文章,其中让我在意的是它的“零开销抽象”“内存安全性”及 Cargo 。最吸引我的是 Cargo ,相对于 C/C++ 几乎没有包管理,没有集成工具链,偶尔一个开源库的构建让人焦头烂额,Cargo 让我的编程体验有了很大的提升,非常的高效和便捷,rustup 作为工具链管理,非常方便地完成了跨平台编译的工作。相对的,生命周期和借用检查是比较复杂的部分,需要多加学习、实战才能熟练的掌握运用。在本次相关实验中,让我对生命周期、借用检查有了更深的理解,对 unsafe 的相关内容有了新的认识。

rCore

RISC-V

很久就听闻过 RISC-V,诸如阿里的“平头哥”,但是只知道是一套基于精简指令集的开源指令集架构,并不曾深入了解,借本次训练营正好熟悉了相关内容。使用下来发现,RISC-V 的 SBI 提供了很大的便利性,在之前 x86 架构上的 OS 实验未曾体验过相似功能,感觉启动流程似乎没有那么复杂(或者是未使用 grub 等相关 Bootloader)。这次仅使用了 SBI 的少量功能,诸如物理内存探测等其他能力。相较于 x86 , RISC-V 的指令简洁,类似于 pop push 这样的指令都不存在,文档也相对简洁了不少。本次实验中个人对 RISC-V 的相关投入比较少,但非常感兴趣,希望 RISC-V 的生态越来越好,后续能有机会设计一款基于 RISC-V 的 CPU。

实验内容

从实验文档的第零章 “实验环境配置” 开始搭建 rCore 实验环境,个人采用 wsl2。在编译 qemu 的过程中发现编译非常慢,一度以为是电脑硬件或操作系统本身导致的原因,后续经过查阅,发现是 wsl2 的问题,在 wsl2 中访问 Windows 分区中的内容会很慢。后来尝试将 qemu 的源码移动到 wsl2 中,编译速度大幅提升。

从第一章开始 Rust 的实际代码编写,了解了 Rust 除了 std 标准库以外的另一个核心库 core,使用 ![no_mangle] 防止编译器修改函数名称的方法,以及 Rust 的裸机开发能力。第二章讲解了如何实现应用程序以及基于RISC-V的特权级切换完成任务批处理功能,RISC-V 的机器模式(machine mode)、为 Linux、Windows 提供支持的监管者模式(supervisor mode)以及用户模式(user mode)。rCore 的用户程序设计运行在用户模式。从用户模式发起系统调用的操作,特权级寄存器的处理、栈切换、Trap 上下文的保存恢复等相关汇编内容为后续的学习内容的重点。

lab1

第三章实现简单的任务功能,完成任务的加载与切换,使用 RISC-V 提供的时钟中断实现时间片轮转算法来进行任务调度。时钟中断的的开启与中断触发间隔由 RustSBI 提供的相关接口完成设置。

lab1 的实验内容相对简单,在任务控制块中加入相关字段存储任务首次启动时间及相关任务的系统调用次数,在每次发生中断和系统调用时更新相关系统调用的调用次数。在 sys_task_info 中返回当前时间减去任务首次启动时间及存储的系统调用次数即可。

lab2

第四章开始学习物理内存管理相关内容。使用 Rust 的全局的动态内存分配器的实现 buddy_system_allocator 完成在内核中使用堆空间。
基于 RISC-V 硬件支持的 SV39 多级页表实现地址空间、物理页的分配与回收。开始划分内核地址空间及应用地址空间,实现应用程序不在需要关注应用起始地址及存放位置。相比上一章节的中断和系统调用处理过程添加了跳板页面,来避免在用户模式进入内核模式或内核模式退回用户模式时导致的地址不一致。

lab2 实验中 lab1 实验的相关内容需要重新实现,因为内存分页后在用户态传递进来的参数地址是虚拟地址,内核的访问地址映射和物理地址一致,无法通过虚拟地址对传递进来的参数赋值,所以需要将虚拟地址转换为物理地址,才能完成赋值。
sys_mmap 的实现参考系统中 insert_framed_area 的实现,添加逻辑校验给定的地址中是否包含已经映射的地址即可。sys_munmap 根据 sys_mmap 的实现反推即可实现。

lab3

第五章将前面章节中的任务进化为“进程”,讲解了进程的含义、进程的核心数据结构及进程调度。在本章中进程 (Process) 的含义是在操作系统管理下的程序的一次执行过程,是系统进行资源分配的基本单位,进程标识符是进程的id,表示进程的唯一性。内核栈保存着进程运行期间的数据,进程控制块是内核对进程进行管理的单位,等价于一个进程。任务管理器仅负责管理所有进程,处理器管理用于进程调度,维护进程的处理器状态。同时实现了使用 SBI 的 console_getchar 从用户键盘读取输入的用户程序 shell,以及fork 的实现要注意子进程的返回值。

la3 spawn 的实现调用任务控制块(TaskControlBlock)的 new 方法基于 elf 创建一个新的任务控制块,然后将其添加到当前任务的子线程集合,最后调用 add_task 将其添加到任务调用队列。

lab4

第六章着手文件系统,讲解了文件与文件描述符、标准输入与标准书输出。rCore 对文件系统的设计进行了大量的简化,在进程中结构中添加了文件描述符表,用于语录所有它请求内核打开并可以读写的文件集合。进程通过文件描述符在自身的文件描述符表中招到对应的文件进行操作。rCore 将文件系统独立为 easy-fs 模块,设计感觉比 fat12 简单,分为五层,磁盘块设备接口、块缓存、磁盘数据结构、磁盘块管理器、索引节点,采用 virtio-drivers 库完成了对 qemu 虚拟磁盘的驱动。

lab4 sys_link 的实现参照 Inode 的 create 方法,创建 DirEntry 指向同一个 inode_id,不同的文件名称。sys_unlinkat 选择使用 modify_disk_inode 方法遍历根目录,在所有的 DirEntry 中查找名称一致的,将其赋值为 empty。ys_stat 主要关注 nlink, 在 DiskInode 中添加 nlink 字段用于记录文件对应的硬链接数量,并且在 DiskInode 初始化、添加硬链接以及删除硬链接时,对应修改其数值。

第七章开始实现 Linux 中常用的管道符,管道是一种由操作系统提供的进程间通信机制,可通过编程或在shell程序的帮助下轻松地把不同进程的输入和输出对接起来,实现不同进程功能的组合。同时还有基于“事件通知”需求的信号机制,信号是操作系统间通信的一种异步机制,用来提醒某进程一个特定事件已经发生,需要及时处理。与硬件中断进行比较,我们可以把信号描述为软件中断,它们都可以用某种方式屏蔽,还细分为全局屏蔽和局部屏蔽。

lab5

第八章开始为 rCore 支持线程,线程的实现依托于进程,是进程的组成部分,进程可包含 1 – n 个线程,属于同一个进程的线程共享进程的资源。在有了线程后,进程是线程的资源容器,线程成为了程序的基本执行实体。基于前面进程的设计将进程重构,将线程相关数据结构整理转移到线程控制块中,并用进程管理线程控制块,完成相关结构的设计,沿用第三章的任务上下文切换及特权级上下文切换即可。互斥锁、信号量以及条件变量相关机制的引入解决了由多线程引起的线程同步问题,完成了对共享资源的临界区保护,实现的方案有基于原子指令 CAS 及 CAS,以及关闭中断。同时需要考虑实现让权等待防止忙等待占用 CPU 资源。

lab5 使用银行家算法完成死锁检测,对相关逻辑封装为结构体 DeadlockChecker,在 mutex 和 semaphore 分配时设置可用资源,在申请锁和释放锁时相应的进行资源申请的安全检测和资源的释放。根据提示使用银行家算法完成相关测试,个人感觉有待商榷,实现的有些取巧,后续在整理优化一下。


本次训练营收获颇丰,对操作系统的原理有了更多的理解,巩固了理论知识,也加强了我对 Rust 的掌握。本次课程未能加入多核相关内容,稍有遗憾。非常感谢训练营的各位老师以及LearningOS的所有贡献者们提供的机会及相关资料。