今天要讲的故事是:

我这个人不懂什么操作系统,于是我用 Go 语言模拟出了一个......

起因

以前看过一篇文章叫做《我这个人不懂什么CPU,于是我用代码模拟出了一个》。这篇文章介绍了一位大佬一言不合用 Go 语言模拟了一台计算机(项目:djhworld/simple-computer)的故事,帅爆了好吗。

这学期有操作系统课,上这个课程的主要收获就是,课上那些活在 PPT 中的算法,就很迷!我是不太喜欢这种方式的,我奉行 Talk is cheap. Show me the code.

所以,我也用 Go 语言模拟了一个“操作系统”——一个拥有标准输入输出与进程间通信的、基于时间片轮转调度的多道程序运行器:

(sham 意为:骗局、虚假事物)

取名叫做 sham 就是希望大家原谅我的标题党行为,事实上,这个东西远谈不上一个“操作系统”,和 djhworld/simple-computer 等类似的优秀项目比起来,,我做的什么都不是。但我只做了不到 10 分钟的设计,并用不到 2k 行代码实现了它。作为业余蒟蒻实现的玩具项目,你还能对它有什么更高的要求呢?

下面,介绍 Sham 系统。

Sham 系统概述

在这里,简要介绍系统中的一些关键设计思路。

至于具体的实现,事实上,这个项目非常简单,而且我写了不少注释,在 git commit message 中也留下了(在我看来)较为详细的说明,如果您不介意,完全可以去读一读源码:

Or:

当然,如果你喜欢这个项目,欢迎 Star、Fork、Watch 三连。如果有任何疑问、意见或建议,也欢迎 Issue 和 PR。

抽象结构

这个模拟的操作系统中,主要包含以下抽象:

  • CPU:一个带有互斥锁的结构,单独在一个协程中运行应用程序的线程。
  • 内存:一个无限大的结构,不需要管理。
  • IO设备:一个独立的设备,单独在一个协程中运行,可以输入或输出。
  • 操作系统:包括操作系统基础结构、调度器、系统调用、中断处理程序组。为例简化模型,操作系统单独运行在一个协程中,它本身不需要在 CPU、内存上运行,而是在内部持有 CPU 和内存。
  • 操作系统基础结构:持有并管理 CPU,内存、IO 设备、进程表和中断向量。
  • 调度器:完成进程调度工作的具体算法。
  • 系统调用:为应用程序提供的“内核态”操作接口。
  • 中断处理程序:处理应用程序或操作系统内部发出的各类中断。
  • 进程、线程:为了简化模型,一个进程只能持有且必须持有一个线程。
  • 进程:包括一个可运行的进程、当前状态(运行、就绪、阻塞)以及需要的各种资源(内存等)。
  • 线程:进程具体可执行的部分,包含要运行的“程序代码”以及运行时的上下文。
  • 上下文:包括程序计数器、进程指针、操作系统接口。
  • 具体应用程序:进程的实例(在 sham_test.go 中实现了一些有意思的应用)。

目前的实现中,调度器使用的是一个带时间片轮转的 FCFS 调度器,但可以十分容易地添加、替换新的调度算法。

系统的工作流程



线程的运行

关于线程的运行,直接看一部分 Thread 具体代码实现:

具体的应用程序把“代码”写在一个 Runnable 型的函数中,这个函数每执行完一个原子操作就应该返回一次。同时 runnable 也可以在执行完任何一个原子操作时,被外部事件强行打断(比如时间片用尽)。

Thread.Run()Thread.Run()Thread.Run()
contextual.PCswitch

例如,下面的这个 Runnable 函数(程序)有 4 个操作(4 条指令)它们会依次执行:

这样的写法有些像汇编语言,而事实上,runnable 甚至就是这个模拟的操作系统的机器语言!所以写起来略为繁琐,这个难以避免。

运行的效果如下:



:把上面的程序翻译成我们平时的 Go 程序就是:

中断与 IO

中断在操作系统中至关重要,所以这个模拟的操作系统也需要中断的实现。

这里对中断的建模如下:

中断包含一个类型,处理程序(处理程序和中断类型一一对应),以及附加的数据。

应用程序,或操作系统内部都可以通过一个系统调用发出中断(这个模拟中几乎没有硬件,所以不考虑硬件中断)。操作系统受到中断请求,会把中断放入一个中断表,然后在当前时钟周期结束后,调度运行时程序阻塞,依次调用相应中断处理程序处理中断表里的中断。

中断处理程序会做必要的工作,完成后解除发起进程的阻塞状态。

有了这个中断机制,就可以很容易地实现时间片轮转——在时间片用尽后,发出一个时钟中断,其处理程序会把进程就绪。

同时,中断支持了各种 IO 设备的使用。例如,最简单的是使用标准输出,发出一个标准输出的中断请求,传入需要的数据即可:

这个中断的处理程序会使用标准输出设备,完成输出。运行结果:



出了标准输入,我还实现了标准输出设备,以及特殊的 Pipe 设备。这两个东西的调用代码都比较复杂,在此不做演示。

标准输入、输出都是模拟的“硬件”,这些东西会单独在一个协程中运行。而 Pipe 设备则是一个虚拟的设备,它是对 Go 语言 channel 的封装。操作系统可以动态生成任意多个 Pipe 并分配给需要的进程,通过 Pipe 进程之间就可以利用 CSP 模型可以完成通信与同步。

利用 Pipe 设备,在这个模拟的操作系统中,可以实现「生产者-消费者」问题。代码稍微复杂(需要约 150 行代码,见 sham_test.go 之 TestProducerConsumer),但完全可以正确工作:



## Sham 程序设计

作为系统可用性的~~不严谨~~证明,下面给出一份 Sham 程序设计指南。

在 Sham 源码的 sham_test.go 文件中,包含了一系列开发过程中测试使用的应用程序实现。我们完全可以把这个文章看作 Sham 程序设计的官方实例。下文中任何不明朗部分都请参考那些实例。

如何运行 Sham?

获取一个 OS 实例,指定调度器(FCFSScheduler 是先来先服务算法),添加应用程序进程,然后 Boot 就开始运行了!

系统的就绪队列中默认有一个 Noop(NO OPeration)进程,这个进程什么也不做。如果不需要,可以参考被注释掉的第三行代码删除它。

shamOS.CreateProcess

precedence 和 timeCost 是给留特定的调度算法使用的,如果使用 FCFSScheduler 就没什么用,随便写即可,runnable 就是具体的程序代码了,下面就介绍 runnable 的写法。

顺序程序

switch contextual.PCreturn StatusRunningreturn StatusDone

注:这里是调用了 log(http://github.com/sirupsen/logrus,打一条日志),这其实是个「超系统调用」,调用了 sham 以外的东西。作为测试,这没问题。

系统调用

CreateProcess
CreateProcess(pid string, precedence uint, timeCost uint, runnable Runnable)
contextual.OS.XXXCreateProcess

还有一个更有用的系统调用——中断请求:

InterruptRequest(thread *Thread, typ string, channel chan interface{})
contextual.Process.Thread

由于当前线程与中断处理程序显然不同步,channel 必须是带缓冲区的!

目前可用的中断类型有:

typ说明channel
StdOutInterrupt输出到标准输出放入药输出的东西
StdInInterrupt从标准输入获取输入一个空 chan,标准输入会写东西进去
NewPipeInterrupt新建一个 Pipe 设备放入 pipeID 和 pipeBufferSize
GetPipeInterrupt获取一个 Pipe 设备放入 pipeID

(关于 Pipe 后面再详细介绍。)

使用标准输入时,需要注意,把 chan 放到 sham 线程的内存中,而不是使用一个 go 变量:

变量使用

VarPool

E.g.

条件/循环

条件、循环这里做的不太好,比较麻烦。需要大家手动维护额外的程序计数器:

dpcJMP

这段代码中关于 Pipe 不太好理解,所以下面介绍 Pipe。

进程通信: Pipe

Pipe 是 Sham 中的一种特殊 IO 设备,它是对 Golang chan 的封装,它提供了 Sham 进程间通信的能力。

创建 Pipe:

其他进程获取 Pipe:

NewPipeInterruptGetPipeInterruptcontextual.Process.Devices["pipeID"]

在读写 Pipe 的时候需要注意边界条件,不可读时强行去读会造成死锁。

pipe.Inputable()pipe.Input() <- something

读也是类似的,检测—读取—否则让出 CPU:

文章结束之前

无论您是技术比我强的大佬,或是在某一方面比我稍弱的同学,看到这里可能都会骂一声——“什么东西?”

为了回答您的疑问,我想重述我对 Sham 的定义:

cdfmlr/sham (sham 意为:骗局、虚假事物):一个拥有标准输入输出与进程间通信的、基于时间片轮转调度的多道程序运行器。

这是我在学习操作系统时的个人娱乐作品。它的功能、整体设计、代码细节都做的很差,因为它最初的设计,仅是为了开玩笑写出的 200 余个字符:


如果您喜欢这个项目,欢迎 Star、Fork 和 PR。如果您不喜欢这东西,或有任何看法、疑问、意见或建议,也欢迎 Issue,望不吝赐教为感!