1. Starry
Starry是2023年全国大学生计算机系统能力大赛操作系统设计赛-内核实现赛的二等奖作品。Starry是在组件化OS的arceos的基础上,进行二次开发的操作系统内核,使用宏内核架构,能够运行Linux应用的内核。
原始的操作系统大赛的仓库为
https://gitlab.eduxiji.net/202310003101649/starry
目前的宏内核的arceos的开发仓库
https://github.com/Arceos-monolithic (推荐)
(目前仅仅建议使用较为稳定的riscv64架构)
2. Starry 宏内核实现
2.1 axmem 内存管理
宏内核相比于unikernel主要差别在于多进程和多个地址空间。axmem模块负责进程地址空间的管理更确切的说是虚拟地址空间的管理。
axmem模块依赖于例如page_table和page_table_entry,slab_allocator等其他crate。先回顾一下基本的页表相关的内容页表相当于一棵树Satp是作为页表的根。
然后是具体sv39模式下的分页
总而言之,从页表中,可以获取到对应的一个具体虚拟地址页到一个物理地址页的映射但是,对于一个进程来说,同样需要知道他拥有了哪些虚拟地址空间。
首先,操作系统不会把所有空间都分配给某个进程,而是把该进程用到的空间都分配给他。其次,举个例子,在结束进程的时候,需要清理该进程所拥有的进程地址空间,所以必须进行一个记录和维护。
进程虚拟地址空间的特点:拥有多个不一定连续的段(并不是实际意义上的代码段啥的,只是类比,类比)每个段具有多个虚拟空间的页。
因此Starry用了两个数据结构来表示这个MapArea用于表示一段空间,其中维护了起始地址,对应的物理页面信息等等。分别放在 axmem/src/lib.rs 和axmem/src/area.rs中。
当数据结构确定了之后,相应的操作其实都很好理解了,无非就是增删查改。(请注意这两个数据结构的调用关系,比如下图中上层memoryset需要增加一个area的时候,就要调用下层的一个接口)当然还有剩下两个文件,分别是用于描述shared mem和文件系统映射到内存的问题。但是这两个对于理解地址空间这件事情并没有太大影响。
除此之外,值得关注的还有更改pagetable的satp寄存器内容来切换页表的操作值得留意,和前面介绍的内容不同,这部分并没有放在axmem中,而是具体到不同架构中,放在axhal/src/arch里面对应架构的mod中。
2.2 axtask 任务管理
Axtask是在之前arceos里面就有的一个模块,但是为了能够适应多进程多地址空间,做了很多的更改。
从启动开始看一个新的task是怎么生成的最开始是汇编代码,在axhal/platform对应的platform文件夹下面的platform下面,进入rust_entry函数。
在处理了一大堆前置的东西之后,在rust_entry里面终于进入了rust_main函数。
在rust_main函数里面,终于开始了喜闻乐见的往屏幕上打印一些基础信息的阶段,在之后,终于开始了一大堆东西的初始化。
在这里,我们可以看到这么一段代码这段代码的含义是,如果是多进程的内核,就启动多进程,而如果是有多线程的要求,那么启动多线程的。
但是多进程也会有多线程的呀,那么不要灰心,进入init_kernel_processer函数仍然需要调用axtask的init_scheduler,至于其他的内容则是初始化一个内核进程的内容罢了。
来看看init_scheduler做了什么实质性的内容,只有run_queue的init(虽然timer也很重要,但是跟我们的多进程多线程没啥关系)
在这里,你可以看到一个idle的task以及一个main_task。并且在最后,把current task设置为了main task请注意idle和main的差别,当前的代码执行流应当归属于main task,因此idle是一个新建的,然后继续运行main。
除此之外,和一个task相关的数据结构则放在task.rs文件中。
2.3 axprocess 进程管理
axprocess是增加的一个模块。该模块实现了进程这个概念。核心主模块在process.rs文件中。
一个很大的不同点在于:axtask和axprocess分别相当于线程和进程,因此,与很多只能看到内核进程的操作系统不同,在Starry中,是能够同时看到进程和进程中的线程的。
对于一个进程而言需要管理的资源如下图所示:
- 子进程
- 所拥有的线程
- 文件描述符
- …
还是回到前面的分支,多任务和多进程的区分之处,这回,来看看除了init_scheduler生成了idle和main的任务之外,还做了什么。
他初始化了一个process结构体,并且把对应的IDLE的task塞进去了。
思考一下:main task咋办?他没被塞到任何进程里面去
在rust_main里面process或者task init之后,再经过了其他的init,最后会运行到main。
而这部分,会最终运行到apps里面的用户态函数入口。
在之前我们讲到测例的这个run testcase的这个loop的时候,来注意一下这里面代码的main_task。
最后来考虑一下进程的切换。在这里他调用了一个yield now task。
在axprocess/api.rs中借用了axtask的yield_now。这又调用了run_queue的yield_current,最终调用runqueue的一个resched
随后是run_queue的switch_to函数,他在这里,对于切换不同的进程地址空间,使用了如下的代码(又回到了熟悉的函数),来切换不同的页表基地址寄存器。
最后会运行到context_switch,从而使用汇编,保存旧的任务的寄存器值,加载新的寄存器的值。