Skip to content

Latest commit

 

History

History
215 lines (103 loc) · 26.7 KB

tangram-oskits.md

File metadata and controls

215 lines (103 loc) · 26.7 KB

像开发应用一样开发操作系统

-- 基于泛型独立组件构建各种领域OS的想法

关键字:操作系统内核(OS kernel)、组件(component)、操作系统内核组件(os-kits)、裸机组件 (baremetal-kits)、操作系统内核主干(os-backbone) 七巧板组件(tangram-kits)

新应用发展推动操作系统新需求

在新兴的计算机领域,如偏基础类的机器学习、机密计算,或者偏行业类的自动驾驶、工业机器人等,有各种不同的新应用需求,而现有的传统通用操作系统(如Linux、Windows等)并不能充分满足这些需求。这就推动了这些领域的新玩家加入面向特定领域的新型操作系统的研发中来。但操作系统开发的复杂性让开发者望而却步。我们想解决的问题就是,让开发操作系统变得和开发应用一样方便和高效。

传统的Linux、Windows、FreeBSD其实也采用了模块化/组件化的设计思路,而且支持内核模块的动态加载和卸载。但由于它们把通用性和性能放在第一位,而且采用C语言设计,各个模块之间的数据访问和函数调用都是一种紧耦合的方式。所以,我们可以看到一个特点,这些操作系统的模块很难被其他操作系统使用,即这些操作系统的软件不具有重用性,这样这些操作系统为了实现对各种应用的支持,且难以重用其他的成熟软件模块,变得越来越庞大和臃肿。

我们认为将来的操作系统不是像现在统治世界的 Linux、Windows 那样庞大而通用,而是各种可以迅速组合形成的,并且功能丰富多彩的组件化定制操作系统,能够快速适配未来多种多样的处理器、加速器、外设和应用需求,在开发的便捷性、性能和安全性等方面优于已有的通用操作系统。但如何设计组件化定制操作系统是一个需要深入思考的挑战性问题。

在我们的操作系统课程的各种OS实验设计中,我们经历了开发操作系统的野蛮生长阶段。同学们和老师一起尝试过,基于x86/ARM/MIPS/RISC-V/OR1200等,用C/C++/Go/Rust/Haskell/自创语言等来开发操作系统,实践过UNIX、Linux、Minix、FreeRTOS等不同特点的操作系统架构,开发过完整的操作系统或某种功能的内核子系统或算法等,并尝试重走它们的开发过程。同学们在学完后,留下了他们的实践过程纪要,为我们现在尝试采用应用开发模式来开发操作系统内核积累了丰富的失败和成功(失败居多)的经验。

组件化定制操作系统的历史回顾

组件化定制操作系统的这种想法早在二十世纪九十年代学术界就有过大胆的尝试,如MIT的PDOS课题组提出的Exokernel和剑桥大学等提出的Nemesis操作系统是第一轮组件化操作系统的探索,其特点是通过把操作系统的部分功能以专用库的形式提供给应用程序使用,运行在用户态,以提升系统的整体性能。由于开发的成本过高,而使用领域比较窄,没有在产业界落地。

进入二十一世纪后,Internet席卷全球,如何优化云计算和虚拟化成为新热点,学术界开始了第二轮组件化操作系统的发展,典型的例子是犹它(utah)大学计算机科学系FLUX研究组提出的OSKit项目,基于C语言设计了34个组件库和一个连接这些组件库的框架来形成不同功能的特定操作系统内核。2007年剑桥大学计算机实验室用领域相关语言(Domain-Specific Language)MPL和类型安全的OCaml函数式语言设计实现了SSH和DNS服务器,也成为了后续基于OCaml语言的组件化MirageOS的基础。这一次的探索依然没有被产业界广泛接受。其中很重要的一部分原因其实在编程语言上。C语言没有方便的面向对象的能力,也缺失泛型等语言特征,不具有广泛的重用性,难以形成可被广泛重用的组件。而Ocaml语言的抽象能力很强,安全性也很好,但它具有主流函数式语言的通病,相对于C语言,其通用性很弱,而性能更差,缺少对已有应用的支持,难以得到产业界认可。不过组件化操作系统的探索还在继续,2018年,Rice大学提出了基于Rust语言的Theseus操作系统,通过精心设计的内核组件,支持操作系统的组件在线更新,使得操作系统的可靠性得到很大增强。但由于过于注重高可靠设计,在性能和已有应用的支持比较差。

2014年以后,学术界意识到如果要得到产业界的认可,需要支持在性能和应用支持上突破,这方面的代表是NEC欧洲实验室、Lancaster大学、Manchester大学、University Politehnica of Bucharest等合作设计的Unikraft操作系统。Unikraft操作系统采用C语言编写,由不同的组件库构成,支持云计算环境中的常用Linux应用。由于采用unikernel架构设计,让应用程序和操作系统内核都运行在内核特权态,提高了整体性能。Unikraft的性能和对Linux应用的支持,使得它得到了产业界一定的认可和使用。但Unikraft采用C语言设计实现,这导致了内核组件的重用性和安全性还需提高。

开发新型操作系统的关键问题

而我们觉得设计组件化定制操作系统一个重要因素是重新思考操作系统的编程语言。通过这几年用Rust语言设计操作系统内核的探索,我们觉得采用Rust语言来设计内核组件并形成各种定制的操作系统也许是一条可行的道路。我们的工程师和学生在Rust写操作系统方面进行了探索,设计了rCore、zCore、aCore、rCore Tutorial等各种类型的操作系统原型。在这个过程中,我们发现,虽然操作系统的设计实现细节不同、支持应用程序的Syscall(系统调用)不同、支持的处理器硬件和外设各不相同,但还是有大量共性的操作系统功能存在。这促使我们重新思考如下一些问题:

  1. 操作系统能向小型专用的方向发展吗?

我们的分析结果是能,且很有必要。到目前为止,大家经常见到的都是通用的操作系统,如Windows、Linux、MacOS等,或者是基于Linux操作系统的Android系统等。但这样的操作系统对于目前新的应用领域不一定合适,比如在智能汽车的控制领域,需要支持非常轻量实时高可靠的嵌入式应用,在智能汽车的智驾领域,需要的是支持AI加速和及时处理环境大数据的人工智能应用。这几类应用不需要我们常见的可视化图形交互界面等,对操作系统的需求不在于丰富的人机交互,不需要处理办公所需的打印机需求等,而在于安全、可靠地及时响应事件和高效处理数据。操作系统需要的功能需求和要应对的外设相对通用操作系统也少不少。如果对于这类应用采用庞大的Linux操作系统,虽然在功能上能满足需求,但在性能、安全和可靠性等方面的要求,Linux操作系统难以满足。采用小型专用的操作系统相对更容易满足这方面的要求。

  1. 现有的RTOS等专用系统能满足以人工智能应用为代表的新应用需求吗?

我们认为是比较困难。现有的RTOS主要面向的是传统的嵌入式应用,所以在设计上很简洁,实时性是其考虑的重点。但现有的人工智能应用的复杂性更高了,大量人工智能应用在Linux上开发和运行,难以直接运行在RTOS上。如果要运行在RTOS上,需要考虑对AI加速外设的支持,以及对Linux应用的支持,这对很多RTOS而言,有比较大的挑战。

  1. 主流的通用操作系统和RTOS在安全性上有何需要注意的地方?

我们认为注意的关键因素之一是编程语言。主流的通用操作系统和RTOS大部分都是基于C语言开发,C语言简洁高效、灵活性很大(如指针操作和类型转换等),就是为开发UNIX操作系统而诞生的。但随着软件复杂性的增加,即使是操作系统内核的专业开发人员,也会出现常见的内存相关和并发相关的编程错误。所以,采用安全性高的编程语言(如Go、Swift、C#、Java、Rust等)是一种可行的解决方案,如果把性能也作为重要因素来考虑,那么采用Rust语言开发操作系统是一种合适选择。

  1. 开发操作系统的痛点是什么?

我们认为开发操作系统的痛点编写操作系统软件很繁琐,需要关注的细节太多。其根本原因是操作系统内部模块广泛的相互依赖带来的软件复杂性,以及操作系统的自包含性带来的软件不可重用性。

每个应用程序员都知道,自己写的程序一定会依赖下层的各种软件库和编程框架。现在编写一个流行的人工智能应用比较容易,我们经常看到经验不是很丰富的程序员用十几行Python代码就可以实现人脸识别。这并不意味着人工智能的技术不复杂且很容易掌握,而是由于实现深度机器学习算法的专家给上层应用提供了统一的编程框架(如TensorFlow、Pytorch)和简洁编程接口,而实现NVIDIA AI加速硬件驱动的专家给实现深度机器学习算法的专家提供了统一的CUDA编程框架和接口。

Linux的GUI 应用程序员需要了解KDE或Gnome的编程框架和API接口,但不用了解KDE或Gnome的内部实现。实现KDE或Gnome的程序员知道自己写的程序一定依赖底层绘图库Qt或GTK的API接口,以及底层的X Windows或Wayland 库的GUI支持API,但也不用了解这些底层库的具体实现。

每一层的开发者基本上不用考虑下面各层开发者需要考虑的细节,只需根据直接下层开发者提供的编程框架和接口进行开发就好了。这其实就是非常经典的软件工程开发方法:层次化软件程序设计。这种设计方法带来了软件重用和开发方便的好处,推动程序员开发出丰富多彩的应用程序。

另外,对于提供软件编程框架或软件库的软件设计者而言,他有很明确的服务对象 -- 上层软件开发者,而不是他自己。所以,他需要考虑上层应用的需求,把尽可能多的复杂实现细节留给自己,把尽可能简洁和通用的编程接口留给上层软件开发者。如果他不这样做,很难让开发者接受自己提供的软件。

但一个奇怪的现象是:在操作系统的设计实现中,并没有采用层次化软件程序设计这样的开发方法。以Linux为例,虽然Linux为了支持应用,在系统调用层面,采用了得到业界共识的POSIX接口作为系统调用的接口,并保证了接口的长期稳定不变。但在Linux内核的内部实现中,各个功能模块之间的[相互依赖性(函数调用或数据访问)非常大](unikraft eurosys21 paper),如果要对其中一个模块进行重新设计,几乎需要了解与其它各个模块的依赖关系。这使得Linux内核开发是一个非常有挑战的工作。

操作系统中软件的不可重用性,也极大地阻碍了内核程序员像应用程序员一样开发出丰富多彩的各种操作系统。前面已经提到支持上层应用的软件层次形成了一个软件栈,软件栈中的每层软件是直接向下依赖的。每一层的软件都知道自己需要使用的下层软件提供了哪些功能和服务,这样就可以充分利用下层软件来实现自己的功能,从而达到了软件重用的目的。

但操作系统是这个软件栈的最底层,再向下就是CPU和各种外设的计算机硬件了。所以操作系统是一个自包含的软件系统,它需要实现它所需要的各种功能,且实现的功能是很多是为自己服务的。以Linux为例,它除了给上层应用提供POSIX接口的系统调用所需功能外,这些功能所依赖的其它软件也都是操作系统的组成部分,所以,我们在操作系统内部可以看到给应用提供服务的进程管理、内存管理、文件系统管理这些核心模块,还需要调度、任务切换、中断处理、页表处理、异常处理,I/O缓冲区管理、设备驱动程序支持、同步互斥支持等各种内核模块的支持。而且模块间是多对多的依赖关系。这使得如果想把一个Linux的内核模块拿出来重用来实现一个新的操作系统,就不得不解决模块间繁多的依赖关系,让开发新的操作系统变得非常困难。

  1. 为何Linux操作系统内部具有强相互依赖性和自包含性?

这可以通过操作系统的发展史来分析。操作系统可以说是生存周期最长的软件,但操作系统的前期规划与设计具有很大的偶然性、历史局限性和开发者的个人主观特征。1991年发布的Linux的软件设计思路来源于1969年的UNIX,而它们在最开始都是由一位天才的软件开发程序员来完成的。但开发Linux的Linus Torvalds和开发UNIX的 Ken Thompson难以预估到2023年的今天操作系统跑在哪些硬件上,支持哪些应用。

先看看UNIX。在1969年~1972年期间,贝尔实验室的两位软件工程师 Ken Thompson 与 Dennis Ritchie经历多次迭代开发,用汇编语言写UNIX,再尝试用B语言写UNIX,最后发明C语言重写UNIX。整个操作系统的核心代码量在一万行以下。对于用三周时间就一个人独立写出UNIX的天才程序员Ken Thompson而言,编写一个一万行的软件游刃有余。对于规模一万行的软件,其内部功能模块的强相互依赖性和自包含性都不会严重阻碍软件开发和软件重用。让他考虑层次化软件设计方法来写UNIX有点像高炮打蚊子。而Dennis Ritchie设计C语言就是为了方便开发UNIX操作系统,所以编程简洁灵活,方便访问硬件,以及良好的可移植性成为C语言的首要设计目标。安全、并发、面向对象、泛型、支持大规模软件等这些现代高级编程语言的设计目标不仅在当时不存在,即使到了2023年的今天,在C语言中也不存在。当AT&T早期的UNIX操作系统源码免费发放给外界程序员后,后续的内核开发者对UNIX的功能进行了广泛的扩展。C语言的灵活性在其中发挥了巨大的作用,C语言非常方便的类型转换和指针,以及简明的结构化编程支持,使得在早期直接添加软件模块实现新功能比较容易。但操作系统的代码量的迅速膨胀,软件规模从一万行扩展到一百万行,再扩展到一千万行,量变形成了质变。强相互依赖性和自包含性就从微不足道的特征从软件工程的角度看,变成阻碍操作系统发展的瓶颈。我们可以看到,开发Linux内核的程序员数量,远少于其它系统软件或应用软件的程序员数量;国际上各种操作系统的数量也远少于某类应用或某类系统软件的数量。目前的现状是,主流操作系统变得越来越庞大,种类也变得越来越少。这其实不利于操作系统自身生态的发展。

采用应用开发模式的操作系统设计

如何能做到像开发应用一样开发操作系统,并形成丰富多彩的操作系统自己的生态呢?方法其实就是学习用户态软件的开发思路,培养具有层次化软件开发方法的软件工程师。为此,我们需要把操作系统看出是一个多层次的软件栈,分析操作系统中各个功能的层次划分,从而形成面向操作系统的层次化软件开发方法。首先需要确定操作系统的两端,即应用需求和运行硬件。这里以在Qemu上开发一个具有进程、虚存和文件系统功能的操作系统内核为例来说明如何开发一个组件操作系统内核。

需求分析

首先,进行操作系统内核的初步需求分析,即分析在操作系统上的应用和执行操作系统的计算机硬件。由于应用需要能创建执行程序对应的进程,能支持进程间的地址空间隔离,为此需要有基于页表的虚存管理,需要保存数据,所以需要有文件系统。这样的应用需求需要有硬件的支持。Qemu可以模拟基于单核RISC-V 64位处理器的 virt machine 计算机,它具有S-Mode特权级,支持操作系统的运行;处理器中包含MMU结构,支持基于页表的虚存访问;具有处理器时钟中断,支持进程调度与切换;具有 UART外设,可以用于基于字符的输入输出;具有 virtio-blk 块存储设备,可以支持文件系统。这样,我们就完成了初步内核分析。

第二步,进行操作系统内核的详细需求分析。首先分析应用所需的系统调用,形成系统调用的列表。初步分析,主要需要两类系统调用:进程相关系统调用和文件系统相关系统调用(包括了串口的输入输出),以及其它辅助类系统调用。我们可以参考 rCore Tutorial操作系统内核开发实践教程,形成下面的系统调用列表:

编号 系统调用 所在章节 功能描述
1 sys_exit 2 进程:结束执行
2 sys_write 2/6 文件:(2)输出字符串/(6)写文件
3 sys_yield 3 进程:暂时放弃执行
4 sys_get_time 3 其它:获取当前时间
5 sys_getpid 5 进程:获取进程id
6 sys_fork 5 进程:创建子进程
7 sys_exec 5 进程:执行新程序
8 sys_waitpid 5 进程:等待子进程结束
9 sys_read 5/6 文件:(5)读取字符串/(6)读文件
10 sys_open 6 文件:打开/创建文件
11 sys_close 6 文件:关闭文件
12 sys_sleep 8 进程:休眠一段时间

接下来分析所需的硬件支持,特别是外设支持,通过上述的应用需求,以及对 virt machine 计算机的分析,可以了解到操作系统需要提供如下与硬件相关的功能:

编号 硬件相关的内核功能 所在章节 功能描述
1 进程上下文切换 5 进程:支持进程切换
2 基于页表的虚拟地址空间 4 虚存:支持进程的地址空间隔离
3 处理器特权级切换 2 进程:用户态进程进入内核与从内核返回到用户态进程
4 时钟中断 3 进程:打断当前进程执行,让操作系统调度下一就绪进程
5 UART设备驱动支持 9 文件:支持字符输入与输出
6 virtio-blk设备驱动支持 9 文件:支持文件系统
7 PLIC设备驱动支持 9 文件:连接CPU与外设的平台级的外设中断管理

概要设计

接下来,就是进行操作系统内核的概要设计了。我们这里考虑的是一种在指定硬件上仅满足应用需求的领域操作系统。需要多位程序员一起来完成这个工作,而不是像以前的UNIX和Linux开发,由一位天才程序员完成所有工作。所以架构设计和组件化设计还是很有必要的。首先是架构设计,我们需要把操作系统看成是一个有序软件栈,而不是一个具有强依赖关系的软件线团。根据对操作系统功能的深入分析,可以把把操作系统初步拆分为三个部分:

  1. 第0层:最下层,是硬件相关的软件层,主要包括各种设备驱动和与处理器特权级、页表、进程上下文切换、中断与异常处理相关的部分。
  2. 第1层:中间层,是操作系统的抽象资源管理层,基本上与具体硬件无关,为系统调用提供服务,主要包括:进程管理子系统、虚存管理子系统和文件系统。
  3. 第2层:最上层:系统调用相关的接口层,应对中间层提供的服务。

同学们在之前的操作系统开发中,当完成这一步后,就开始编写操作系统或操作系统模块了。其实到这一步还不足以达到设计实现丰富多彩的操作系统的目标。我们还需对各个层次进行更加细化的分析。

组件划分

如果要让程序员在较短的时间内实现丰富多彩的操作系统,需要像应用程序一样,在软件栈的各个层次上,都有可以重用且接口比较明确的软件层组件。上面的概要设计还没有解决这样的问题。基于组件化设计的基本思路,整个操作系统可以按不同维度进行划分:

  1. 硬件:硬件相关层与硬件无关层;
  2. 主干与组件:不可拆分的紧耦合的核心部分与可拆分的具有一定独立性的组件部分

硬件相关层的层次划分

我们先看最下层,即硬件相关的软件层。通过对我们之前实现的操作系统驱动程序、处理器与内存相关的硬件操作函数的分析,我们发现与硬件相关的内核代码在功能和组成上其实可以进一步细化成不同的层次和软件组成,形成主干部分和组件部分。

就像计算机系统的总体架构一样,硬件系统也分核心部分和外围部分,处理器是最核心部分,然后是属于次核心部分的内存,外围部分是连接处理器/内存的总线,以及各种外设。所以与硬件相关的内核代码也可进行类似划分,把与硬件相关的操作系统部分分为:

  1. 内核主干(os-backbone)的核心部分,主要完成进程上下文切换、控制寄存器访问、处理器特权级切换、中断总体处理、时钟中断处理的硬件相关的代码;

  2. 内核主干的次核心部分,主要完成基于页表的虚拟地址空间管理的相关代码;

  3. 裸机组件 (baremetal-kits)模态的设备驱动:主要完成某外设(如 UARTvirtio-blk)的基本硬件功能的设备驱动代码库,与具体计算机系统(如virt machine)和具体操作系统无关,可在没有操作系统功能的最小裸机执行环境下运行;

  4. 内核组件(os-kits)模态的设备驱动:可直接调用裸机组件 (baremetal-kits)模态的设备驱动代码库,并结合具体计算机系统(如virt machine)的硬件配置信息(如外设控制寄存器的基址等),可驱动具体计算机中的外设。同时与操作系统中相关子系统对接(如块存储设备驱动内核组件与文件子系统内核组件对接),服务操作系统中上层软件栈的需求。

  5. 裸机组件(baremetal-kits)模态的硬件平台(platform)驱动:管理各种外设的硬件平台(如PCI、PLIC)驱动库,与具体计算机系统(如virt machine)有关,但与具体操作系统无关,可在没有操作系统功能的最小裸机执行环境下运行;

  6. 内核组件(os-kits)模态的硬件平台驱动:可直接调用裸机组件 (baremetal-kits)模态的硬件平台驱动代码库,同时与操作系统中相关内核组件(os-kits)模态的设备驱动对接,让设备驱动能正常工作。

其中进程和虚存相关的部分形成操作系统中与硬件相关的内核主干,而各种设备驱动和平台驱动是裸机组件 (baremetal-kits)或内核组件(os-kits)。

抽象资源管理层的层次划分

接下来我们看中间层,即抽象资源管理的软件层。这里的抽象有三个:进程(CPU抽象)、地址空间(内存抽象)、文件(存储与I/O抽象)。这些抽象是操作系统中的重要组成成分,与硬件相关层有直接的对应关系:

  1. 进程组件对接硬件相关层的内核主干的核心部分;
  2. 地址空间组件对接硬件相关层的内核主干的次核心部分;
  3. 功能组件(functional-kits)模态的文件系统:在伪存储设备驱动(如内存模拟等)支持下,可以在实现与具体操作系统无关的文件系统库,可在用户态的最小用户态执行环境下运行;
  4. 内核组件(os-kits)模态的文件系统:可直接调用功能组件(functional-kits)模态的文件系统库,并与内核组件(os-kits)模态的块设备驱动对接,服务系统调用相关的接口层。

其中进程组件与地址空间组件形成内核主干,而各种文件系统是功能组件(functional-kits)或内核组件(os-kits)。而系统调用接口层主要是对接抽象资源管理层,可以看成是抽象资源管理层面向应用的接口,也是内核主干的一部分。

设计与实现

在设计实现上,首先需要定义号组件间的接口,以及组件与主干间的接口,然后就可以分别实现了。需要注意的是,无论是组件还是主干,都需要有一个支持它们单独运行测试的最小执行环境,即实现类似应用程序开发中的单元测试。

对于操作系统主干的开发过程,可以参考 rCore Tutorial操作系统内核开发实践教程,从简单到复杂,逐步开发。

裸机组件模态的virtio-blk驱动设计

可阅读 rCore Tutorial操作系统内核开发实践教程第九章第六节:virtio_blk块设备驱动程序

功能组件模态的easyfs文件系统设计

可阅读 rCore Tutorial操作系统内核开发实践教程第六章第三节:简易文件系统 easy-fs

内核组件模态的virtio-blk驱动设计

可阅读 rCore Tutorial操作系统内核开发实践教程第九章第六节:virtio_blk块设备驱动程序

内核组件模态的文件系统设计

可阅读 rCore Tutorial操作系统内核开发实践教程第六章第四节:在内核中接入 easy-fs

模块化操作系统实现原型参考

可阅读模块化rCore-Tutorial内核原代码和相关说明

2023.01.20