Skip to content

Commit

Permalink
Merge pull request #211 from TheSayOL/master
Browse files Browse the repository at this point in the history
summary for stage3
  • Loading branch information
ZhiyuanSue authored Dec 2, 2023
2 parents f8cb0ec + a674338 commit 8871a28
Show file tree
Hide file tree
Showing 2 changed files with 356 additions and 0 deletions.
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`的作用和结构
- 现代内存分配算法
- 调度算法
- 页表机制
- ...

总之, 十分感谢能有这次机会参加训练营, 此间经历, 此生难忘.
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`, 成功

0 comments on commit 8871a28

Please sign in to comment.