前天看一本 Gitbook ,说操作系统有四个基本抽象:中断、进程、虚存、文件,它们是操作系统设计的基础。
为什么一定要有这四个基本抽象?我想谈谈自己浅薄的理解以及感想。
为什么一定要有进程的存在?
目的大家都知道:为了让单个处理机同时处理多个有独立功能的任务、尽量提高处理机的利用率。
如果大家没有选择让操作系统来统筹调度多任务的话,计算机行业会选择什么样的替代方案?
我能想到的方案是用更多的处理机,一个处理机当然只做一个任务。发展到今天,可能会是这样的情况:今天我去买了 intel 最新的 256 核处理器,可以同时处理更多的任务;新的单机游戏 GTA5 要求至少128核处理器才能跑起来,因为它有 80 个线程在工作。诸如此类。
但是计算机行业的选择并不是这样!我们把复杂的逻辑加在操作系统上,发明进程这个概念,让单个处理机同时处理更多的任务。
有句话经常被用来嘲讽不停“学习”却成绩差的“笨”学生:不要用战术上的勤奋,掩饰战略上的懒惰。计算机的发展正是如此。很多问题有简单粗暴的解决办法也有复杂巧妙的解决办法。简单粗暴比如进程没有被设计出来,大家需要依赖行业发展出多核处理器;比如项目性能不行,我们要依赖加钱加机器。复杂巧妙则是在现有的技术条件下对运作方式进行改良。
假如计算机行业的发展是简单粗暴的,那么虚存、文件系统都是这样:
虚存的存在是为了实现三件事:让内存看起来更大、让程序使用看起来连续的地址空间、让各个程序使用的内存互相隔离。如果没有用虚存这个方案,简单粗暴的“操作系统”将会:给程序分配连续固定且足够大的空间。这样利用率相比现在低了很多,不够的时候怎么办?加内存。
文件系统也是如此,简单粗暴的“操作系统”让数据连续摆放,没有分块这种事情存在,你看到的数据的顺序和物理上一致。不够的时候怎么办,加硬盘。
计算机行业用战略上的勤奋,让我们有了今天这样复杂的操作系统和廉价的个人电脑。
用户态线程
把目光向上加一层。
我自己的操作系统课本里有“用户态线程”这个概念,解释为不能被操作系统感知的线程,和我们常说的“线程”有很大不同。按照这个概念,协程就是用户态线程。每个协程有自己独立的上下文,可以在当中被切入切出。不过这个调度不是由操作系统来做的(操作系统根本无法感知),Python 的协程让用户来做显式的切换,Go 语言则是在语言级别有一个调度器。
再抽象一点,用到回调的编程方法时,系统向程序通知一个事件,程序让出当前的顺序的代码的执行权,进入设置好的回调函数的栈上下文,也是一个独立的“用户态线程”。只不过它的入口和出口固定了。
WEB 后端编程喜欢用多线程,来一个请求,就开启一个独立的线程。开一个新线程是很简单的事,当它阻塞的时候就让它阻塞好了,因为操作系统很聪明,它发现这个线程阻塞了,然后会把处理机的执行权和上下文切换为其它线程。但是量足够大时,线程的创建和销毁是很大的开销,因为这两件事要经历内核态和用户态的切换。于是我们有 I/O 多路复用,我们有线程池,减少这个开销。但是量再大时,到 C10K 问题的出现,则是线程的切换成本已经成为了巨大的负担。
我认为不停地开线程,就是战略上的懒惰。使用协程(或者用户态线程)则是战略上的勤奋,它把切换的工作交给了自己而不是操作系统。当我执行一个网络或者文件操作时,我知道马上会是阻塞的,于是标记一下,然后立马切到其他上下文中执行其他协程;这个操作标记完成时,再在其他协程让出执行权时切换回来。这样,协程不需要像系统线程一样保有优先级以及多核的负载均衡等信息,也不需要在切换协程时在内核态和用户态之间切换。复杂了自己程序的逻辑,节省了负担。
不用战术上的勤奋,掩饰战略上的懒惰。“战术上的勤奋”是各种意义上资源的浪费,战略上的勤奋则是资源的节约。所以今天高科技的各种廉价,感谢商业的法则,感谢计算机行业的人才,也期待 Go 语言以及各种自带“用户态线程”语言的前途。