-
Notifications
You must be signed in to change notification settings - Fork 438
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #211 from TheSayOL/master
summary for stage3
- Loading branch information
Showing
2 changed files
with
356 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
--- | ||
title: 2023开源操作系统训练营第三阶段项目一基本任务总结报告-TheSayOL | ||
date: 2023-12-01 18:52:58 | ||
categories: | ||
- report | ||
tags: | ||
- author:TheSayOL | ||
- repo:[<rcore-os-repo_you_worked_on>](https://github.com/TheSayOL/arceos) | ||
--- | ||
|
||
# 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`的作用和结构 | ||
- 现代内存分配算法 | ||
- 调度算法 | ||
- 页表机制 | ||
- ... | ||
|
||
总之, 十分感谢能有这次机会参加训练营, 此间经历, 此生难忘. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
--- | ||
title: 2023开源操作系统训练营第三阶段项目一最终任务总结报告-TheSayOL | ||
date: 2023-12-01 18:52:58 | ||
categories: | ||
- report | ||
tags: | ||
- author:TheSayOL | ||
- repo:[<rcore-os-repo_you_worked_on>](https://github.com/TheSayOL/arceos) | ||
--- | ||
|
||
# 2023秋冬季开源操作系统训练营第三阶段总结报告 | ||
|
||
## 前言 | ||
|
||
此为训练营第三阶段`Unikernel`项目最终任务的报告 | ||
- 描述了我对`arceos`支持`Linux`原始应用以及支持多应用的实现思路以及验证过程 | ||
|
||
|
||
# 思路 | ||
|
||
## 支持 Linux 原始应用 | ||
|
||
> 在思路一的基础上支持思路二 | ||
思路一 | ||
- 需要获得应用源码 | ||
- 将应用和`arceos`一起静态编译 | ||
|
||
思路二 | ||
- 应用不需要重新编译, 但必须动态编译 | ||
- 实现一个和`libc`接口一致的动态库 | ||
- 运行时, 使用一个动态链接器将应用重定位到该动态库 | ||
- 该动态库必须知道`arceos`系统调用函数的地址 | ||
- 动态库不使用中断, 而是用事先获得的地址, 直接跳转过去进行处理 | ||
- 细节参考第二周的练习`4` | ||
|
||
本思路 | ||
- 应用不需要重新编译, 但必须动态编译 | ||
- 实现一个和`libc`接口一致的库, 和一个动态链接器, 两者和`arceos`一起静态编译 | ||
- 如此一来, 库的内部实现可以直接调用`arceos`函数 | ||
- 应用装入内存时, 用动态链接器重定位到库函数 | ||
|
||
## 支持多应用 | ||
|
||
每个应用都有一个`arceos`的`TCB`, 在其中保存一些关键信息 | ||
- 应用有自己的内存空间, 所以需要保存自己的页表 | ||
- 应用可以创建线程, 所以需要保存子线程的`TCB` | ||
- 多应用情况下, 必须进行内存的管理, 所以还需要保存应用申请的物理页 | ||
|
||
|
||
# 原理和实现 | ||
|
||
## 支持 Linux 原始应用 | ||
|
||
### 实现接口一致的库 | ||
|
||
这部分很简单 | ||
- 对外接口和`libc`一致即可, 目的是让应用感知不到动态库被替换 | ||
- 内部实现随意, 只要达到对应功能即可 | ||
- 因为和`arceos`静态链接, 调用系统调用十分方便 | ||
- 具体实现可以参考或者直接使用`axlibc` | ||
- 也可以"翻译"`glibc`或`musl-libc` | ||
|
||
|
||
### 实现的库函数 | ||
|
||
> 参考 musl-libc 1.2.4 源码 | ||
对于一个`C`程序, 其启动流程如下 | ||
- 内核将一些信息放入栈中, 随后跳转到程序入口`_start` | ||
- `_start`, 将当前`sp`作为参数, 跳转到`_start_c` | ||
- `_start_c`从`sp`里解析出`argc`和`argv`, 跳转到`__libc_start_main` | ||
- `__libc_start_main`进行初始化工作, 完成后跳转到应用的`main` | ||
- `main`执行完毕, 回到`__libc_start_main` | ||
- `__libc_start_main`完成收尾工作 | ||
|
||
对于动态编译的程序 | ||
- 上述提高的函数里, 仅有`_start, _start_c, main` | ||
- 当然可能还有其他函数比如`_init, _fini`等, 这些往往作为参数交给`__libc_start_main`进行处理 | ||
- 而`__libc_start_main`本身是不包括在程序里的, 也就是说, 需要由我们实现 | ||
|
||
`__libc_start_main`的实现 | ||
- 对于一个`hello world`程序而言, `__libc_start_main`的实现十分简单 | ||
- 不需要进行初始化工作, 直接跳转到`main`即可 | ||
- 也不需要进行收尾工作, 直接`exit`即可 | ||
|
||
|
||
### 动态链接器 | ||
|
||
动态链接器自举 | ||
- 动态链接器装入内存的地址不确定, 需要自举, 即对自己重定位, 这是动态链接器最复杂的部分 | ||
- 自举时无法使用标准库功能, 甚至不能进行函数调用 | ||
- 但因为该动态链接器和`arceos`静态编译, 所以不需要自举 | ||
|
||
### 动态重定位 | ||
|
||
对于一个动态链接的应用 | ||
- 函数调用的地址在编译期无法确定, 需要在运行时查询`GOT`来确定 | ||
- `GOT`可以确定一个函数名到函数地址的映射 | ||
- 装入时重定位: 应用在装入时, 动态链接器修改`GOT`, 将函数地址改成正确对应地址 | ||
- 运行时重定位: 动态链接器将函数地址改成`dl_runtime_resolve()`的地址, 仅当应用调用函数的时候才修改`GOT`为正确地址 | ||
|
||
本实现里, 使用装入时重定位 | ||
- 应用装入内存时, 修改`GOT`, 改成上文的"实现接口一致的库"的对应函数的地址 | ||
- 如此一来, 应用调用库函数便会跳转到期望的地址执行 | ||
|
||
|
||
|
||
## 支持多应用 | ||
|
||
|
||
### 内存管理 | ||
|
||
`arceos`的设计 | ||
- 只支持单应用, 内核以库的形式存在, 即`libos` | ||
- 应用拥有所有内存空间, 随意使用 | ||
|
||
为了支持多应用, 需要对应用进行内存管理 | ||
- `TCB`里使用一个数组保存应用使用的物理内存 | ||
- 当应用退出时, 回收所申请的内存 | ||
|
||
|
||
### 应用独立地址空间 | ||
|
||
`arceos`的设计 | ||
- 只支持单应用, 因此不需要进行页表切换 | ||
- 内核初始化时初始一张页表, 映射内核的数据 | ||
- 将该页表作为根页表写入页表寄存器, 之后不再改动 | ||
|
||
为了支持多应用 | ||
- 每个应用在建立的时候, 都为之新建一张页表, 保存在`TCB`中 | ||
- 页表初始化时, 也需要映射内核的数据 | ||
- 对于`sv39`, 地址空间分为高`256G`和低`256G` | ||
- `arceos`在完成初始化工作之后, 将自己的运行地址设置在了`256G` | ||
- 因此应用运行在低`256G`即可, 高`256G`留给内核运行 | ||
- 设计应用二进制数据的起始地址为`0x10000`, 重定位完毕之后, 将数据写入该地址 | ||
- 申请应用和数据大小一致的物理内存 | ||
- 将应用数据写入该物理内存 | ||
- 在页表上将`0x10000`映射到该内存地址 | ||
|
||
支持页表切换 | ||
- 调度时, 需要将切换页表, 将地址空间从上一个应用切换到下一个 | ||
- 系统初始化创建`main`进程运行, 但是此时没有发生调度, 因此需要手动将页表切换成`main`的页表 | ||
|
||
|
||
### 将来的改进 | ||
|
||
完善库函数 | ||
- 完善`__libc_start_main`, 初始化和收尾工作必须符合应用期望 | ||
- 可以直接用成熟的`__libc_start_main`实现 | ||
- 也可以直接"翻译"一个`libc`中的相关实现 | ||
|
||
支持运行时重定位 | ||
- 目前支持装入时重定位, 在面对复杂应用时, 重定位工作很耗时间 | ||
- 可以进一步改为运行时重定位, 减少应用启动时间 | ||
|
||
支持应用创建子线程 | ||
- 可以在`tcb`中保存一个`pid` | ||
- 创建子线程时, 将`pid`设置为一致, 表示属于同一个进程 | ||
|
||
|
||
# 验证 | ||
|
||
过程 | ||
- 下载`riscv`版本的`Ubuntu`, 使用`qemu`安装并运行 | ||
- 在其中下载`musl-libc`源码, 编译 | ||
- 创建两个`hello world`应用, 使用`musl-gcc`编译 | ||
- 复制到`arceos`项目文件中, 运行`arceos`, 成功 |