2023秋冬季开源操作系统训练营第三阶段总结报告
前言
在训练营第三阶段, 我选择了Unikernel
项目, 不觉间四周已过, 训练营也步入尾声, 遂做个总结.
Unikernel 项目学习总结
week1
输出有颜色的字符
查阅资料得知, 只要在字符串两侧包裹\u001b[<color>m
和\u001b[0m
就可以了
<color>
为颜色数字, 比如红色为31
\u001b[31mhello world!\u001b[0m
即可输出红色的hello world!
支持 HashMap 数据类型
思路
- 一开始无从下手, 感谢老师在群里提示, 去看了标准库实现
- 将标准库代码复制过来, 全部注释
- 一点点放开注释, 缺啥补啥
Rust
标准库的哈希表的一些具体内容
- 底层: 对
Google
的C++
哈希表的包装 new
: 用参数hashbuilder
生成一个哈希表, 默认为RandomState
RandomState
: 结构体, 保存两个随机数, 实现了BuildHasher Trait
BuildHasher
: 可以根据key
创建Hasher
, 要求实现方法build_hasher() -> Hasher
Hasher
: 一个Trait
, 代表一种哈希算法, 可以根据key
(字节流)返回哈希值, 要求实现方法write()
和finish()
write()
: 往Hasher
里写key
finish()
: 结束写, 返回哈希值
build_hasher()
: 生成Hasher
, 默认的DefaultHasher
是调用SipHasher13::new_with_key()
生成的DefaultHasher
: 结构体, 保存了一个SipHasher13
SipHasher13::new_with_key()
: 新建一个SipHasher13
SipHasher13
: 一个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
的作用和结构- 现代内存分配算法
- 调度算法
- 页表机制
- …
总之, 十分感谢能有这次机会参加训练营, 此间经历, 此生难忘.