Python 建模操作系统
在中断或是 trap 指令后,通常由一段汇编代码将当前状态机 (执行流) 的寄存器保存到内存中,完成状态的 “封存”。
本讲内容:一个 Python “操作系统玩具” 的设计与实现,帮助大家在更高的 “抽象层次” 上理解操作系统的行为。这个 “玩具” 将贯穿整个课程
理解操作系统的新途径
回顾:程序/硬件的状态机模型
计算机软件
- 状态机 (C/汇编)
- 允许执行特殊指令 (syscall) 请求操作系统
- 操作系统 = API + 对象
计算机硬件
- “无情执行指令的机器”
- 从 CPU Reset 状态开始执行 Firmware 代码
- 操作系统 = C 程序
画状态机
状态和状态的迁移是可以 “画” 出来的!
- 理论上说,只需要两个 API
dump_state()
- 获取当前程序状态single_step()
- 执行一步
GDB 可以做这事,但检查状态有缺陷,主要是太复杂:
- 状态太多(指令数很多)
- 状态太大(很多库函数状态)
简化:把复杂的东西分解成简单的东西
strace
:只关注系统调用
重要的概念
- 应用程序 (高级语言状态机) + 系统调用 (操作系统 API) + 操作系统内部实现
通常的实现思路:真实的操作系统 + QEMU/NEMU 模拟器
我们的思路 - 应用程序 = 纯粹计算的 Python 代码 + 系统调用
- 操作系统 = Python 系统调用实现,有 “假想” 的 I/O 设备
操作系统 “玩具”:设计与实现
Python 是单线程执行的,即使用 Thread 也因为有全局锁。
操作系统玩具:API
四个 “系统调用” API
- choose(xs): 返回
xs
中的一个随机选项 - write(s): 输出字符串
s
- spawn(fn): 创建一个可运行的状态机
fn
- sched(): 随机切换到任意状态机执行
除此之外,所有的代码都是确定 (deterministic) 的纯粹计算
- 允许使用 list, dict 等数据结构
操作系统玩具:应用程序
操作系统玩具:我们可以动手把状态机画出来!
count = 0
def Tprint(name):
global count
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])
sys_write(f'#Thread = {n}\n')
for name in 'ABCDE'[:n]:
sys_spawn(Tprint, name)
sys_sched()
实现系统调用
随堂代码:os-model.py
- 不支持
sys_*
的系统调用的返回值;
有些 “系统调用” 的实现是显而易见的
def sys_write(s): print(s)
def sys_choose(xs): return random.choice(xs)
def sys_spawn(t): runnables.append(t)
sys_sched() 的难点:
- 封存当前状态机的状态
- 恢复另一个 “被封存” 状态机的执行
Python 语言机制:Generator objects (无栈协程/轻量级线程/...)
def numbers():
i = 0
while True:
# f'{i:b}' 将i转为2进制
# send 中参数会替代 yield 赋值给 ret
ret = yield f'{i:b}' # “封存” 状态机状态
i += ret
使用方法:
n = numbers() # 封存状态机初始状态
n.send(None) # 恢复封存的状态(或者 next(gen)),开始时只能发送 None,输出 ‘0’
n.send(0) # 恢复封存的状态 (并传入返回值,用于赋值给 ret)
如果send()
没有产生下一个值,则抛出 StopIteration
;
玩具的意义
并没有脱离真实的操作系统
- “简化” 了操作系统的 API
- 在暂时不要过度关注细节的时候理解操作系统
- 细节也会有的,但不是现在
- 学习路线:先 100% 理解玩具,再理解真实系统和玩具的差异
void sys_write(const char *s) { printf("%s", s); }
void sys_sched() { usleep(rand() % 10000); }
int sys_choose(int x) { return rand() % x; }
void sys_spawn(void *(*fn)(void *), void *args) {
pthread_create(&threads[nthreads++], NULL, fn, args);
}
建模操作系统
一个更 “全面” 的操作系统模型
进程 + 线程 + 终端 + 存储 (崩溃一致性)
系统调用/Linux 对应 | 行为 |
---|---|
sys_spawn(fn)/pthread_create | 创建从 fn 开始执行的线程 |
sys_fork()/fork | 创建当前状态机的完整复制(进程) |
sys_sched()/定时被动调用 | 切换到随机的线程/进程执行 |
sys_choose(xs)/rand | 返回一个 xs 中的随机的选择 |
sys_write(s)/printf | 向调试终端输出字符串 s |
sys_bread(k)/read | 读取虚拟设磁盘块 *k 的数据 |
sys_bwrite(k, v)/write | 向虚拟磁盘块 k 写入数据 v |
sys_sync()/sync | 将所有向虚拟磁盘的数据写入落盘 |
sys_crash()/长按电源按键 | 模拟系统崩溃 |
模型的简化
被动(通过sys_sched
)进行进程/线程切换
- 实际程序随时都可能被动调用
sys_sched()
切换
只有一个终端
- 没有
read()
(用 choose 替代 “允许读到任意值”)
磁盘是一个 dict
- 把任意 key 映射到任意 value
- 实际的磁盘
- key 为整数
- value 是固定大小 (例如 4KB) 的数据
- 二者在某种程度上是可以 “互相转换” 的
模型实现
随堂代码:mosaic.py - 500 行建模操作系统
- 原理与刚才的 “最小操作系统玩具” 类似
- 进程/线程都是 Generator Object
- 共享内存用 heap 变量访问
- 线程会得到共享 heap 的指针
- 进程会得到一个独立的 heap clone
- 输出程序运行的 “状态图”
- JSON Object 可读
- Vertices: 线程/进程、内存快照、设备历史输出
- Edges: 系统调用
- 操作系统就是 “状态机的管理者”
建模的意义
可以把状态机的执行画出来了!
- 可以直观地理解程序执行的全流程
- 可以对照程序在真实操作系统上的运行结果
这对于更复杂的程序(多线程并发问题)来说是十分关键的
void Tsum() {
for (int i = 0; i < n; i++) {
// 这里是因为建模的代码每行都是原子执行的(不会发生线程切换),因此采用临时变量的方法
// 实际上即使 sum++ 会被汇编成多条指令,并不是原子执行的,但这个建模的操作系统做不到
int tmp = sum;
tmp++;
// 假设此时可能发生进程/线程切换
// sum 最终的值并不和期望值一致,多线程的并发问题。
sum = tmp;
}
}
课后实践
阅读、调试 os-model.py 的代码,观察如何使用 generator 实现在状态机之间的切换。
在现代分时操作系统中,状态机的隔离 (通过虚拟存储系统) 和切换是一项基础性的基础,也是操作系统最有趣的一小部分代码:
- 在中断或是 trap 指令后,通常由一段汇编代码将当前状态机 (执行流) 的寄存器保存到内存中,完成状态的 “封存”