本文档主要对内核中的公共依赖库、公共模块进行介绍
内核库包含了内核功能无关的数据结构与函数,主要为标准C库、STL子集和我们扩充的部分。其中标准C库参考了Linux内核项目中nolibc的实现。STL部分区域赛后已改用EASTL。扩充部分主要是用于打印到string
的format
函数、环形缓冲区、带长度数组等数据结构。
为提高开发效率,区域赛后我们调研了开源STL库情况,移植了EA公司开发的EASTL项目至LuoOS内核。
采用该库的原因是,GCC及LLVM套件中的STL实现均与其libc绑定很深,且需要完整实现libc,而我们的内核中仅仅实现了所需的最小功能子集,难以支持;而该库不与特定平台绑定,特性强调可移植性,实际适配起来也的确相对可控。
适配中主要的难点在于:
- EASTL不支持RISC-V架构,对于其中硬件相关的部分如
chronos
时间库、atomic
原子操作等需自行实现。其中原子操作由于无其他依赖,可直接采用编译工具链中的STL<atomic>
- 其设计上仍是面向hosted环境,由于C++标准此前未考虑freestanding环境,因此仍需修剪去除对libc的依赖。修剪过程中又会导致其内部依赖的缺损,如
tuple
和C++17 structural binding语法的模板就经历了一段时间的梳理修复。 - C++运行时的依赖,如
cxa_
系列函数,这部分我们经过学习和观察,发现可以使用stub替代或使用标准库中的相关实现。 - LuoOS自身的依赖有些混乱,为了代码编写简便,我们此前倾向于在header中实现函数,导致移植过程中出现了
alloc->list->new->alloc
诸如此类的循环依赖,通过合适的拆分解决。
开发中我们发现,不同函数返回类型不同、返回值语义不同,不论是强行规定返回值为整型还是定义-1为异常,都难以自然表达错误处理逻辑。因此,我们还从C++20标准中导入了现代化的错误处理机制——std::expected
(采用其参考实现项目expected-lite),其类似于Rust中的Result类型,能以通用的方式封装及处理正常结果与异常结果。我们正在逐步完成对现有基于错误码等繁杂方式的代码的替换,但由于内核的功能需求众多尚未完全完成。例如下例所示:
Result<DERef> DEntry::entCreate(DERef self,string a_name, mode_t mode){
if(auto it=subs.find(a_name); it!=subs.end())
return make_unexpected(-1);
if(auto subnod=nod->mknod(a_name,mode)){
auto sub=make_shared<DEntry>(self,a_name,subnod);
subs[a_name]=weak_ptr<DEntry>(sub);
return sub;
}
return make_unexpected(-1);
}
虚拟文件系统重构过程中,我们发现采用原有的switch(type) case
或是主流内核中的struct ops
方式,实际上都是对虚函数繁杂的模拟,因此决定支持虚函数并使用其实现VFS。尽管业界一直有反对使用RTTI的声音,但至少在目前的开发中我们从中受益颇多。
与STL移植类似,此部分的支持也引入了新的语法结构对运行时的依赖,即RTTI(runtime type info)。同样的,经过学习和尝试,我们结合EASTL和标准库中的cxxabi
、typeinfo
等实现了适配。
在实现readv/writev
调用和Pager调页的过程中,我们发现系统中的各种IO都具有类似的性质,能够抽象出来,减少重复处理各种对齐问题的琐碎细节。
具体来说,系统中的IO操作,无论是设备到内存、内存到设备还是内存到内存,往往具有两个特性:非连续和非对齐。而分页式内存及设备的操作原语往往都是要求连续、对齐。因此,在此之间存在复杂的corner case处理,容易写出各种bug。
据此,我们抽象出了ScatterdIO
接口,其基本类似一个迭代器,拥有avail
方法用于检测是否能读写、next
方法用于将当前区间消耗一定长度并获取下一个最大连续可使用(地址)区间、contConsume
方法用于主动向给定的区间中写入内容。因而两个离散地址空间之间的拷贝,可采用统一的scatteredCopy
实现。
例如,readv/writev
函数可采用由内存区间vector<iovec>
构造出的用户内存读写对象UMemScatteredIO
与由文件构造出的对象进行拷贝实现,内存子系统从基于block cache的文件系统调页也可由内核内存对象与文件对象间进行拷贝。后续我们还会将基于block cache的文件系统磁盘读写转换为内存对象与设备DMA对象间的拷贝,实现类似于数据库中多层迭代器的统一操作(文件offset->文件cluster->块设备sector)。
在实现execve
中对用户栈的填充时,我们发现其与STL中的流式操作非常类似,因此将此前的VMAR::Writer
进一步改造为类流操作符。如
for(auto env:envs){
ustream<<env;
envps.push_back(ustream.addr());
}
ustream<<nullptr;
ustream<<envps;
ustream<<argv;
相比直接增加偏移、调用拷贝、读写地址等多步操作大大简化,这也是STL中流式IO的优势所在。
Logging对于调试是必不可少的,然而由于缺乏STL我们很难移植现有的日志库。最早我们通过DBG
宏开关与printf
简单实现日志打印,但随着模块增加单级日志难以控制,因此改为moduleLevel
(定义于每个文件控制该文件中输出门限)结合enableLevel
(全局日志启用门限)outputLevel
(控制日志器输出门限)的多级日志。当我们实现了区域赛大部分syscall时,出现了一些时间相关的上下文切换bug,若打印至uart会导致日志越详细运行越慢,难以复现bug。因此我们目前实现了将日志打印至内存中的ringbuf
,断点时查看全局的kLogger
对象即可查看当前日志。此外,bug往往在运行很长一段时间后才出现,因此我们将原有的outputLevel
宏改为全局变量enableLevel
以供动态调整。
目前为方便在panic时获取上下文状态,我们又在其中添加了kLogger.dump()
,以自动打印截至此时最新的若干条各级别日志。