开源操作系统训练营记录
caozhanhao
2024-11-07 3

前言

刚开学就被学长推荐做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 需要注意,初学时有些迷糊。

Misc

这几篇涉及一些对个别 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_filterbpf_program__set_type(rss_bpf_ctx->progs.tun_rss_steering_prog, BPF_PROG_TYPE_SOCKET_FILTER);
屏幕截图_20241101_091503.png

GDB

首先是确认各项目(如 os, usereasy-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 相关的功能
sstatusSPP 等字段给出 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的四个阶段是关键。
switch.png

  • 阶段 [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 的位置继续向下执行。

地址空间

第四章我感觉是最难的一章,突然出现了大量的新概念,代码量也激增。理解相关映射方式,跳板,地址空间的布局等是理解地址空间切换的关键。
kernel-as-high.png
kernel-as-low.png
另外第四章的lab可以实现一个辅助函数copy_to_app,用来从内核地址空间复制数据到应用地址空间,这样本章和后面的lab都会方便很多。

进程

这一章与前面的任务切换有些类似,理解前面的内容对这一章有很大帮助。其中进程的调度算法比较复杂,也直接影响操作系统的性能。

文件系统

这一章代码量比较大,新概念也很多,感觉难度仅次于第四章。但是好在这里的代码几乎不涉及汇编等与 Rust 的交互,代码逻辑上与平时的编程较为相似,相对来说更容易理解一些。把握easy-fs的五个层次是关键:

  • 磁盘块设备接口层
  • 块缓存层
  • 磁盘数据结构层
  • 磁盘块管理器层
  • 索引节点层

另外最近校内有一个工作室的招新题涉及到了这一块,于是我用Rust实现了一个简单的虚拟文件系统。功能很简陋,很多特性都没有支持,但是做完后感觉对文件系统这一块的理解更加深入了。

进程间通信

这一章较前面简单一些,主要涉及管道、信号之类的方法,同时在练习中也了解了邮箱这种方式。这部分内容感觉与 Rust 中的mpsc有些相似。

并发

这部分主要是锁,信号量与条件变量的实现。用户态锁的实现比较有意思,特别是 Peterson 算法比较绕。
本章lab的死锁检测比较难,看题目有些摸不着头脑。

首先要辨清 Available, AllocationNeed 分别对应着什么。

  • 可利用资源向量 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。

相关资料


🤔 第三阶段 - 组件化操作系统

内核模式

1.png
其中 Unikernel 的应用与内核处于同一特权级,且共享同一地址空间,最终编译形成一个Image,一体运行。
而其他的内核大多应用与内核隔离特权级运行,具有独立的地址空间,最终是不同的Image,独立运行。

PFlash

Qemu的PFlash模拟闪存磁盘,启动时自动从文件加载内容到固定的MMIO区域,而且对读操作不需要驱动,可以直接访问。
2.png

TLSF (Two-Level Segregated Fit)

3.png

  • First Level: 每一位对应一个范围的内存块,示例中分别对应24 ~ 231。1表示空闲。图中两个1。
  • Second Level: 有几位就表示几等分。例如, 26表示64~127,然后进行4等分就是64~79, 80~95, 96~107, 108~127,每一位对应一个范围,同样1表示空闲。

TLSF——一种简单高效的内存池实现

Buddy

5.png
4.jpg

分配时寻找匹配alloc需要(order)的最小块。如果order大于目标,则二分切割,直至相等,每级剩余的部分挂到对应的Order List
释放时查看是否有邻居空闲块,有则尽可能向高Oder合并,直至无法合并,挂到OrderList。

内存分配[二] - Buddy系统的原理

Slab

6.png

分配时从 block 空闲链表中弹出一个 block。
依靠 Buddy 分配器提供内存分配支持,初始时以及 block 不足时,从 BuddyAllocator 申请,分割 block 后加入 block 空闲链表。

Slab 分配器分配内存以字节为单位,基于 Buddy 分配器的大内存进一步细分成小内存分配。换句话说,Slab 分配器仍然从 Buddy 分配器中申请内存,之后自己对申请来的内存细分管理。

Linux 内核 | 内存管理——slab 分配器

基于 Unikernel 的最小化宏内核

7.png

需要的增量工作:

  1. 用户地址空间的创建和区域映射
  2. 在异常中断响应的基础上增加系统调用
  3. 复用 Unikernel 原来的调度机制,针对宏内核扩展 Task 属性
  4. 在内核与用户两个特权级之间的切换机制

兼容 Linux 应用

2024-12-01T11:09:51.png

在应用和内核交互界面上实现兼容。
兼容界面包含三类:
1) syscall
2) procfs & sysfs等伪文件系统
3) 应用、编译器和libc对地址空间的假定,涉及某些参数定义或某些特殊地址的引用

应用的用户栈初始化

Linux应用基于glibc/musl-libc等库编译,libc在调用应用的main之前,检查用户栈上的参数等内容。
而应用启动之后,也可能会调用这些参数。内核需要在切换到首应用前,为应用准备栈上内容。
8.png

Hypervisor

Hypervisor与模拟器Emulator的区别:
2024-12-01T11:21:46.png
根据1974年,Popek和Goldberg对虚拟机的定义, 虚拟机可以看作是物理机的一种高效隔离的复制,蕴含三层含义:同质、高效和资源受控。同质要求ISA的同构,高效要求虚拟化消耗可忽略,资源受控要求中间层对物理资源的完全控制。Hypervisor必须符合上述要求,而模拟器更侧重的是仿真效果,对性能效率通常没有硬性要求。其根本区别是虚拟运行环境和支撑它的物理运行环境的体系结构即ISA是否一致。

两种类型的虚拟化:
2024-12-01T11:20:17.png

Riscv64在特权级模式的H扩展

2024-12-01T11:22:58.png
2024-12-01T11:23:02.png

特权级从三个扩展到五个,新增了与Host平行的Guest域

  • 原来的S增强了对虚拟化支持的特性后,称它为HS。
  • M/HS/U形成Host域,用来运行I型Hypervisor或者II型的HostOS,三个特权级的作用不变。
  • VS/VU形成Guest域,用来运行GuestOS,这两个特权级分别对应内核态和用户态。
  • HS是关键,作为联通真实世界和虚拟世界的通道。体系结构设计了双向变迁机制。

H扩展后,S模式发送明显变化:原有s[xxx]寄存器组作用不变,新增hs[xxx]和vs[xxx]
hs[xxx]寄存器组的作用:面向Guest进行路径控制,例如异常/中断委托等
vs[xxx]寄存器组的作用:直接操纵Guest域中的VS,为其准备或设置状态
2024-12-01T11:23:51.png

Guest与Host的地址空间关系

Guest是指虚拟机所在的执行环境;Host指Hypervisor所处的执行环境。
Hypervisor负责基于HPA面向Guest映射GPA,基本寄存器是hgatp;
Guest认为看到的GPA是“实际”的物理空间,它基于satp映射内部的GVA虚拟空间。
2024-12-01T11:26:25.png
2024-12-01T11:26:54.png

虚拟机的时间中断

物理环境或者qemu模拟器中,时钟中断触发时,能够正常通过stvec寄存器找到异常中断向量表,然后
进入事先注册的响应函数。但是在虚拟机环境下,宿主环境下的原始路径失效了。有两种解决方案:

  1. 启用 RISC-V AIA 机制,把特定的中断委托到虚拟机 Guest 环境下。要求平台支持,且比较复杂。
  2. 通过中断注入的方式来实现。如下例

2024-12-01T11:40:43.png

需要实现两部分的内容:

  1. 响应虚拟机发出的SBI-Call功能调用SetTimer
  2. 响应宿主机时钟中断导致的VM退出,注入到虚拟机内部
    2024-12-01T11:45:03.png
评论 3
X
#11

曹也是用上了rust了是吧

2024-12-25 14:47:10
回复
hua
#10

mb曹神

2024-12-06 22:43:37
回复

xm操作系统

2024-11-24 23:52:00
回复
评论已关闭
发表评论
评论 取消回复
Copyright © 2025 mkfs