0%

2025s-oscamp-xxxuuu

偶然有天看到 Rust 中文社区的公众号转发了一个开源 OS 训练营的消息,点进去一看原来是 rCore,早就听过 rCore 这个项目,但一直没来得及学习,自己也对 Rust 在嵌入式开发和内核中的应用很感兴趣,于是转发到群里忽悠了几个朋友一起组团参加

训练营总体分为四个阶段,前三个阶段是练习和实验为主,第四阶段是偏自主探索的项目部分

第一阶段

第一阶段是 Rust 基础的学习,基本是完成 rustlings 训练即可,由于之前已经有了一定 Rust 基础,所以没什么障碍很快就做完了

第二阶段

第二阶段是 rCore 的实验,跑在 RISC-V 架构上。ch1 是如何运行一个裸机程序的简单指导,正式的内容从 ch2 开始

ch2 批处理系统

ch2 实现了一个批处理系统:

  • 允许按顺序执行多个应用程序,这些应用在构建时被静态链接到内核 binary 中。启动应用时复制其 binary 到入口地址 0x80400000 上,然后保存上下文切换状态跳转执行
  • 实现了特权级切换机制:
    • 应用在 U 特权级运行,通过 ecall 触发 Trap 切换到 S 特权级
    • 内核处理系统调用或错误后,通过 sret 指令返回U特权级继续执行
  • 提供了错误处理机制:
    • 当应用访问非法地址或执行非法指令时触发 Trap
    • 内核可以终止当前应用并执行下一个

核心组件包括:

  • AppManager:负责应用的加载和运行管理
  • TrapContext:保存 Trap 发生时的上下文信息,包括通用寄存器和 CSR
  • 两个特权级切换的关键函数:__alltraps(保存上下文) 和 __restore(恢复上下文)

ch3 多道任务与分时多任务

相比 ch2 执行完一个才执行下一个的批处理系统。ch3 实现了分时多任务

有几个主要变化:

  1. 需要能同时加载多个程序,ch2 每个程序被加载到固定的地址中运行。ch3 则为每个程序划分了单独的地址范围
  2. 任务切换,ch3 需要在多个任务间切换,要为每个任务保存单独的上下文,任务间切换时也是在 Trap 中进行,通过 __switch 保存当前任务上下文,设置新任务上下文,__restore 再回到用户态。流程变成 __alltraps__switch__restore
  3. 提供了一些新的系统调用允许用户态程序自行让出,例如 sys_yield,以及为用户态程序添加了对应的状态以进行管理和维护
  4. 设置时钟中断,在中断中切换任务,实现抢占式调度

Lab:

  • 实现一个系统调用 sys_trace,可以读取、修改内存地址,以及追踪系统调用次数

比较简单,在 TaskManger 里加一个 map 统计每个 task 的调用计数即可

ch4 地址空间

ch4 实现虚拟内存,在前面的部分中,我们都是直接读写的物理内存

虚拟内存并不是完全由内核纯软件实现的机制,而是软硬件结合的。RISC-V 64 中包括 SV39 和 SV48 两个虚拟内存模式

SV39 模式中,所有 S/U 特权级下的访存都被视为 39 位的虚拟地址。页面大小 4KiB,页表项大小 8 字节,能支持 56 位($2^{26}$ GiB)大小的物理地址

内核现在需要维护页表本身的信息,即元数据的存储,包括页帧,虚拟页和物理页的映射等

另外,内核现在读写的也是虚拟内存。rCore 采用了内核和应用地址空间隔离的设计(与 Linux 中应用地址空间和内核映射在一起的设计不同),在特权级上下文切换时需要切换地址空间。过程中必须确保切换前后的正常运行,例如切换后的下一条指令地址也需要能正常访问,这里通过 trampoline 技巧实现了这一点

Lab:

  1. 重写 sys_get_timesys_trace,参考 sys_write 即可
  2. 实现 mmapmunmap,但不包括文件和 I/O 相关的映射,类似于 linux 中 mmap 带上 MAP_ANON flag。参考每个 Task 维护的 MemorySet 内容即可

ch5 进程及进程管理

ch5 主要实现了进程管理机制,引入了一个简易的 shell 来运行应用程序。核心改动包括:

  • 新增了三个核心系统调用:forkexecwaitpid
  • 进程机制的改进:
    • 链接和加载机制改为按应用名进行
    • 引入进程控制块(PCB),将应用 id 改为 pid,并维护子进程信息
    • TaskManager 职责部分转移到 Processor,支持每核心一个 Processor(当前仅实现单核)

Lab:

  • 重写 sys_get_timesys_mmap/sys_munmap 系统调用,适配新的进程结构
  • 实现 spawn 系统调用,效果上类似 forkexec 的组合(但没有复制地址空间的必要了)
  • 实现 stride 调度算法,注意到调度时通过 TaskManagerfetch() 来获取任务,在这实现即可

ch6 文件系统与 I/O 重定向

ch6 实现了文件系统与 I/O 抽象。核心包括:

  • 使用第三方的 VirtIO 驱动库实现块设备 I/O
  • 设计了单层目录的文件系统 easy-fs,独立为一个单独 crate 实现:
    • DiskInode 作为磁盘上的索引节点
    • Inode 作为内存中暴露给用户的结构体
    • DirEntry 维护文件名称和 inode_id 的映射
  • 应用程序改为以文件形式动态加载,不再直接链接进内核

Lab:

  • 实现 linkat/unlinkat 系统调用:
    • 只要理解了文件系统的底层数据分布就比较简单了
    • DiskInode 中添加链接计数
    • 实现文件的硬链接创建和删除
  • 实现 fstat 系统调用:
    • 可以扩展 File trait 增加 stat 接口,为 Inode 添加 inode_id 字段以获取文件信息

另外需要注意文件系统中的锁使用,这里不支持重入,需要避免嵌套 lock 导致死锁

ch7 进程间通信

ch7 扩展了进程间通信机制,包括:

  • 管道(pipe)机制的实现,允许进程间通过管道进行数据传输
  • I/O 重定向功能,使进程能够重定向其输入输出流

这些功能为进程间的数据交换和通信提供了基础设施支持

ch8 并发

ch8 主要实现了线程管理和锁机制:

  • 线程管理:在 TaskControlBlock 中实现线程,与进程共用地址空间但有独立栈指针
  • 锁实现:包含两种锁机制:
    • MutexSpin:非阻塞锁,使用自旋等待和 yield
    • MutexBlocking:阻塞锁,维护等待队列进行线程阻塞和唤醒

Lab:

  • 实现死锁检测和避免,文档中给的是类似银行家的算法

总结

阶段 2 更多是读文档和熟悉 rCore 代码,实际开发工作量不高,每个实验基本不到百行。但实验文档写得非常详细,质量很高,每个设计也很不错

也体会到 Rust 在内核开发上,得益于强大的类型系统和零成本抽象设计,确实比 C 更加具有表现力,体验更好

第三阶段

三阶段是 ArceOS,包括六个实验

提供一个带颜色的 println! 宏即可,主要是熟悉 ArceOS 整体结构

support_hashmap

此时内核里已经有了 allocator,所以可以使用 alloc 里的 Vec 等集合(std ::vecalloc::vec 的 re-export),但 hash map 的 hash 函数依赖随机数生成器,这需要系统提供,所以不在 alloc 中,这里就需要我们添加 HashMap 支持

实现上可以用简单的多项式 hash 套 Vec<Vec<T>> 即可,代码百行左右,Trait 的设计可以参考标准库里的 HashHasher

alt_alloc

实现一个简单的 bump 内存分配器,同时作为字节分配器和页分配器。这个分配器很简单,只需要递增分配内存空间即可,甚至不用考虑回收实现

这里也可以看出 ArceOS 的可插拔设计能够很灵活地替换各个组件

ramfs_rename

为 ramfs 支持 rename 操作,这个地方比较坑的是跨 mount point 的处理有问题,这导致在 rename 时无法跨 mount point 操作,但涉及底下框架无法修改,只能实现成同目录下的操作

sys_map

实现 mmap,相比 rCore 的这回是可以映射到文件上了。但实现是让 mmap 时直接把文件内容写到内存里,Linux 里的 mmap 应该是一个特殊的缺页处理函数,只有当缺页时才会去读入这个文件内容

simple_hv

这里开始涉及虚拟化的内容,要给 vmm/hypervisor 添加 guest 的两个 trap 处理,一个是特权指令,一个是缺页。理论上这里应该要解析 guest 指令然后在 vmm 执行后再把结果设置回去,但实际上只要给 guest 寄存器写死固定值即可

总结

三阶段难度比预期要低,ArceOS 本身还是非常不错的项目,但实验的测试用例有点过于水了,导致很多不完善甚至错误的实现也能轻松通过测试。从教学角度个人感觉体验不如阶段二,但可以学习 ArceOS 本身的设计和代码,对比 rCore 的差异和改进