面向对象的Rust微内核介绍

电子说

1.3w人已加入

描述

前面介绍的rCore操作系统:rCore入门-来自清华的OS前沿教程,是清华的教学OS,相当于使用Rust语言山寨了下Linux,是一个宏内核

之前我有一篇文章介绍了微内核:seL4微内核入门-微内核介绍,相对来说微内核在学术上更严谨先进一些,尽管性能不如宏内核,其他方面有点多,特别是安全性

所以清华又搞了一个zCore思路还是山寨,这次瞄上了谷歌Fuchsia的Zicron微内核,见之前的文章Fuchsia入门-简介和代码介绍,总结下:

rCore:使用Rust语言山寨宏内核Linux

zCore:使用Rust语言山寨微内核Zicron

注意:这里说的山寨不是完全参考,不同语言也全参考不了,实现方式有很多不同之处。

上图是王润基同学恶搞了下谷歌Fuchsia的发布,改成自己的了真是新iPhone还没出来山寨就有了。    

总结下,这些OS其实都有一个技术指向:Rust+微内核

谷歌的Fuchsia比较难产,所以逼急了又推出来一个KataOS,见之前文章:KataOS入门-简介和代码编译,直接用现成的seL4微内核+Rust应用框架Sparrow搞了。    

回归正题,

1. zCore简介

GDB调试

zCore 是用 Rust 语言重新实现的 Zircon 微内核

它运行在内核态,对外提供与 Zircon 完全相同的系统调用,因此能够运行原生的 Fuchsia 用户程序

之前的文章介绍过Fuchsia的强大,其应用程序更是兼容了安卓,另外对于微内核驱动也是应用,也可以应用Fuchsia强大的驱动程序。可见这个zCore的巨大应用价值。  

2. 面向对象的内核

GDB调试

Zicron是用c++写的,更适合用Rust重写。有一个问题:什么时候需要面向对象?

假如世界上只有一个上帝,那就不需要面向对象。但是世界上还有几亿的人,人都有非常多的共性,那答案就是有很多个实例的时候就需要面向对象。

回到内核,第一直觉是进程,这东西是多个的,特别是用户进程,这必须可以面向对象啊,然后IPC通信,有很多进程直接又很多个通道通信,很多个就又可以面向对象。

凡是可以有多个的东西,都可以面向对象。    

在c语言编写的内核或者宏内核中,经常用到抽象出来的结构体,多个的表达就是结构体数组或者结构体链表,其辅助处理函数都要自己写,例如查找插入改变值等,很繁琐

然后所有的东西都往进程结构体PCB里面塞,很多机制理解起来很费力,要去看那个很大的PCB结构体去理解,一下就懵了

人还是容易理解抽象出来的事物然后加上简单的思维逻辑,不抽象的混乱思维一会就晕了。   

  Zircon 是一个基于对象的内核

下面带你领略下面向对象的魅力,真是轻松学内核的法宝。先来看一个zCore内核运行时组件层次框架图:

GDB调试     

大家知道一般程序处理的流程:用户程序-》系统调用-》内核-》硬件里面去处理。

上图中红框里面就是把内核里面跟硬件无关的元素抽象出来,用面向对象的方法表示出来。

例如系统里面有多个Process,一个Process实例里面有很多个Handle和Rights指向其他的对象。

GDB调试

内核对象相关的三个重要概念:对象(Object),句柄(Handle),权限(Rights)。

对象(Object)具备属性和行为的客体。客体之间可有各种联系。从简单的整数到复杂的操作系统进程等都可看做对象,它不仅仅表示具体的事物,还能表示抽象的规则、计划或事件。

句柄(Handle)标识对象的符号,也可看成是一种指向对象的变量(也可称为标识符、引用、ID等)。

权限(Rights):是指对象的访问者被允许在对象上执行的操作,即对象的访问权限。当对象访问者打开对象的句柄,该句柄具有对其对象的访问权限的某种组合。

说下我的理解,内核的资源都被抽象成对象,对象之间用句柄产生联系,这种联系为了安全性有权限限制,当没有句柄指向的对象也就是没用了会被收回。    

用户程序操作内核对象的一些细节:

创建:每一种内核对象都存在一个系统调用来创建它,例如 zx_channel_create。创建对象时一般需要传入一个参数选项 options,若创建成功则内核会将一个新句柄写入用户指定的内存中。

使用:获得对象句柄后可以通过若干系统调用对它进行操作,例如 zx_channel_write。这类系统调用一般需要传入句柄 handle 作为第一个参数,内核首先对其进行检查,如果句柄非法或者对象类型与系统调用不匹配就会报错。接下来内核会检查句柄的权限是否满足操作的要求,例如 write 操作一般要求句柄具有 WRITE 权限,如果权限不满足就会继续报错。

关闭:当用户程序不再使用对象时,会调用 zx_handle_close 关闭句柄。当用户进程退出时,仍处于打开状态的句柄也都会自动关闭。

总之,面向对象的代码很清晰,很容易理解,不绕弯子,利用面向对象的语法,代码行数能大大缩减,预计5倍以上。上面的介绍大部分都是zCore教程里面的,这个教程更加的干货,直接介绍核心概念,对OS基础知识进行了省略。同rCore教程一样也是从零开始自己写一个OS的教程,这个感觉做的更好。

GDB调试

3. rCore整体架构

zCore的设计主要有两个出发点:

内核对象的封装:将内核对象代码封装为一个库,保证可重用

硬件接口的设计:使硬件与内核对象的设计相对独立,只向上提供统一、抽象的API接口

项目设计从上到下,上层更远离硬件,下层更接近硬件,架构如下图所示:

GDB调试

为了让 zCore 能够同时运行在内核态和用户态,我们在最下面设计了一个硬件抽象层(HAL),将内核所依赖的底层操作封装起来,在裸机环境和 Linux/macOS 环境上分别提供不同的实现。 在 HAL 之上的核心是zircon-object,也就是 Zircon 内核对象,这里面包含了所有内核机制的实现。

在对象层之上是系统调用层,它负责将内核对象的功能封装成 Zircon syscall ABI 暴露给用户进程。 再往上就是整个 OS 的顶层模块,它负责完成系统初始化和加载第一个用户进程的工作,并将所有模块组装到一起,生成一个可执行文件。 zCore 设计的顶层是上层操作系统,比如 zCore、rCore、ZirconLibOS 和 Linux LibOS。

在项目架构中,各版本的操作系统有部分公用代码。与 zCore 微内核设计实现相关的部分则主要是图中左侧蓝色线部分。     

乍一看,真是四不像,有点像嫁接,共用一个根,嫁接上不同的枝条,枝条上可以结出不同的水果。感觉这就是程序的高级玩法,殊途同归,万法归一,还能相互转化,实在是高,万能工具啊。好处是,这几种OS上的应用都可以在zCore上运行,跟吃了几种水果一样甜啊。

4. OS界的变形金刚

不仅如此,它还可以作为一个普通的用户进程运行在 Linux 或 macOS 的用户态,我们一般把这种模式称为 LibOS 或 User-Mode OS。你甚至无需安装 QEMU 模拟器,只需装上 Rust 官方工具链,就可以编译运行体验 zCore!  

 

git clone https://github.com/rcore-os/zCore --recursive
cd zCore
git lfs pull
cargo run --release -p zircon-loader prebuilt/zircon
既然可以用户态运行,那么它其实就是一个普通的用户程序。这带来了巨大的好处:我们可以在用户态开发,用 gdb 配合 IDE 调试,用cargo test 跑单元测试,统计测试覆盖率……这在之前的内核开发中是难以想象的。

GDB调试    

zCore 作为 rCore 的继承者,它并没有把前辈丢掉。事实上,zCore 并不是一个独立的 OS,在它的仓库里还藏着一个小 rCore!只需使用以下命令,即可快速把它召唤出来,我们来运行一个原生 Linux 程序——Busybox:  
make rootfs
cargo run --release -p linux-loader /bin/busybox
  GDB调试     

这里面的奥秘在于,Zircon 作为微内核,其实已经提供了内核中最关键的内存管理和进程管理的功能。

我们只需在它基础上补充 Linux 作为宏内核的其它功能(例如文件系统),并对外提供 Linux 系统调用接口,即可重新构造出一个新的 rCore。

这就是微内核运行宏内核程序的关键,缺的东西再加一层壳子,再封装一层系统调用。

 

5. 编程语言分析

GDB调试

官方 Zircon 是用 C++ 语言编写的,代码量约有 10w 行。而 zCore 只用了1w 行 Rust 就实现了其中大部分核心功能。虽然我们还差一些没有实现,但相差一个数量级的规模还是让我感到有些诧异。

不过至少据我观察,C++ 的 Zircon 代码从设计上就比较复杂,用了各种自己造的轮子,并且充斥着魔法操作。相比之下,Rust 的 zCore 代码看起来更加自然,核心库自带的基础设施再加上一些社区库的辅助,用起来还是非常舒服的。     

关于 Rust 大家更关心的另一个话题是 unsafe。在 zCore 中我们尽量避免了 unsafe 的使用,但没有绝对禁止(毕竟禁止就写不出来了)。

据统计,在 HAL 之上大约有 20 个 unsafe,其中大部分用在了两个对象之间互相取 Weak 引用的操作,剩下的也比较容易检验正确性。

而 HAL 之下 unsafe 就比较多了,由于贴近底层硬件,几乎处处 unsafe,也就跟 C 没什么区别了。不过好在 HAL 代码还是比较少的,不过几百行而已。     

Rust 唯一的问题就是门槛太高了。然而对于编写内核这种对性能、稳定性、安全性都要求极高的程序而言,门槛高一点未必是坏处。

在被 Rust 编译器反复教做人之后,才知道自己当初太天真,写出来的程序处处是隐患。

async 机制 除了上面提到的用户态运行之外,zCore 还有一大创新之处:首次在内核中引入了 async 无栈协程机制。     

熟悉主流编程语言的朋友会知道,async 是近几年开始流行的一种语言特性,能够让开发者用同步的风格编写异步代码。它本质上是将代码变换成状态机,在 OS 线程的基础上又提供了一层轻量级的“协程”,使得程序能够高效处理异步 IO,同时保持开发的高效率。     

Rust 语言于 2019 年底正式稳定了 async-await 语法,并于今年 3 月份的 PR#69033 中为 no_std 环境下使用 async 扫清了障碍。

这使得在内核中全面应用 async 机制成为了可能,而 zCore 可能是第一个吃螃蟹的人。(C++20 中也引入了同样的特性,不过考虑到历史包袱和生态问题,我比较怀疑能否真正用起来)     

在传统 OS 中,每个内核线程需要有自己独立的内核栈。当线程挂起时,它的状态就保存在栈上。

由于内核线程可能很多,因此每个线程的栈都不能太大,在 Linux 中一般是两个页也就是 8KB。而在 zCore 中,所有内核线程都变成了协程,在一个 CPU 核上共享同一个内核栈。

当进入用户态时,内核栈不再清空,因为要保留必要的信息,于是内核-用户切换的风格从传统的「用户态中断调用内核处理函数」变成了「内核主动调用函数切换到用户态执行」。

当任务挂起时,协程的状态被包装成 Future 存储在堆上。根据计算,目前每个 Future 状态机的大小约为 600B 左右,大幅节省了内存空间。     

无栈协程相比线程的好处除了空间占用少以外,还有更小的上下文切换开销,进而实现更高的并发和吞吐率。

不过它的缺点在于协作式、不可抢占,这可能会为系统的实时性带来挑战。关于二者之间的对比,还有待进一步的测试和分析。

zCore 的主要特性和创新点:

第一个完全山寨的 Zircon 内核

使用 Rust 编写,实现精简,层级清晰

支持用户态开发、测试和运行

第一个在内核中使用 async 机制

总的来说,zCore 应该是目前为止我们能想到、做到的,Rust 语言操作系统的集大成之作了。
 

6. 代码下载体验

已经运行过rCore环境的机器,可以直接下载zCore的代码体验下。 

下载命令:  

 

Git clone https://github.com/rcore-os/zCore.git
编译运行命令:
cargo qemu --arch riscv64
还是基于RISC-V硬件的qemu虚拟机运行:

GDB调试

 






审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分