浅析 NJU-os-JYY 的五十行建模操作系统
前言
其实上课 os 之前,我对操作系统的理解,几乎可以等同于 windows XP、linux kde 之类的桌面环境,或者是提供命令行交互环境,来给我们“操作”。
而实际学习过后,我才意识到 os 的主要职责。
其中极其之多的细节,只有动手编写,才会真正地想明白各个地方。
而之前,我一直觉得实现一个 os 的工程量之大,是一个人需要以月、甚至于年才能做到的。
但 JYY 老师的 50 行代码建模操作系统令我由衷地佩服,想着如果我在上 os 课时候如果能有这样的讲解,我一定会有更深入的理解吧。
注:此代码需要在 Python 3.10 的环境下运行(match
关键字在 3.10 版本才被引入)
测试程序
我们先来看一下给的测试程序
count = 0 # variable in main()
def Tprint(name):
global count # get the variable in main()
for i in range(3):
count += 1
sys_write(f'#{count:02} Hello from {name}{i+1}\n')
sys_sched()
def main():
n = sys_choose([3, 4, 5]) # random choose to assign a value to n
sys_write(f'#Thread = {n}\n')
for name in 'ABCDE'[:n]: # create n threads
sys_spawn(Tprint, name)
sys_sched() # schedules to another thread
假设此时 choose 使得 n = 3, 输出#Thread = 3
,则 spawn 了 3 个线程,然后开始调度(如果此时删去 main() 中的 sys_sched() 会怎么样?),接下来,三个线程并发运行(concurrency,在同一时间间隔内运行)每个线程在循环中自增 count 并输出后就准备调度,结果可能是
#Thread = 3
#01 Hello from C1
#02 Hello from A1
#03 Hello from A2
#04 Hello from C2
#05 Hello from C3
#06 Hello from B1
#07 Hello from A3
#08 Hello from B2
#09 Hello from B3
如果删去 sys_sched() 此时输出不会有任何变化,但是 main() 函数所在的线程结束了。
接下来衍生了一个新的问题,main() 结束了,整个进程是否会结束?
理论上来说,正常情况下是不会的,在这个五十行建模操作系统中,必须得等待其他线程返回结果。
也就是说,删去之前,线程列表中有 4 个线程,而删去这句话之后,main() 函数所在线程运行结束,接下来准备调度到其他线程,线程列表仅有 3 个线程。
建模源码分析
首先,进行一个约定,下文所提到的“源代码”不是指建模源码,而是自己编写的测试程序源代码
yield 关键字
建模源码运用到了 Python yield
关键字的特性,这里先对 yield
关键字进行简单的讲解
线程特性
yield
类似 return
会给调用它的函数返回一个东西
但是不同在于,yield
创建了一个无栈协程,在 Python 里叫做生成器的东西
接下来使用 Python 的交互式环境
>>> def A():
... for i in range(0, 3):
... x = yield i
... print(f"received {x}")
...
>>> type(A)
<class 'function'>
>>> a = A()
>>> type(a)
<class 'generator'>
可以看出,A 本身是一个函数,但是调用它返回的 a 是一个生成器
此时 a 所指的生成器的线程,还未开始运行,需要给它一个 None 信号启动
>>> a.send(None)
0
如此,线程已经运行到了 yield
语句,并停止(此时还未到达 print(f"received {x}")
)
接下来,继续传入信号,注意此时传入的信号均为 yield i
在函数内的值
>>> a.send(9)
received 9
1
>>> a.send(8)
received 8
2
>>> a.send(7)
received 7
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
可以看出,每当我们传入一个信号,就会将信号作为 yield
向内返回的值,并继续运行,直到下一次 yield
并向外返回 yield
后面跟的参数的值
当线程运行结束,则会让生成器返回一个 StopIteration
类型的 Exception
另一种方式运行生成器
使用 for ... in ...
的格式,这里就不过多赘述,相信有了前面的理解,这里稍微看一眼就能感受到其用途
>>> a = A()
>>> for i in A():
... print(i)
...
0
received None
1
received None
2
received None
yield 小结
如此,相信读者已经对 yield
关键字有了部分的理解,接下来进入整体,更多相关内容请查阅 Python 文档
完整建模源码
这里先给出 JYY 的完整源码
#!/usr/bin/env python3
# ^ python environment
import sys
import random
from pathlib import Path
class OperatingSystem():
"""A minimal executable operating system model."""
SYSCALLS = ['choose', 'write', 'spawn', 'sched']
class Thread:
"""A "freezed" thread state."""
def __init__(self, func, *args):
self._func = func(*args)
self.retval = None
def step(self):
"""Proceed with the thread until its next trap."""
syscall, args, *_ = self._func.send(self.retval)
self.retval = None
return syscall, args
def __init__(self, src):
variables = {}
exec(src, variables)
self._main = variables['main']
def run(self):
threads = [OperatingSystem.Thread(self._main)]
while threads: # Any thread lives
try:
match (t := threads[0]).step():
case 'choose', xs: # Return a random choice
t.retval = random.choice(xs)
case 'write', xs: # Write to debug console
print(xs, end='')
case 'spawn', (fn, args): # Spawn a new thread
threads += [OperatingSystem.Thread(fn, *args)]
case 'sched', _: # Non-deterministic schedule
random.shuffle(threads)
except StopIteration: # A thread terminates
threads.remove(t)
random.shuffle(threads) # sys_sched()
if __name__ == '__main__':
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} file')
exit(1)
src = Path(sys.argv[1]).read_text()
for syscall in OperatingSystem.SYSCALLS:
src = src.replace(f'sys_{syscall}', # sys_write(...)
f'yield "{syscall}", ') # -> yield 'write', (...)
OperatingSystem(src).run()
main()
以参数形式读入源码,并将 sys_{SYSCALL} 的 sys_SYSCALL(args)
调用形式替换为 yield, SYSCALL, (args)
至于改变形式的原因,请看下回分解 下文将围绕此进行解释
class OperatingSystem():
SYSCALLS = ['choose', 'write', 'spawn', 'sched']
if __name__ == '__main__':
if len(sys.argv) < 2: # 检验传入参数
print(f'Usage: {sys.argv[0]} file')
exit(1)
src = Path(sys.argv[1]).read_text() # 读取文件
for syscall in OperatingSystem.SYSCALLS: # 将文件读入,并替换 4 种系统调用为 yield 格式
src = src.replace(f'sys_{syscall}', # 例如替换 sys_write(...)
f'yield "{syscall}", ') # 为 yield 'write', (...)
OperatingSystem(src).run() # 接下来接入 OperatingSystem.__init__()
OperatingSystem.init()
获取运行的源代码的 main() 函数入口
class OperatingSystem():
def __init__(self, src):
variables = {}
exec(src, variables) # 传入源代码,并获得变量表(整个机器的状态)
# 注: exec 与 eval 不同,后者有运行代码结束的返回值,而前者没有
self._main = variables['main'] # 把 main() 函数赋入 self._main,接下来接入 OperatingSystem.run()
OperatingSystem.run() PART.1
过渡段
class OperatingSystem():
def run(self):
threads = [OperatingSystem.Thread(self._main)] # 下接 OperatingSystem.Thread.__init__()
OperatingSystem.Thread.init()
重点部分来了,这里的 self._func = func(*args)
搭配前面关于 yield
关键字的讲解,应该不那么难理解,可以上下结合理解
class OperatingSystem():
class Thread:
"""A "freezed" thread state."""
def __init__(self, func, *args):
self._func = func(*args) # <--- 运行源代码,并等待到 yield 第一个返回
self.retval = None # retval == return value,下接 OperatingSystem.run() 的后续部分
OperatingSystem.run() PART.2
每当程序 yield
回来,去 match
一个系统调用(如果有的话,给出返回值 retval)
class OperatingSystem():
def run(self):
threads = [OperatingSystem.Thread(self._main)] # <--- PART.1 end
while threads: # <--- PART.2 begin 检查是否还有线程存活
try:
match (temp := (t := threads[0]).step()): # 将控制权交给进程
# temp 是 t := threads[0]).step() 的返回值
# 即 temp 是一个元组 (函数名, 参数),这里的 temp 没有实际用途,只是方便说明
# 下接 OperatingSystem.Thread.step()
OperatingSystem.Thread.step()
将线程进行一步,但这里的一步是指,到下一次系统调用
class OperatingSystem():
class Thread:
def step(self):
"""Proceed with the thread until its next trap."""
syscall, args, *_ = self._func.send(self.retval) # 向对应生成器发送信号,送入 retval
self.retval = None # 清空 retval
return syscall, args # 返回元组 (函数名, 参数),即返回给上文提到的 temp
OperatingSystem.run() PART.3
接下来是 match
到相应系统调用后,检查线程是否结束,做出相应操作
class OperatingSystem():
def run(self):
threads = [OperatingSystem.Thread(self._main)]
while threads:
try:
match (temp := (t := threads[0]).step()): # <--- PART.2 end temp 是一个元组 (函数名, 参数)
case 'choose', xs: # <--- PART.3 begin 随机选择一个元素
t.retval = random.choice(xs) # 这里将 t.retval 设置为随机选取的值,准备作为信号送回线程
case 'write', xs: # 写入 stdout
print(xs, end='')
case 'spawn', (fn, args): # 产生一个新线程,在这个模型玩具中,只需要将其加入到 Thread 的 list 中即可
threads += [OperatingSystem.Thread(fn, *args)]
case 'sched', _: # 非确定性调度
random.shuffle(threads) # 随机排序一下即可(因为从 match 那句话可以看出,永远是选择 thread[0] 进行一步)
except StopIteration: # 某一个线程结束
threads.remove(t) # 移除结束的线程
random.shuffle(threads) # 等同于 sys_sched()
此后,各线程不断运行,直至所有线程全部结束,threads 内容为空,退出循环,运行结束。
小结
再次佩服一下 JYY 老师的这个 50 行操作系统的巧妙,使用了 yield
的机制完美的描述出了系统调用时管、目态(内核态、用户态)的切换
线程 yield
后将控制权交给操作系统(发送 0x80 中断信号),接下来操作系统查询对应系统调用编号(eax 寄存器上的值),执行系统调用并返回结果(到 eax 上)
顺带提一下,x86 架构寄存器命名方式大致如下
0000000000000000000000000000000000000000000000000000000000000000
| rax |
| eax |
| ax |
| ah |
| al |
至此,相信读者已经对整个操作系统就能够有一个很好的理解了(吴恩达:you're already an expert in machine learning.jpg)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!