0%

2025春季开源操作系统训练营总结-李明涛

rcore 总结

前言

之前就对rust有一些了解了,跟着《Rust语言圣经(Rust Course)》学过一遍,也做了《Rust by Example》,所以rustling很快就做完了。不过rcore是需要把操作系统相关的知识利用起来的,做完之后是有很多感想和体会的。

ch1

第一个难点是,我第一次接触rust的裸机编程,不过很幸运,因为它和C/C++是很相似的,extern "C"声明一个弱引用符号, 就可以在rust内部获取到符号的地址,通过这种方式可以很轻松地与汇编进行交互了。

第二个难点是rust sbi, 首先我不太能理解SBI是什么,全称叫Supervisor Binary Interface,其次x86里面也没有这种东西可以参考。
我参考了《The RISC-V reader: an open architecture atlas》有关特权架构的部分,了解到riscv中,存在三层特权等级,从下往上依次是机器特权(M),监控特权(S),用户特权(U),SBI有一个类似于x86中bootlooder的作用,用于启动内核镜像,还有一个作用是管理最底层的硬件资源,包括电源、串口和时钟,给处于S模式的内核提供基本的支持。

ch2

这一个章节是一个批处理操作系统,从我们的视角上看,我们终于有了一个用户态和内核态的区分了。在这个操作系统中,一个内核和一个应用被打包到同一个镜像中,在开机时也同时加载到内存中,由内核来执行在内存中放置好的应用程序,结束后直接关机。

这一章节比较复杂的是build.rs,它的任务是加载应用程序并生成一个link_app.S文件,用于指导应用在内存中的布局,如何正确加载到合理的地方,并生成符号,让内核能够读取到应用的入口点。

这一个章节第一次加入了系统调用的概念,这样就将操作系统和用户程序之间联系起来了。在现代处理器中,为了安全,U态的应用是不能访问S态的内存的,这意味着用户应用将将永远不能调用内核的函数,修改内核的数据。但是内核将应用程序与机器隔离了,如果用户不能影响内核,那应用在内核的视角下,与一个while(true)是等价的。

所以系统调用显得尤为重要,用户要进入内核态,需要使用ecall进行调用,在riscv中ecall会使控制流会进入stvec指向的地址,这就是从用户态向内核态的转变入口了,我们在这个位置设置一个trap就可以进行捕获。内核工作结束后,内核调用sret在此返回用户态,只要保证切换前后必须的寄存器状态不变,那么应用就会像什么也没有发生一样正常工作了。

ch3

这一个章节是一个多道程序处理系统,这次需要处理两件事情,第一个是我们的系统需要一次性在内存中加载多个应用程序,第二个是实现分时多任务。

面对第一个问题,基本保持ch2中的代码不变,只需要依次把应用顺序地加载到内存相应的位置即可。唯一需要在意的就是用户的栈空间,这个只需要在内存的某个地方分配一个固定大小的空间即可。

但是第二个问题,需要考虑的就多的了。需要加入以下的内容:

  1. 进程的概念
  2. 上下文切换
  3. 时钟中断

这里的进程并不复杂,最好理解为一个应用程序的执行流,我们需要对这个执行流维护寄存器信息,来保证能够合理地进行上下文切换(switch)。switch操作是不用进入内核的,这个过程只需要保存原进程的寄存器到TaskContext中,再将目标进程的TaskContext恢复回当前的寄存器状态中,如此完成进程切换。

下来是时钟中断,对于时钟和时钟中断,我们需要有硬件的配合,这个任务主要交给SBI。但是要实现合理的时钟中断机制,还需要在trap_handler中添加对Trap的处理。

现在来完成题目,需要引入一个新的系统调用 sys_trace(ID 为 410)用来追踪当前任务系统调用的历史信息,并做对应的修改。

由于当前,我们的内核中还没有“虚拟地址”的概念,所以对于传入的指针可以直接进行读写,不用担心地址空间不同的问题,因此trace_request为0、1的情况可以轻松解决。对于trace_request为2的情况,我在每个进程的结构体中添加了BTreeMap<uszie, usize>来对每一个系统调用的此时进行映射,也由此完成了lab1。

ch4

这一章加入了地址空间的概念,这也是我做得最吃力的一章。我所遇到的核心难点如下:

  1. 如何对代码进行更好地debug?
  2. 用户空间和内核空间如何进行交互?

针对第一个难点,其实rcore里面已经给了很好的方案了,但是在gdb中看代码真的很痛苦!所以我选择使用vscode的codelldb插件,其实后端用的也是gdb,但是在vscode中看代码绝对会舒服很多。

想要用上vscode来debug,涉及两个文件

  1. .vscode/launch.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "version": "0.2.0",
    "configurations": [
    // 对 qemu 进行调试
    {
    "type": "lldb",
    "request": "launch",
    "name": "qemu-debug",
    // 从 ELF 文件中获取 debug 符号表
    "program": "${workspaceFolder}/os/target/riscv64gc-unknown-none-elf/release/os",
    "args": [],
    "processCreateCommands": ["gdb-remote 1234"],
    "cwd": "${workspaceFolder}"
    }
    ]
    }
  2. os/Cargo.toml

    1
    2
    [profile.release]
    debug = 2

配置完成后,直接启动

1
2
3
4
# 启动 gdbgdbserver
cd os
make gdbserver
# 在 vscode 调试页面启动 qemu-debug

实现了debug,接下来还需要处理用户空间和内核空间的交互问题。源代码中已经提供了一个translated_byte_buffer函数,用于获取用户态下,虚拟地址空间区域在物理地址空间的映射。因此,在内核态访问和修改用户态内存,可以通过处理返回的Vec<&'static mut [u8]>,逐个字节进行处理。接下来就可以对sys_tracesys_get_time通过这样的方式进行更新了。

ch5

这一章向操作系统添加了进程管理,前几章的task可以理解为一个执行流,并且其中只是维护了一个用于switch的上下文,缺少父子进程的概念,无法在进程中生成其他进程,同时也几乎没有任务调度的方案。

本章的难度并不大,通过参考已有的sys_forksys_exec系统调用,可以轻松添加sys_spawn

ch6

这一章向操作系统中添加了文件系统,这样,当我们运行任务时,我们不再需要将所有的应用加载到内存中,而是保存在更廉价的磁盘里。在easy-fs中特别值得关注的就是分层的思想,从下到上分五层:

  1. 磁盘块设备接口层
  2. 块缓存层
  3. 磁盘数据结构层
  4. 磁盘块管理器层
  5. 索引节点层

我对文件系统的认知基本来自于《操作系统导论》的VSFS(Very Simple File System),里面还没有硬链接的概念,而且并不区分Inode(面向用户操作的对象)和DiskInode(位于物理磁盘上)两者。也是这个原因,我在做实验时就很疑惑,因为我不清楚links需要在哪里进行维护。所以我还得找资料,我参考了《图解linux内核 基于6.x》中文件系统篇,最后将links放置在DiskInode中。这是这部分第一个需要考量的问题。

第二个问题是结构体的大小,layout.rs中部分结构体大小是需要严格划定的,因为那些结构体是磁盘上的映射,本质上是放在硬盘上的,所以存在一些对齐要求:

  1. DiskInode:125字节(由于#[repr(C)]进行4字节对齐,实际大小为128字节)
  2. DirEntry:32字节

所以如果放置在DiskInode中的links类型的大小大于3个字节,是会出现运行时异常的!参考《死灵书》:https://nomicon.purewhite.io/data.html

接下来的作业就不成问题了。

ch7

说真的ch7没怎么看,因为它也跑不起来,看报错大概是因为zerocopy库不支持nightly-2024-02-25的版本

1
2
3
4
5
[toolchain]
profile = "minimal"
# use the nightly version of the last stable toolchain, see <https://forge.rust-lang.org/>
channel = "nightly-2024-02-25"
components = ["rust-src", "llvm-tools-preview", "rustfmt", "clippy"]

有趣的是其他章节的版本用的工具链是nightly-2024-05-02

1
2
3
4
5
[toolchain]
profile = "minimal"
# use the nightly version of the last stable toolchain, see <https://forge.rust-lang.org/>
channel = "nightly-2024-05-02"
components = ["rust-src", "llvm-tools-preview", "rustfmt", "clippy"]

ch8

这一章节,可以看到rcore中的进程模型又发生了变化,和linux相似。线程作为最基本的调度单位只维护用于switch的上下文,而进程变成了多个线程的载体,维护所有线程的共享资源,主要就包括这一节添加的muxcondvarsemaphore

本节的练习是死锁检测,好巧不巧,我参考的《操作系统概念(原书第10版)》上就有对应的算法解析,类似于用于死锁避免的银行家算法。

最后这道题算是一个趣味题,像这样仅仅提供文字资料和要求的练习还是相对的考验人的!

第二阶段总结

第二阶段给我最大压力的应该就是虚拟地址和文件系统部分,其他章节还好。之前对于操作系统,基本停留在理论阶段,稍微看过一点xv6的代码,不过其实没有太深入,通过rcore的训练,我对操作系统的认识更加深入了。虽然每个地方都是做了一点点的训练,但是从ch1一直到ch8真的实实在在从零构建了一个较为完整的操作系统了。

这个过程中,最受益匪浅的还是查资料的过程,rcore的资料已经很丰富了,但是永远不能指望所有细节都能面面俱到,我参考了很多手册,从配置环境用的qemu9.0,需要修改sbi.rs参考的《RISC-V Supervisor Binary Interface》,还有需要深入rv39虚拟地址机制参考的《The RISC-V Reader: An Open Architecture Atlas》,这样的经历都非常的有价值。

最后是rcore的设计理念,最惊艳的还是文件系统中分层架构,借助rust的语言机制实现几乎零成本抽象,这个过程中,我也逐渐知道什么是好的代码,又一次对于“抽象”有了更深一层的理解。