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
, 成功