oberon 操作系统

2020-01-06

这周末把 oberon 系统研究了一番。oberon 是一个非主流的操作系统,之所以想研究一下它,主要是在好几个地方都有人提到过它。比如王垠写过《Oberon 操作系统:一个被忽略的珍宝》。另外,我在之前研究 Acme 的时候,也注意到了 Acme 是 inspired by oberon 的。它们都是偏好 TUI 的交互模式,并且强调鼠标操作。Acme 的鼠标手势给我留下了深刻的印象。周末我把 AOS 下载下来试用了一番。AOS 是可以直接跑在 linux 上面的,有一点像 plan9port 那种形式。然后有一本书叫做 《Project Oberon: The Design of an Operating System, a Compiler, and a Computer》,我把其中的系统相关的章节读了一下。

总的来说,作为一个操作系统,oberon 是疯狂地做减法。把非常多的概念都扔掉了,剩下的系统就非常的精简了。如果我们接触的一直是教科书上讲操作系统知识,然后使用的一直是 unix-like 的系统,会形成一些固定的思维模式。看一看 oberon 系统怎么做的,可以打破这种思维定势,看它的一些设计上的取舍。当然,顺便也再复习了一遍操作系统的一些知识点。现在的软件都太复杂了,做减法才是对的。像我们有容器,有 unikernel,很多很多新的东西出来,其实都是在减法。我们并不是都需要一个大而复杂的系统的。好,言归正传,开始讲 oberon 了。

首先,oberon 是没有进程的概念的。没有进程隔离怎么搞?我们知道在现代操作系统里面,进程是系统资源管理的基本单位。操作系统会负责内存管理,像分页分段,虚拟地址映射,内存的换入换出;有进程调度,比如说基于时间片;还文件等等资源。这些在 oberon 里面,都没有。oberon 就是一个单一地址空间的东西,没有进程概念。这个单一地址空间里面,包括内核,包括用户的程序,这些东西,全部是加载到一个地址空间里面。系统运行时,就跑着这一个程序。

单一地址空间,没有进程隔离了。所有任务,都可以看到其它的任务。程序以模块来组织的。按照原文的说法,像 unix 那种属于多用户系统,而 oberon 是一个单用户系统。运行多用户系统时,确实需要一些更复杂的抽象。oberon 典型的使用场景是一个用户,从 oberon 连到工作站上去工作。

分时系统中是不停地有时钟中断,内核处理中断,然后有时间片的概念,如果一个进程时间片用完了,就保存进程状态,切换到另一个进程。oberon 没有时钟中断概念。 基本上可以把 oberon 想象成是一个像 linux 上运行着的进程。整个系统,只跑这一个进程。

oberon 里面还是有任务(task)的概念的。它是一个单用户的系统,但并不是单任务系统。系统同时还是会跑许多许多的任务的。比如说处理网络的 io,处理设备中断,像鼠标,键盘上指令,处理图形界面显示的更新,等等。

调度就是处理这些 task。分为前台 task 和后台 task。其实所谓的调度呢,就类似于我们平时写单个程序里面,有一个 dispatch loop,至少会处理 network 和 timer 的事件,然后会回调相应的 handler。 它里面也是一样的,要被调用的东西就是一个一个的 task。viewer 收到一个消息就是一个前台 task。垃圾回收或者网络收到消息就是一个后台 task。控制流就只有普通的调用和返回,没有抢占。活跃的 task 就是顺序的执行的。由于单进程的,顺序一个一个的执行,根本没有死锁问题。

代码是组织成不同的模块的,比如说有 kernel 模块,viewer 模块这些。模块的加载全部是动态链接的,一个模块只加载一次。不需要每个进程去映射,也没有多个副本。如果有模块的依赖关系,会递归的加载需要的模块。

至于中断处理,它是没有中断处理的,不像其它操作系统内核,需要处理设备中断,然后内核和用户态的进程上下文的切换等等复杂概念。由于没有中断处理,所有设备都是带 buffer 的,驱动那层要处理这些。dispatch 的 loop 处理到相应任务,直接读取并消费掉消息。

任务优先级还是有,调度就是一个 loop,先看高优先级的任务。有就执行,没有就执行低优先级的。task handle 的过程其实就是一个函数,接受一个参数。它用了面向对象的写法,类似于

struct Viewer {
  HandleFunc handle;
  // ...
};

type (*HandleFunc) (struct Viewer* self, struct Msg *msg)

整个系统就是一个函数 + 状态。在 unix 里面,命令是有一个 shell,执行各种小程序。而在 oberon 里面,命令就是函数,是 Module.Procedure 这样子的,调用之后就等于在(系统)全局状态上面执行一个原子操作。

发消息就会激活 task 了。比如消息 InputMsg 和 ControlMsg 分别是鼠标和键盘,然后消息会有相应的处理。 正常的分时系统是任何时候都会接受输入,而 Oberon 相当于只有上个 task 处理完了,才接受下个 command

这些就是关于系统整体的一个大概情况了。后面书里面描述了各模块的具体实现的一些细节:

  • 任务系统
  • 显示系统
  • 文本系统
  • 模块加载
  • 文件系统
  • 存储布局
  • 设备驱动

显示系统,view 是一个可视化状态的黑盒,是一个屏幕上面的显示区域,以及是一个消息处理器。viewer 提供的函数接口是由它能处理的消息决定的。

我比较感兴趣的一个地方是模块系统。它的模块和 loader 部分,比如类似于 linux 系统里面的动态链接。代码以模块形式组织,每个 module 会 load 进去。跟 linux 这一类不同的是,它是实现全动态链接的,并且没有静态链接。 相应章节讲了模块实现和对象格式表示之类的东西。

文件系统跟教科书上常见的讲得差不多。没有发现什么特别有价值的。文件系统是 disk 之上的抽象,然后怎么样把文件存储在 disk 上,如何组织文件,比如读取。 文件是不能连续块分配的,因为长度不固定。也不能链表链起来,因为访问效率太低了。所以是表示成固定长度的 blocks 的 list。这里固定长度 block 其实就是 sector,磁盘里面一块一块的概念。

文件被切到各个 sector 之后,访问效率就会受影响。为了高效的文件访问,在文件头部里面存储 indexed sector table。 然而如果固定索引条目数会有问题:太少能承受的单个文件大小有限制。太多则每个文件即使什么都不存也会浪费比较多的存储空间。 所以 oberon 的文件系统是做成多级索引,前面的条目是一级的,到后面就二级或者更多。用物理地址做分配完回收会有洞,它用的是首次适应算法,分配第一个足够大的块。 文件会有删除,删除会用引用计数管理。当 sector 没有被文件引用到了,就可以回收空间了。至于目录的组织是用的 B 树,文件的索引信息。

存储布局那一章,其实讲的跟进程运行时的结构都差不多。在 oberon 里面是分了模块区,栈区,堆区。由于它是单地址空间的,也就相当于一个进程了。注意,Oberon 里面是没有引入 MMU 做虚拟地址映射的。 它的内存管理就是相当于物理地址模式了。传统的操作系统里面,这一块的实现,虽然是用硬件做,这里会有 cache 不命中,还是会有效率问题。 文章里面写道,引入虚拟内存是为了方便于 disk 作为后端存储,当内存不足的时候按 page 做换入换出。 然而在单用户操作系统里面,这个功能实用价值不大。并且 oberon 的硬件架构用的是 RISC 的。它直接把 MMU 这块给砍掉,可以对系统简化很多。

动态内存管理,也就是申请和释放这些。oberon 内存分配是维护了 4 个 freelist,前三个大小分别是 32 64 128 以及最后一个是 256 整数倍。这样有利于相邻的合并后移进下一级 freelist 减少碎片。垃圾回收还是用的简单的 mark sweep。 相当于就没有传统操作系统里面那么复杂的东西了。什么时候跑交换分区,进程的虚拟页跟物理页的映射,哪些进程 spawn 和销毁时回收等等,都没有了。

那么 kernel 模块有哪些东西呢? 只需要提供 gc 和内存分配相关的操作。比如 new,mark,scan,以及 sector 的分配回收相关的,为上层的 file 模块提供支持,还有就是时钟。

说一个搞笑的,oberon 作者的观点,内存管理这么重要,怎么能交给程序员搞呢?必须有 GC 呀!程序员根本不知道啥时候该释放,而且即使他们告诉系统去释放,也通常是错的,根本就不可信 -_-||

The Oberon System does not provide an explicit deallocation procedure allowing the programmer to signal that a variable will no longer be referenced. The first reason for this omission is that usually a programmer would not know when to call for deallocation. And secondly, this "hint" could not be taken as trustworthy. An erroneous deallocation, i.e. one occurring when there still exist references to the object in question, could lead to a multiple allocation of the same space with disastrous consequences. Hence, it appears wise to fully rely on system management to determine which areas of the store are truly reusable.

想一想,普通的操作系统里面,由于有进程的隔离,只要内核不 panic 掉,系统就是可以继续工作的。而在内核层和应用层之间,是有严格的交互协议的,都要走系统调用。即使用户程序写得挫,至少很难会把内核搞 panic 掉,当然,驱动层很挫就不好保证了。oberon 鸡贼的地方在于,它根本就不信任程序员,它又是单地址空间,那怎么保证用户写的出错不搞整休系统搞挂呢?所以提供垃圾回收是必须的选项。

最后,说一说感想吧。对比几个我觉得特别有意思的程序:emacs,acme,oberon。

acme 的思想还是偏向 unix,虽然它偷了 oberon 里面一些鼠标操作以及 plumb 之类的,它的方向还是 unix 思想。一个命令或者说工具只完成一件很小的事情。然后用 shell 管道这些将工具变成可组合的,以方便用户发挥威力。

oberon 特殊之处在于,它是单一地址空间的一个系统。它没有各种各样的小程序和组合,而是一个一个的模块,交互是走的语言层面的调用。模块之间是可以调用(发消息)的。通过这种方式,只要加载进去的模块,就可以为其它模块提供基础 API 完成相应的功能。

emacs 算是一个 lisp 操作系统了。硕果仅存的 lisp machine。它也是一个单进程系统,并且也是像 oberon 一个在语言层交互的,而不是 unix 那样小工具组合的。不过 emacs 就是 gui 比较弱,毕竟是一个文本编辑器伪装成的操作系统,所以搞一些现代的东西就会有些脱节,像浏览器什么的。

oberon