Introducing: Sham
1024那天写了篇的文章《Python 代码一键转流程图》。CSDN 居然给了个 “最趣味”奖🏆:https://blogdev.blog.csdn.net/article/details/109536460 。
一开始我是不知道获奖的事的,(害,咱想都不敢想),直到有一天, CSDN 的小姐姐主动来加我微信,怒斥我仍未填写收货地址[捂脸]。。。
经历了一些曲折,前两天我终于还是收到了 CSDN 的奖品:
感谢 CSDN 🙏。
不过,这不是今天的主题,只是炫耀一下。今天要讲的故事是:
我这个人不懂什么操作系统,于是我用 Go 语言模拟出了一个…
起因
以前看过一篇文章叫做《我这个人不懂什么CPU,于是我用代码模拟出了一个》。这篇文章介绍了一位大佬一言不合用 Go 语言模拟了一台计算机(项目:djhworld/simple-computer)的故事,帅爆了好吗。
这学期有操作系统课,上这个课程的主要收获就是,课上那些活在 PPT 中的算法,就很迷!我是不太喜欢这种方式的,我奉行 Talk is cheap. Show me the code.
所以,我也用 Go 语言模拟了一个“操作系统”——一个拥有标准输入输出与进程间通信的、基于时间片轮转调度的多道程序运行器:cdfmlr/sham (sham 意为:骗局、虚假事物)。
取名叫做 sham 就是希望大家原谅我的标题党行为,事实上,这个东西远谈不上一个“操作系统”,和 djhworld/simple-computer 等类似的优秀项目比起来,,我做的什么都不是。但我只做了不到 10 分钟的设计,并用不到 2k 行代码实现了它。作为业余蒟蒻实现的玩具项目,你还能对它有什么更高的要求呢?
下面,介绍 Sham 系统。
Sham 系统概述
在这里,简要介绍系统中的一些关键设计思路。
至于具体的实现,事实上,这个项目非常简单,而且我写了不少注释,在 git commit message 中也留下了(在我看来)较为详细的说明,如果您不介意,完全可以去读一读源码:
当然,如果你喜欢这个项目,欢迎 Star、Fork、Watch 三连。如果有任何疑问、意见或建议,也欢迎 Issue 和 PR。
抽象结构
这个模拟的操作系统中,主要包含以下抽象:
- CPU:一个带有互斥锁的结构,单独在一个协程中运行应用程序的线程。
- 内存:一个无限大的结构,不需要管理。
- IO设备:一个独立的设备,单独在一个协程中运行,可以输入或输出。
- 操作系统:包括操作系统基础结构、调度器、系统调用、中断处理程序组。为例简化模型,操作系统单独运行在一个协程中,它本身不需要在 CPU、内存上运行,而是在内部持有 CPU 和内存。
- 操作系统基础结构:持有并管理 CPU,内存、IO 设备、进程表和中断向量。
- 调度器:完成进程调度工作的具体算法。
- 系统调用:为应用程序提供的“内核态”操作接口。
- 中断处理程序:处理应用程序或操作系统内部发出的各类中断。
- 进程、线程:为了简化模型,一个进程只能持有且必须持有一个线程。
- 进程:包括一个可运行的进程、当前状态(运行、就绪、阻塞)以及需要的各种资源(内存等)。
- 线程:进程具体可执行的部分,包含要运行的“程序代码”以及运行时的上下文。
- 上下文:包括程序计数器、进程指针、操作系统接口。
- 具体应用程序:进程的实例(在 sham_test.go 中实现了一些有意思的应用)。
目前的实现中,调度器使用的是一个带时间片轮转的 FCFS 调度器,但可以十分容易地添加、替换新的调度算法。
系统的工作流程
线程的运行
关于线程的运行,直接看一部分 Thread 具体代码实现:
// Thread 线程:是一个可以在 CPU 里跑的东西。
type Thread struct {
// runnable 是实际要运行的内容
runnable Runnable
// contextual 是 Thread 的环境
contextual *Contextual
}
// Runnable 程序:应用程序的具体的代码就写在这里面
// 每一次返回就代表“一条指令”(一个原子操作)执行完毕,返回值为状态:
// - StatusRunning 继续运行(如果时间片未用尽)
// - StatusReady 会进入就绪队列(即 yield,主动让出 CPU)
// - StatusBlocked 会进入阻塞状态(一般不用。需要阻塞时一般通过中断请求)
// - StatusDone 进程运行结束。
type Runnable func(contextual *Contextual) int
// 进程的状态
const (
StatusBlocked = -1
StatusReady = 0
StatusRunning = 1
StatusDone = 2
)
// Contextual 上下文:线程的上下文。
type Contextual struct {
// 指向 Process 的指针,可以取用 Process 的资源
Process *Process
// 通过 Contextual.OS.XX 调系统调用
OS OSInterface
// 程序计数器
PC uint
}
具体的应用程序把“代码”写在一个 Runnable 型的函数中,这个函数每执行完一个原子操作就应该返回一次。同时 runnable 也可以在执行完任何一个原子操作时,被外部事件强行打断(比如时间片用尽)。
Thread.Run()
实现了上述控制,runnable 的返回、外部的打断都会被 Thread.Run()
捕获。前面的流程图中,「CPU 运行线程」也就是调用 Thread.Run()
方法:
// Run 包装并运行 Thread 的 runnable。
// 该函数返回的 done、cancel 让 runnable 变得可控:
// - 当 runnable 返回,即 Thread 结束时,done 会接收到 Process/Thread 的状态。
// - 当外部需要强制终止 runnable 的运行(调度),调用 cancel() 即可。
func (t *Thread) Run() (done chan int, cancel context.CancelFunc) {
done = make(chan int)
_ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
// 一条条代码不停跑,直到阻塞|退出|被取消
select {
case <-_ctx.Done(): // 被取消,取消由 CPU 发起
// 取消时 CPU 会临时置 Status 为需要转到的状态,
// 这里获取并把这个值传给操作系统
// 同时把状态重置为 StatusRunning
//(状态转化需由操作系统完成,这里只是暂时借用这个值,故要还原)
s := t.contextual.Process.Status
t.contextual.Process.Status = StatusRunning
done <- s
return
default:
ret := t.runnable(t.contextual)
t.contextual.Commit() // PC++, clockTick()
if ret != StatusRunning {
// 结束|阻塞|就绪,交给调度器处理
done <- ret
return
}
}
}
}()
return done, cancel
}
这里使用 go 语言标准库的 context 包,控制线程运行的取消(外部打断)。如果没有外部打断,runnable 返回时就会将程序计数器和时钟加一。如果 runnable 一直返回 StatusRunning 就可以一直运行下去。在 runnable 内部,通过 contextual.PC
以及例如 switch
的流程控制语句即可实现一条条代码执行的效果。