2023秋冬季开源操作系统训练营第三阶段总结报告
前言
在训练营第三阶段, 我选择了Unikernel项目, 不觉间四周已过, 训练营也步入尾声, 遂做个总结.
Unikernel 项目学习总结
week1
输出有颜色的字符
查阅资料得知, 只要在字符串两侧包裹\u001b[<color>m和\u001b[0m就可以了
<color>为颜色数字, 比如红色为31\u001b[31mhello world!\u001b[0m即可输出红色的hello world!
支持 HashMap 数据类型
思路
- 一开始无从下手, 感谢老师在群里提示, 去看了标准库实现
- 将标准库代码复制过来, 全部注释
- 一点点放开注释, 缺啥补啥
Rust标准库的哈希表的一些具体内容
- 底层: 对
Google的C++哈希表的包装 new: 用参数hashbuilder生成一个哈希表, 默认为RandomStateRandomState: 结构体, 保存两个随机数, 实现了BuildHasher TraitBuildHasher: 可以根据key创建Hasher, 要求实现方法build_hasher() -> HasherHasher: 一个Trait, 代表一种哈希算法, 可以根据key(字节流)返回哈希值, 要求实现方法write()和finish()write(): 往Hasher里写keyfinish(): 结束写, 返回哈希值
build_hasher(): 生成Hasher, 默认的DefaultHasher是调用SipHasher13::new_with_key()生成的DefaultHasher: 结构体, 保存了一个SipHasher13SipHasher13::new_with_key(): 新建一个SipHasher13SipHasher13: 一个Hasher, 即一种哈希算法的实现
内存管理
实现内存分配算法Early
- 参考
TLSF的代码, 在其基础上修改 - 初始化页和字节分配器, 共用一块连续空间
- 字节分配指针从前往后, 页从后往前
- 指针相遇了就意味着内存耗尽
- 页分配器不回收释放的内存, 指针一直往前
- 字节分配器仅当所有分配的内存都释放了才回收
- 初始化一个计数器为 0
- 每次分配 +1, 释放 -1
- 如果计数为 0, 就把指针移回起点
dtb
解析dtb
- 群友有推荐使用
hermit-dtb进行解析 - 最后选择了名为
dtb的crate
解析后获取相应信息
- 遍历每个节点
- 每个节点的
reg属性都有4个值 - 其中第一个和第三个是起始地址和大小
- 目前还未找到相关规定, 姑且先当成结论
调度
将fifo改造成抢占式, 最简单的实现:
- 将
task_tick()的返回值从false改成true即可 - 类似于一个时片极短的
RR
week2
练习 1 & 2
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。
扩展 image 头结构,让 image 可以包含两个应用。打印出每一个应用的二进制代码。
实现
- 给每个
app的文件头都加上24字节的元信息, 如下- 魔数
app起始地址app大小
- 循环: 解析前
24字节, 如果发现是app, 就将其内容打印出来, 否则退出循环
反思
- 或许这样会更好: 学习
inode, 将每个文件的元信息统一放在image开头, 而不是每个文件的开头
练习 3
批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi
思路
- 从练习
2继续, 循环读取app的代码 - 每读取完一个
app, 就跳转到该app指令处执行 - 执行完毕后返回, 继续循环, 即读取下一个
app的代码
实现
- 尝试用
jalr跳到app的指令处运行, 但是失败 debug发现问题和jalr无关, 而是app执行完毕后, 没有返回- 观察实验代码, 发现
app的函数有noreturn的标志, 删去即可
练习 4
请实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机
照猫画虎, 模仿前几号系统调用实现3号即可
- 使用的退出函数为
arceos_api的ax_exit() axstd也有相关函数, 当时对arceos的结构了解尚浅
练习 5
把三个功能调用的汇编实现封装为函数, 基于 putchar 实现 puts 输出字符串。
思路
- 在实验中已经将
abi_table基址放在a7中传给了应用 - 应用用一个全局变量保存基址
- 每个函数根据该基址, 用汇编
jalr跳转到系统调用函数执行 puts即循环调用putchar
遇到的问题: 调用puts时死循环
- 观察
gdb, 发现在puts的最后, 执行ret无法返回, 而是停留在原地 - 检查发现, 原因:
riscv使用ra保存返回值, 但ra不是被调用者保存寄存器- 使用内联汇编跳转时, 编译器不会自动帮你加上保存
ra的指令 - 在
puts循环调用putchar的过程中,ra被修改, 所以无法正常返回
- 解决: 调用前将
ra入栈, 调用后再出栈
练习 6
实现一个应用,唯一功能是打印字符 ‘D’。 现在有两个应用,让它们分别有自己的地址空间。 让 loader 顺序加载、执行这两个应用。
顺序加载和执行
- 在练习
3已实现
地址空间的分配: 最简单的实现思路
- 实验过程中, 新建了一张页表, 映射虚拟地址
0x4000_0000开始的1G空间 - 应用起始地址为
0x4010_0000 - 既然应用是顺序执行的, 完全可以共用该页表
- 将第一个应用加载到
0x4010_0000处, 运行完毕后, 第二个应用直接覆盖上去即可
地址空间的分配: 加载全部应用后再执行的思路
- 额外建立一张页表给第二个应用
- 第一个应用映射
0x4000_0000到0x8000_0000, 大小1G - 第二个应用映射
0x4000_0000到0x8000_1000, 大小1G - 这要求第一个应用大小不能超过
1页, 不然会和第二个应用重合
遇到的问题
- 第二个应用无法映射成功, 每当访问该虚拟地址时均会产生
pagefault - 感谢老师和同学@王瑞康的帮助, 告诉我这是
riscv的规定sv39的页表机制要求, 如果使用一级页表, 那么地址必须1G对齐- 如果想实现上述的映射, 必须用到三级页表
- 将第二个应用的页表改成三级页表, 即可完成映射
感想与收获
对于arceos各个组件说的非常清晰
- 初看
arceos的代码时一团乱麻 - 看过
ppt之后才逐渐梳理清楚, 一图胜千言
第一次分析几千行的标准库代码
- 起初打算自己实现哈希表, 要是真这么做肯定费力不讨好
- 学会了如何借鉴和复用他人的代码
第一周的内容给我补了很多急缺的知识, 也加深了很多知识的理解
dtb的作用和结构- 现代内存分配算法
- 调度算法
- 页表机制
- …
总之, 十分感谢能有这次机会参加训练营, 此间经历, 此生难忘.

