操作系统何以至此:时间与等待,抽象和幻象

chaos 2025-06-25 三 10:13

操作系统应该算是计算机里最复杂的程序,而编写同步程序算是最复杂程序之一

从 eval/apply 到 fork/wait

等待是一种程序的特殊排列组合

交互需求和中断处理程序

早期的计算机实际都是单线程(或进程)的,那时候程序大多是用来解方程。编写好程序装载到机器上后,机器一旦运行起来,除非程序执行结束或者手动拔电源,基本是不会停下来的,这种机器被称为批处理机器。

最初的交互需求更多来自于 cpu 和其他设备(比如硬盘、打印输出装置)之间速度的不匹配,有些程序涉及读取大量数据的过程,由于硬盘 速度慢,cpu 大部分时间在等待数据,那么在后面排队的人就会想,“既然 cpu 都在等待了,不如让我的程序先算,我的程序 不涉及太多数据读取或保存操作”。所以一种解决 cpu 浪费的方法就是事先调度,假设多人提交了同一台机器的使用申请,那么可以估算各个任务的运行时间和 cpu 的使用率,计算时间短或 cpu 使用率高的程序可以先执行,这就会引伸出许多静态的任务调度算法。

但申请什么时候来有可能是一个概率性事件,如果当前只有一个申请,那么只能按先后顺序去执行。一种极端的情况是,A 申请了一个需要计算 3 天的任务,其中大部分时间还是花在数据读写上,机器刚运行起 A 的任务,B 就来了,他申请运行一个只需要计算 30 分钟的 cpu 密集型任务,那么由于机器没法中途停下来, B 的任务就只能等三天后才能执行。

这么看,人们还是需要一种能够随时停止程序的方法,也就是在程序运行中和机器进行交互,这样即便 A 的程序已经运行了,但工作人员可以中途暂停 它(需要把该程序运行的状态都先保存起来),把 B 的任务加载到机器上算 30 分钟,然后状态恢复 A 的状态继续执行 。

这就引出了对程序的中断处理,而中断处理程序最终发展成了如今的(分时)操作系统。

由于 cpu 运算的速度远远超过人类一般的反应速度,依托于这种极为悬殊的差距, 当前的操作系统的运作几乎像是一种降维打击般的魔法。

比如,由于连续的图片每秒刷新 24 次以上,人就会觉得看到的对象是流畅 的、动态的视频,而 cpu 频率是每秒 1G 次以上,假设 cpu 切换上下文(也就是把正在运行的程序的状态保存起来然后读取下一个程序状态并运行)的频率是 cpu 主频率的 1/1000, 即切换一次需要花费 1000 个 cpu 时钟周期,cpu 也可以在一秒钟内切换任务 100 万次,而即便把屏幕的刷新率提高到 100 ,cpu 也只需要每 10ms 去更新一次屏幕区内存,然后显示器读取内存重新按行绘制一遍(重绘一次的时间和刷新率成反比,比如 1000ms/100=10ms 表示显示器按行从上到下更新一次画面需要 10ms),而 10ms 里 cpu 还能执行 1 万条其他任务的指令。因此对于大部分简单计算任务,在刷新屏幕的间隔里就可以做完,所以现在才可以一边看视频一边运行其他程序(比如浏览器,各种后台进程)。

对于人的输入也是如此,一般人输入一个字符的时间可能是数百毫秒,一秒钟最多敲 10 个字符,这比屏幕刷新率还要低一个数量级,因此 cpu 只要在一秒里拿出 10 万分之 1 的时间去读取这个键盘敲击事件,然后再花 1 万分之一的时间把这个事件转成对应的字符编码去更新显示器的内存区,人们就会觉得自己在流畅的打字了,而实际上 cpu 大部分时间是在继续做别的事情。

在这种模型下,前文提到的 A B 程序调度的问题就变成这样:

  • 机器开机后一直在运行着,但它随时可以响应键盘和鼠标事件,也可以更新屏幕显示的内容,这只花了它万分之一的精力
  • 管理员把程序 A 手动敲入机器中,机器完全不需要“停下来”(当然如果把处理一个字符输入称为停下来的话,它实际 1s 里停了十多次)
  • 开始运行 A 程序后,每一秒机器还是会花费万分之一的时间去均匀地更新屏幕一百次以及监听键盘鼠标事件 10 次,大部分时候它都没有读取到新的键盘事件,而这一秒其他时间都在执行 A 程序。
  • 在某一段时间里,机器感知到管理员在把 B 程序一个一个字符地敲入电脑,屏幕上也一直在刷新代码编辑器里的代码。
  • 之后 B 程序也运行了起来,这意味着 cpu 在 1s 钟里要额外分出时间去执行 B, 比如每一秒机器仍然花万分之一的时间均匀地更新屏幕 100 次、监听键盘鼠标事件 10 次,但剩下的时间,有一半是在执行 A, 另一半是在执行 B 。
  • 由于 A 程序经常需要去读取硬盘,甚至把数据从硬盘里某个区域移动到另外一个区域,cpu 往往发完 指令给硬盘后,要等待 1 万个 cpu 时钟周期后硬盘才把数据返回到特定内存,因此实际上 cpu 在执行到这种指令后,可以马上切换去执行 B 程序(花费掉 1000 个时钟周期用于保存上下文),然后执行 8000 步 B 程序,再花 1000 周期切换回来,尽管耗费了 2000 个周期用于切换,但这期间多执行了 8000 步 B 程序,这还只是不到一秒钟 里发生的事情。
  • 最终,从人的角度看,没有任何程序停下来了,机器没有任何卡顿迹象,它能接收用户打字,刷新屏幕,并且 B 虽然比 A 更晚到,但 1 小时后就执行完成了。
radioLinkPopups

如对本文有任何疑问,欢迎通过 github issue 邮件 metaescape at foxmail dot com 进行反馈