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的所有贡献者们提供的机会及相关资料。