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进行初始化工作, 完成后跳转到应用的mainmain执行完毕, 回到__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, 成功