前言
刚开学就被学长推荐做rCore, 一开始是参加了校内工作室举办的光点计划Ⅱ,做到后面还剩下些时间就来参加了这个训练营,确实学到了很多东西,感觉这几个月是技术提升最快的一段时间。
🦀 第一阶段 - Rust
由于我之前有一点 C++ 基础,这一阶段的入门没有那么困难。Rust 中相当多的概念是与 C++ 互通的,如 RAII, move等等。在这一阶段了解到了 Rust 很多优秀的方面,比如:
- borrow checker (带来了更好的安全性,但是也提升了学习曲线)
- macro (过程宏很强大)
- cargo (太方便了,包管理比 C++ 完善多了)
- rustdoc (标准化了第三方库的文档管理,不管是写文档还是看文档都舒服了很多)
相关资料
学习中确实遇到了很多困惑,以下是一些资料的整理
All
Move
Rust 的 Move by default
有点像带GC的语言,与 C++ 有较大区别。
这几篇虽然主要是讲 C++ 的,但是其语义与 Rust 的 Move 很类似,文章中也有与 Rust 的对比
Slice
Fn/FnMut/FnOnce Trait
Rust 的闭包涉及到了一些所有权的转移问题,所以有一些特别的 Trait 需要注意,初学时有些迷糊。
- Closures: Anonymous Functions that Capture Their Environment
- 三种 Fn 特征
- Rust 中的闭包:function-like types and their traits
- 绕弯大王Rust里的Fn/FnMut/FnOnce
Misc
这几篇涉及一些对个别 Rust 语法的分析,其中最后一篇是我写的
- Rust 中的 Magic function params
- Does Rust have a way similar to prvalue in C++?
- Don't use boxed trait objects
- Rust数组长度中使用泛型参数
😋 第二阶段 - rCore
实验环境配置
本文实验环境为 Manjaro Linux x86_64 6.9.12-3-MANJARO
Qemu 7.0.0
wget https://download.qemu.org/qemu-7.0.0.tar.xz
tar xvJf qemu-7.0.0.tar.xz
cd qemu-7.0.0
./configure --target-list=riscv64-softmmu,riscv64-linux-user
make -j$(nproc)
qemu 7.0.0 编译的时候可能会报错,解决方案如下
修改 ebpf/ebpf_rss.c
中的 bpf_program__set_socket_filter
为 bpf_program__set_type(rss_bpf_ctx->progs.tun_rss_steering_prog, BPF_PROG_TYPE_SOCKET_FILTER);
GDB
首先是确认各项目(如 os
, user
和 easy-fs
) 的 Cargo.toml
中包含如下配置:
[profile.release]
debug = true
这一步如果没做好会导致后面GDB调试的时候断点不生效,十分重要。
关于 GDB 的配置也可以参考以下资料:
主要内容
RISC-V 相关资料
寄存器组 | 保存者 | 功能 |
---|---|---|
a0~a7 (x10~x17) | 调用者保存 | 用来传递输入参数。其中的 a0 和 a1 还用来保存返回值。 |
t0~t6 (x5~x7,x28~x31) | 调用者保存 | 作为临时寄存器使用,在被调函数中可以随意使用无需保存。 |
s0~s11 (x8~x9,x18~x27) | 被调用者保存 | 作为临时寄存器使用,被调函数保存后才能在被调函数中使用。 |
- zero (x0) 恒为零,函数调用不会对它产生影响
- ra (x1) 被调用者保存。被调用者函数可能也会调用函数,在调用之前就需要修改 ra 使得这次调用能正确返回。因此,每个函数都需要在开头保存 ra 到自己的栈帧中,并在结尾使用 ret 返回之前将其恢复。栈帧是当前执行函数用于存储局部变量和函数返回信息的内存结构。
- sp (x2) 是被调用者保存的。这个是之后就会提到的栈指针(Stack Pointer)寄存器,它指向下一个将要被存储的栈顶位置。
- fp (s0),它既可作为s0临时寄存器,也可作为栈帧指针(Frame Pointer)寄存器,表示当前栈帧的起始位置,是一个被调用者保存寄存器。fp 指向的栈帧起始位置 和 sp 指向的栈帧的当前栈顶位置形成了所对应函数栈帧的空间范围。
特权级的切换
特权级机制实现了用户态和内核态的隔离,因此这里的代码涉及到汇编与 Rust 代码的交互,硬件与操作系统的交互,与我以前接触的代码差别很大,虽然代码量不大,但难以理解。正确认识ecall
, sret
以及CSR
相关的原子指令是理解这块内容的关键。
CSR 名 | 该 CSR 与 Trap 相关的功能 |
---|---|
sstatus | SPP 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息 |
sepc | 当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址 |
scause | 描述 Trap 的原因 |
stval | 给出 Trap 附加信息 |
stvec | 控制 Trap 处理代码的入口地址 |
- csrr rd, csr (把控制状态寄存器 csr 的值写入 x[rd])
- csrrw rd, csr, rs (记控制状态寄存器 csr 中的值为 t。把寄存器 x[rs]的值写入 csr,再把 t 写入 x[rd]。)
任务切换
任务切换不涉及特权级切换。任务切换同样对应用是透明的,因此也需要保存相关的寄存器。理解__switch
的四个阶段是关键。
- 阶段 [1]:在 Trap 控制流 A 调用
__switch
之前,A 的内核栈上只有 Trap 上下文和 Trap 处理函数的调用栈信息,而 B 是之前被切换出去的; - 阶段 [2]:A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照;
- 阶段 [3]:这一步极为关键,读取
next_task_cx_ptr
指向的 B 任务上下文,根据 B 任务上下文保存的内容来恢复ra
寄存器、s0~s11
寄存器以及sp
寄存器。只有这一步做完后,__switch
才能做到一个函数跨两条控制流执行,即 通过换栈也就实现了控制流的切换 。 - 阶段 [4]:上一步寄存器恢复完成后,可以看到通过恢复
sp
寄存器换到了任务 B 的内核栈上,进而实现了控制流的切换。这就是为什么__switch
能做到一个函数跨两条控制流执行。此后,当 CPU 执行ret
汇编伪指令完成__switch
函数返回后,任务 B 可以从调用__switch
的位置继续向下执行。
地址空间
第四章我感觉是最难的一章,突然出现了大量的新概念,代码量也激增。理解相关映射方式,跳板,地址空间的布局等是理解地址空间切换的关键。
另外第四章的lab可以实现一个辅助函数copy_to_app
,用来从内核地址空间复制数据到应用地址空间,这样本章和后面的lab都会方便很多。
进程
这一章与前面的任务切换有些类似,理解前面的内容对这一章有很大帮助。其中进程的调度算法比较复杂,也直接影响操作系统的性能。
文件系统
这一章代码量比较大,新概念也很多,感觉难度仅次于第四章。但是好在这里的代码几乎不涉及汇编等与 Rust 的交互,代码逻辑上与平时的编程较为相似,相对来说更容易理解一些。把握easy-fs
的五个层次是关键:
- 磁盘块设备接口层
- 块缓存层
- 磁盘数据结构层
- 磁盘块管理器层
- 索引节点层
另外最近校内有一个工作室的招新题涉及到了这一块,于是我用Rust实现了一个简单的虚拟文件系统。功能很简陋,很多特性都没有支持,但是做完后感觉对文件系统这一块的理解更加深入了。
进程间通信
这一章较前面简单一些,主要涉及管道、信号之类的方法,同时在练习中也了解了邮箱这种方式。这部分内容感觉与 Rust 中的mpsc有些相似。
并发
这部分主要是锁,信号量与条件变量的实现。用户态锁的实现比较有意思,特别是 Peterson 算法比较绕。
本章lab的死锁检测比较难,看题目有些摸不着头脑。
首先要辨清 Available
, Allocation
和 Need
分别对应着什么。
- 可利用资源向量 Available :含有 m 个元素的一维数组,每个元素代表可利用的某一类资源的数目,其初值是该类资源的全部可用数目,其值随该类资源的分配和回收而动态地改变。 Available[j] = k,表示第 j 类资源的可用数量为 k。
- 分配矩阵 Allocation:n * m 矩阵,表示每类资源已分配给每个线程的资源数。 Allocation[i,j] = g,则表示线程 i 当前己分得第 j 类资源的数量为 g。
- 需求矩阵 Need:n * m 的矩阵,表示每个线程还需要的各类资源数量。 Need[i,j] = d,则表示线程 i 还需要第 j 类资源的数量为 d 。
要注意的是这里的资源就是 mutex/semaphore, 第 j 类资源就是 id 为 j 的 mutex/semaphore。