用协程模拟操作系统

David Beazley 在 PyCon 上进行过一场关于协程的演讲,他在演讲中演示了如何使用 Python 的协程机制模拟一个多任务的「操作系统」。这两天玩 TIC-80 的时候发现 Lua 也在语言层面上支持协程,同样可以用来实现这个「操作系统」,干脆就当成是一个 Kata 吧。

代码:manque-os

与真正的操作系统相比,这个小型的「操作系统」可简单多了。系统中一共有三种角色:

  • 任务(Task)

    一个任务就是一个包裹着协程的对象。当一个任务被创建之后,它会获得一个 ID,然后被放入等候队列中,以供调度器进行调度。当一个任务被调度器从等候队列中选出来运行时,它会执行成员函数 run(),唤醒它包裹的协程并传值给它,此时协程接收参数并开始执行,直到遇到下一个 yield。遇到 yield 语句后,控制权将返回给调度器,因此一个 yield 语句可以看成是操作系统中的陷阱(Trap),而调度器在这里充当操作系统的角色。

  • 系统调用(System Call)

    既然是操作系统,那就肯定少不了系统调用。在真实的操作系统中,系统调用是应用程序请求服务的一种方式。而在这个项目中,系统调用由任务发起,也就是让调度器根据任务执行的返回值来决定开启哪种系统服务。而任务执行的返回值其实就是任务中包裹的协程中 yield 语句的返回值,因此系统调用本质上是由任务中包裹的协程通过 yield 语句返回一个系统调用对象来发起。通过定义一个系统调用父类,再派生出一系列具体的系统调用子类,即可提供各种系统服务。此项目中实现的系统调用包括:创建新任务、获取任务 ID、结束任务,以及等待某个任务完成。

  • 调度器(Scheduler)

    调度器中存储了一个等待队列,一个任务字典(保存任务 ID 到任务的映射)和一个等待结束字典(保存任务 ID 到其等待结束队列的映射)。像之前说的那样,任务创建后会被放入等候队列,调度器会循环选择等候队列中的任务来执行。任务执行后(任务执行完毕或者遇到 yield 语句)会给调度器返回结果,调度器根据返回值执行相应的操作。如果返回值表明任务执行完毕,则调度器让任务结束;如果返回值是系统调用,则执行相应的系统服务;而如果是普通的返回值,则将任务再次放入等候队列。特别地,如果调度器得到「等候某个任务完成」的系统调用,则调度器会根据任务 ID 找到需要被等待的任务对应的等待结束队列,并将发出系统调用的任务放入该队列中。当需要被等待的任务结束后,调度器会将其对应的等待结束队列中的任务再次放入等候队列。

附:与 Python 版本的不同

  • 协程的结束方式

    在「结束任务」的系统调用中,需要终止协程的操作,但是 Lua 没有提供从外部显式终止协程的接口(WHAT THE HELL)。一种解决办法是等待协程自然结束,这种方法显然不符合要求;另一种方法是向协程发送某个哨符值,让协程抛出异常而终止,这种方法又不太优雅。后来在 Stack Overflow 上学到一种方法:利用调式库中的钩子机制为协程注册一个抛出异常的钩子函数,当协程进入新的一条语句准备执行时,钩子函数会抛出异常并终止协程。

      debug.sethook(your_coroutine, function()
          error("almost dead")
      end, "l")
    
  • isinstance 函数的代替品

    由于 Lua 的中类的继承由元表实现,因此可用 getmetatable 函数来代替 Python 中的 isinstance 函数。

      # Python
      isinstance(result, SystemCall)
    
      -- Lua
      getmetatable(getmetatable(result)) == SystemCall
    

Updated: