宏内核
宏内核模式相对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
主要区别在资源相关属性,跟调度没什么关系
unikernel,资源全局
宏内核,需要以进程为单位管理和隔离资源,每个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
对应两种方式
线性映射
alloc,用到的时候才会触发
并管理页表
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联编译形成的
TODO
联编是啥
除了应用本身的部分,还有附加部分,会对栈上的数据进行检查和访问
切换用户之前,需要准备这些内容
满足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