浅析 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)

posted @ 2023-07-12 18:35  LacLic  阅读(362)  评论(0编辑  收藏  举报