Skip to content

Latest commit

 

History

History
74 lines (51 loc) · 6.36 KB

基础设施及杂项.md

File metadata and controls

74 lines (51 loc) · 6.36 KB

基础设施及杂项

本文档主要对内核中的公共依赖库、公共模块进行介绍

基础设施

内核库

内核库包含了内核功能无关的数据结构与函数,主要为标准C库、STL子集和我们扩充的部分。其中标准C库参考了Linux内核项目中nolibc的实现。STL部分区域赛后已改用EASTL。扩充部分主要是用于打印到stringformat函数、环形缓冲区、带长度数组等数据结构。

为提高开发效率,区域赛后我们调研了开源STL库情况,移植了EA公司开发的EASTL项目至LuoOS内核。

采用该库的原因是,GCC及LLVM套件中的STL实现均与其libc绑定很深,且需要完整实现libc,而我们的内核中仅仅实现了所需的最小功能子集,难以支持;而该库不与特定平台绑定,特性强调可移植性,实际适配起来也的确相对可控。

适配中主要的难点在于:

  1. EASTL不支持RISC-V架构,对于其中硬件相关的部分如chronos时间库、atomic原子操作等需自行实现。其中原子操作由于无其他依赖,可直接采用编译工具链中的STL <atomic>
  2. 其设计上仍是面向hosted环境,由于C++标准此前未考虑freestanding环境,因此仍需修剪去除对libc的依赖。修剪过程中又会导致其内部依赖的缺损,如tuple和C++17 structural binding语法的模板就经历了一段时间的梳理修复。
  3. C++运行时的依赖,如cxa_系列函数,这部分我们经过学习和观察,发现可以使用stub替代或使用标准库中的相关实现。
  4. LuoOS自身的依赖有些混乱,为了代码编写简便,我们此前倾向于在header中实现函数,导致移植过程中出现了alloc->list->new->alloc诸如此类的循环依赖,通过合适的拆分解决。

std::expected<> 错误处理机制

开发中我们发现,不同函数返回类型不同、返回值语义不同,不论是强行规定返回值为整型还是定义-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和标准库中的cxxabitypeinfo等实现了适配。

IO易用性

Scattered IO

在实现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)。

Stream IO

在实现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(),以自动打印截至此时最新的若干条各级别日志。