宏内核
|
宏内核模式相对unikernel模式特点
|
|
如何以Unikernel为基础,构建最小化宏内核
用户地址空间,建立用户stack,分出来code area,能加载外部的app binary
|
在主task来创建child task
|
内核和用户通信通过syscalls,trap
|
|
|
需要
建立用户地址空间,对指定区域进行映射,能指定属性
|
添加系统调用到异常中断
|
能复用调度,
|
切换switch
|
|
|
|
实现过程
创建用户地址空间
|
需要用户stack和应用代码区域
|
伪造一个返回应用的上下文现场
|
就是一组寄存器状态
|
假装我们之前从应用来的
|
保存在内核栈上
|
内核栈属于新创建的任务
|
新任务运行,运行下sret
|
会从伪造现场里恢复寄存器状态
|
完成特权级切换
|
就可以从内核态返回用户态
|
首次启动用户态应用,开始执行用户代码
|
origin直接sys_exit
|
|
|
|
上下文
任务上下文/进程上下文/用户进程上下文
|
运行用户应用正常逻辑
|
只要不涉及到更高级的资源申请,或者缺页异常,也需要进到内核态,就在这个上下文运行,就是没有中断打断
|
|
异常上下文
|
上面需要syscall或者中断打断,或者异常处理
|
进来
|
|
中断上下文,图中没画
|
中断处理优先级>异常处理优先级
|
处理时间不能长,要尽快返回所以对锁同步机制有要求
|
比异常要求严格
|
|
|
|
创建用户地址空间address space,axmm
把用户应用加载进来
|
加载进user地址空间的相应segment里去
|
怎么通过axfs来遍历文件系统,加载应用
|
|
初始化用户stack
|
|
创建用户task
|
执行到最后要从用户态切换到内核态
|
通过axtask,需要实现taskext,任务实现的扩展
|
|
join等待task完成
|
|
退出
|
|
|
|
用户空间
sv39,高低两部分
|
高端内核空间,低端用户应用空间
|
初始化的时候有一个内核根页表,中间的那个图
|
低端保持空,高端是内核空间
|
创建用户根页表,使用复制的方式
|
不管复制多少次,用户也和内核页,内容一样
|
低端是应用空间
|
一开始空,加载应用代码,数据,会填充
|
|
|
实现对应关系
左边,sv39的高端低端,地址两个
|
|
跟linux完全一致
|
|
用户空间,内核空间都用了一个AddrSpace来表示
|
可见范围
|
|
区域列表
|
出现了MemorySet
|
管理的页表
|
|
|
用户address space,虽然kernel也复制过去了,但用户不可见
|
|
范围只在蓝色区域内,应用通过系统调用或者异常,可以访问高端那一部分
|
|
页表不需要切换,因为自带,但会占用更多空间,这个机制是和linux兼容的
|
|
|
|
用于应用的编译链接
目前是rust工具链+rust嵌入式汇编
|
ecall能从用户特权级跳到kernel特权级
|
现在不支持elf,所以需要转成二进制
|
|
|
应用安装到根文件系统里
make disk_img已经创建磁盘设备disk.img
|
建立了文件系统fat32
|
安装用户应用是mount该磁盘设备文件到./mnt目录
|
更新应用程序image
|
|
|
应用加载到用户地址空间
将用户img从磁盘上加载到内存里
|
|
第一步把image从磁盘离读出来,存到临时缓冲区里,可栈可堆
|
|
然后往地址空间里写
|
|
还没有启动应用,需要kernel替应用把地址空间准备好
|
|
所以需要在用户地址空间里完成一个映射
|
能跟物理页帧形成一个映射关系
|
内核地址空间可以访问所有物理页帧,通过偏移访问
|
|
在内核,将应用代码从缓冲区拷过来
|
|
|
|
创建用户任务
ext对应不同模式有不同扩展
|
|
unikernel基本是null
|
|
宏内核需要加以下属性
|
proc_id
|
pid
|
uctx
|
上下文
|
aspace
|
用户地址空间
|
|
通过一个宏def_task_ext!注册
|
|
|
|
因为从调度上来看,大家几乎一致,不管unikernel,宏内核,还是hypvider
|
主要区别在资源相关属性,跟调度没什么关系
|
|
|
|
|
宏内核,需要以进程为单位管理和隔离资源,每个task有自己的资源引用
|
|
|
尽量保证调度子系统通用
|
|
|
|
进入用户态,让出CPU,kick off用户任务
一般没有专门指令从内核态到用户态
|
伪装一个异常上下文现场,假装来自用户态,用sret指令返回去
|
比如假装一个用户应用运行,发生了异常,进入内核态
|
然后保存用户应用的通用寄存器,控制寄存器状态,保存在用户栈的区域里,叫做保存现场的栈帧
|
|
保存三个比较重要
|
sp
|
应用的栈
|
epc
|
触发异常的时候应用指令寄存器当前执行到的位置
|
status
|
发生异常时的状态
|
|
|
这个栈帧做好了,执行sret的时候,会反过来,按照刚才保存的顺序,恢复出去,再从内核态切换到用户态
|
sepc
|
直接写入的是write应用的入口,sret后就会从应用入口处开始执行
|
csrw sstatus,
|
保存状态,表示上次是从用户态来的,好正常返回用户态
|
LDR sp, sp, 1
|
保存栈的地址
|
gp和tp
|
在支持linux的时候有特殊用途
|
tp
|
用来存任务指针
|
gp
|
跟一些什么cpu没听清是相关的,跟relaxed机制的相对偏移有关
|
|
这些都需要保存好
|
|
|
前面都是启动过程
应用里只有一个exit系统调用要回到kernel里,需要一个响应函数,就是handle_syscall
|
riscv_trap_handler是异常中断向量表的入口函数,根据原因cause,注册相应的函数
|
现在只实现了SYS_EXIT,直接退出就行了
|
|
|
宏内核地址空间管理,包含哪些对象
首先AddrSpace地址空间
|
管理一系列有序区域,并对应一个页表
|
|
MemorySet是对BTreeMap的简单封装
|
对MemoryArea进行有序管理
|
对应一个连续的虚拟地址内存区域
|
|
又对应一个backend
|
|
|
|
|
并管理页表
|
|
|
|
|
|
MemorySet
表示一系列area,按地址有序排列
|
对地址空间的主要操作就是查找目标area或者查找area之间的空隙
|
有序管理能够快速找到目标区间和之间的空隙
|
|
映射的新的地址空间可能与原有空间重叠
|
发生重叠的会先被unmap掉
|
取消掉后,对新的区域进行映射
|
|
还有新区域在旧区域大的区域中间
|
旧的会被unmap,因而发生了切割
|
|
是在使用mmap和munmap的时候经常处理的情况
|
|
|
跟区域有关的是backend
实现是一个trait
|
|
线性映射
|
需要物理有连续的空间存在
|
在虚拟空间对它做直接映射
|
映射直接有一个偏移必须是一个常数
|
unikernel第一次切换采用的方式一样
|
常用在设备MMIO区域进行映射
|
或者内核,应用之间共享内存映射的时候
|
对应的物理页帧必须连续
|
|
alloc
|
更常用
|
如果没有指定polulate,只建立空映射
|
真正访问的时候触发缺页异常
|
缺页异常内,完成物理页帧的申请,补齐映射
|
也是Lazy方式
|
页帧可能分散,按页映射
|
|
|
|
缺页异常响应函数实现
通过handle_page_fault
|
会调用一连串的handle_page_fault
|
申请物理页帧
|
将物理页帧映射到缺页的虚拟地址上
|
|
|
系统调用sys_mmap实现
跟缺页异常关系密切的系统调用mmap
|
|
触发原因不同,这个是系统调用
|
|
跟缺页异常逻辑相似,但可以传populate参数
|
|
true的时候,类似,申请物理页帧,并映射到虚拟地址
|
|
如果false,建立空的页表项,等pagefault
|
省内存,但效率低
|
|
|
支持原生linux应用
因为glibc比较复杂,所以实验都基于musl-libc来编译
|
开始采用elf格式应用
|
启动原始linux应用hello
|
|
|
具体要解决的问题
兼容界面
syscall
|
|
procfs & sysfs伪文件系统
|
查询状态
|
一些假定=_=
|
awareness of aspace
|
|
|
|
elf格式
有一个布局的概念
|
可以通过命令查看布局
|
entry point,应用的入口在哪里,编译不同应用不同,非0地址
|
两个段,代码段,用户段,LOAD代表这俩段需要加载进内存里
|
通过flg判断,RE是读执行,代码段,RW是数据段
|
|
|
elf的具体加载
|
|
elf采取紧凑存储的方式
|
加载到虚拟机空间需要展开
|
要符合对齐之类的要求
|
|
一般可读可写数据段会包含两个部分
data段
|
都是有初值的一些变量
|
bss
|
初始化为0的变量,比如static变量属于这里
|
由于全0不需要存所以文件里没有bss,只会标记
|
|
|
所以filesiz包含的时候有初值的data段的内容
|
memsize包含data段+bss段的长度
|
|
由于bss没有第三方加载器,内核就是加载器,所以清零操作得自己做
|
|
|
应用用户栈初始化
linux都是跟glibc/musl-libc联编译形成的
|
|
|
除了应用本身的部分,还有附加部分,会对栈上的数据进行检查和访问
|
切换用户之前,需要准备这些内容
|
满足libc库的需要
|
main的最全形式见图
|
最顶端是0
|
文件名
|
环境变量
|
参数
|
随机数码,auxv使用(叫什么辅助向量,没细讲)可以没有,如果没有,退回低级形式
|
|
|
下面对应main的那3个参数
|
arg_ptr存的是指针,arg存的事参数的值
|
|
最后需要把最后一个低地址赋给用户栈sp
|
sp,对于x86架构,赋给栈的sp要求16字节对齐
|
x86指定了什么sse,有向量指定对齐的要求
|
|
|
|
|
支持系统调用的层次结构
与unikernel大致
|
使用linkme从trap到m_X_0里注册了系统调用入口
|
posix,是参照posix封装的api接口
|
|
|
是musl以静态方式编译的
|
动态会比较麻烦
|
启动的时候需要先加载ld动态库
|
以它为解析器,把响应动态库给依次加载进来
|
|
把libc库和应用,编译到一块
|
多了个准备部分_start
|
完成一些预备工作
|
比如检查栈上的数据
|
环境变量参数传给main
|
|
_exit工具链附加
|
|
|
要想把musl编译的hello运行起来需要4个系统调用
|
set_tid_address会设置clear tid的指针
|
linux宏内核进程上有两个相关变量
|
一个set_tid指针,clear_tid指针
|
set跟线程启动相关
|
主线程创建子线程,子线程把tid记到set tid里,别人能知道新线程的tid是什么
|
clear tid是线程要释放,或者自己要退出,比如释放锁mutex会把自己返回值写到clear_tid
|
|
ioctl
|
|
writev
|
write变形
|
用来写一组字符串序列
|
两个,一个是那句话,一个是换行符
|
只有这个系统调用响应的事应用main里对应的系统调用,前面的那俩96和29是准备用的
|
94是清理用的
|
|
exit group
|
会让线程所在的组的所有线程,都退出
|
|
x86的系统调用号与剩下的不同
|
|
|
|
常用文件系统支持
|
|
procFS, sysfs, devfs
|
伪文件系统
|
背后不是数据块,是设备或者某一种对象的数据
|
|
procfs,内核和进程信息的接口,挂在/proc下,能像访问文件一样访问当前进程的相关数据
|
|
类似的sysfs,想取代devfs对于设备管理混乱的问题,devfs依然存在为了保持兼容性而存在
|
|
procfs和sysfs基于ramfs,内存文件系统来实现,访问一个基于内存的文件节点
|
|
这是个临时机制,linux方式不应该是一个缓存作用的数据区,应该是个回调函数的注册
|
|
比如访问procfs下面的某个文件,会调用一个函数,区取内存的当前信息或某个进程的当前信息
|
|
Devfs需要实现dev下的none和zero,专门组建axfs_devfs
|
|
|
|
异构内核
|
|
什么东西放到backbone
调度,trap
|
容易复用
|
地址空间管理axmm模块
|
|
|
如何不同架构兼容
主要是task
|
利用task ext扩展
|
利用def_task_ext注册
|
|
task扩展实现原理
|
编译器确定扩展域大小
|
在堆上申请内存
|
将扩展域指针指向该内存
|
|
|
|
|
Generated
2025-05-05 04:17:35 +0000
25bcfca-dirty
2025-05-04 14:47:56 +0000