上周末台风过境时,华为终于发布了鸿蒙操作系统(HarmonyOS),我没看发布会,因为我不喜欢开发者大会上出现的任何经理,所以,我在DOS上玩波斯王子,魂斗罗…试图通过修改ROM调整魂斗罗到30条命… 请注意,余大嘴说的鸿蒙OS并非仅仅指内核,他说的OS颇有Linux发行版的意思,而且更甚。华为可以在没有鸿蒙内核的情况下发布鸿蒙OS,因为PPT上说了,鸿蒙OS可以跑在安卓Linux以及鸿蒙内核之上。 XXOS不等于XX内核,XXOS完全可以使用YY内核,就比如说GNU/Linux,GNU软件是可以跑超级多的内核上的,甚至Windows内核,GNU/Linux特指一个操作系统,该系统以Linux内核为核心,GNU软件跑在该Linux核心上。 我不是鸿蒙生态上的应用开发者,我对开发也不感兴趣,所以本文不谈和系统生态开发有关的事,本文只是闲谈。在下面一篇文章中,我会专门通过Minix内核介绍微内核,但不是这篇。 本文想再谈谈关于人机交互操作系统本身以及微内核,调度等操作系统比较核心的问题。 也许,鸿蒙内核确实对调度算法进行了精心的设计,这一点是余大嘴自己说的,我也希望是这样,但是仅仅从一张PPT上也看不出什么究竟。说好的开发者大会只是展示了个PPT,肯定是要挨喷的。 我之前在对比Windows和Linux的调度机制的时候也说过类似的,不知不觉就被怼了(不知道是我没说清,是我说错了,还是别人没有get到点,反正大多数人是没有看完就怼的,因为怼的内容和调度算法毫无关系)…不过无所谓,作为一个非科班出身的行业外部人员,从没有真正涉足过操作系统的正儿八经的研发,纯粹是兴趣能让自己有这点儿认知,已经很满足了。当然,我本身也对开发不感兴趣,所以,我不是开发者,我也不是华为的人,本文提到的鸿蒙只是一个引子,就算没有鸿蒙,没有华为,也都无所谓。 鸿蒙也好,Windows也罢,其实都是针对用户体验的,在PC时代以及互联网时代的客户端,强调的是人机交互,在IoT时代,可能要换一个词,毕竟操作者可能不再是人了。不管怎样,作为客户端而不是进行大量运算的服务端,系统强调的东西都是一样的,这种客户端上运行的系统,我统称为 客户端操作系统 ,相对而言,UNIX/Linux这种,我称为 服务器操作系统 : 服务端操作系统 强调系统总体的高并发,高吞吐。 客户端操作系统 强调前台任务的低延时,快速响应。 是的,它们非常不同,因为它们的受众不同。当然了,Windows也搞过Server版本,Linux也搞过桌面发行版,但效果都不是很好。 在如今动不动就SMP,NUMA,众核场景,UNIX/Linux这种强调高并发的服务端系统更容易遭遇并发问题,即 锁的问题。 可以这么说吧,服务端操作系统需要一种 自组织协作机制 ,在多个服务进程之间同时公平分配资源,每个进程都必须尽力索取资源但又不能太贪婪,设计这么一种算法是非常复杂的。 相比之下,客户端操作系统需要是的资源的中心调配, 系统知道当前哪个进程是前台进程,谁更重要。 微内核最适合干这个了。 我觉得微内核更像是在做资源的统一调配,而非协作式争抢。这点和PCI总线和PCIe总线之间的差异有点类似,同样的对比例子还有总线以太网和交换式以太网之间的差异。 说白了就是仲裁,说白了就是谁能在特定时间占有资源的问题。客户端系统对这个问题的回答非常明确,但是服务端却不得不在多个并发服务之间做公平资源调配。 Windows可以做到将资源倾斜给前台任务,Linux不行。 Linux可以做到资源的公平份额调度,Windows不行。 Linux不行的地方,Androidu也不行;Windows不行的地方,鸿蒙可能也不行。 所以说,分开就好了嘛,根本就不是一类东西,何来对比。 说说多核心场景Linux内核的同步问题。 首先,让我们看看Linux内核的实时性。 Linux内核被看作是非实时内核,这是为什么呢? 我们知道,实时内核需要 任务的调度是可预期的 。但是Linux内核做不到,因为Linux内核中有明确的 不可抢占点 。而设置不可抢占点的权力却是所有内核代码以及驱动模块所共有的,比如,只要下面的语句: spin_lock(&lock); //...区间A spin_unlock(&lock); 那么区间A便是不可抢占的,而区间A的执行时间不可预期,这取决于代码是怎么写的,所以实时任务即便就绪,它被调度的时间也是不可预期的。 Linux内核的调度机制并没有规定开发者必须如何如何写代码,更没有规定不可抢占的区间的最长执行时间,所以Linux内核调度器本身便无法确保一个任务从就绪到被真正调度的最长时间,为了使这段时间原则上不会太久,Linux内核开发手册上只是定性的做了书面规定: spin_lock的时间不能过长。 为什么不让调度器把所有权力收回呢?这样仅仅设计一个可以用数学证明其正确性的算法就可以满足任务调度的实时性需求了。 spin_lock区间不可抢占的原因是可能造成死锁,特别是中断上下文本身就是不可抢占的,因此中断上下文只能用spin_lock。 鉴于此,只需要做两点就可以完全收回调度权力了: 中断处理线程化。 可被抢占的信号量替代spin_lock。 事实上,Linux 5.3的实时补丁就是这么做的。 说完了Linux内核的实时性,再来看下Linux内核中锁的并发性能。 做服务端开发的基本都曾遭遇过spin_lock热点导致CPU利用率100%的情形,为此可能还加了不少班吧。但是究竟问题的根源何在? 在我看来,Linux的两类上下文是问题的根源: 中断上下文 进程上下文 特别是网络收发逻辑,中断上下文的路径过长且过于复杂。关于这一点,参见: Linux内核UDP收包为什么效率低?能做什么优化? 越是CPU核心多的环境,两类上下文的数量就越多,Linux内核作为现代操作系统内核强调进程上下文之间的隔离,而现代多队列网卡的CPU亲和性也强调cache的隔离,因此所有的两类上下文其实是无法进行协作的,因为它们之间显然不可能达到任何共识,而协作的前提就是共识。 多个上下文在互不知情的情况下访问共享资源,不晓得别人在干什么,只能是争锁了,这非常类似于早期以太网的CSMA/CD协议,借鉴以太网此后的经验,从总线式进化到交换式以太网,其根本就是增加了一个仲裁者-交换机! spin_lock缺一个仲裁者!有仲裁就能有序排队,有队列就能调度,这便是微内核的优势。 微内核将特定的工作,比如网络协议栈,比如文件系统独立成一个单独的进程,这个进程就相当于仲裁者,相当于一个交换机。 以文件系统为例,微内核Minix2中的文件服务进程叫做FS,FS进程负责所有的整个系统的其他用户进程的文件IO服务,所有的文件操作均在这一个进程中进行,因此它便可以将不同进程的文件IO进行某种有序的排队,然后实施某种调度策略,实现有区别服务。 微内核的文件系统进程就相当于一个交换机! 【 关于微内核的话题,我接下来单独写一篇文章阐释。 】 这非常不同于Linux内核作为宏内核的表现,在Linux内核中,文件IO是在各自的进程上下文中进行的,而底层的文件系统以及磁盘是共享的,无仲裁的系统只能靠锁来保证共享资源的同步访问。【如果是异步文件IO,虽摆脱了进程上下文,但同理,各个异步IO上下文无仲裁操作共享数据】 宏内核的文件系统就相当于一条总线! 操作系统的调度,说到底就是一系列时间和空间资源的分配问题,说起来就是这么简单,但是实施起来,这却比打铁炼钢不知难了多少倍,这是计算机科学中最美的地方,到处是trade off,又到处是trick。 不得不再次谈谈任务调度的问题了。 不得不说,我看过的最好的操作系统方面的教材就是《操作系统导论》了,人称OSTEP,这本书第一次将所有的调度算法按照其脉络统一了起来。其它的教材只是先简单罗列了各种调度算法的概念,然后挑个别重要的各个进行深入分析,但是基本上第二天就忘了… 首先,N个任务,其执行时间分别为T1,T2,T3…,Tn,那么总体看来,在多任务系统中,所有任务总的执行时间当然是大于等于
了。 考虑到任务的总体等待时间,采用 最短任务优先 便是合理的。 最短任务优先 落实都实现,那便是 最早完成任务优先 。 平滑的算法就是一个无级变速的实现,那便是加入了 抢占 ,这便让任务的调度和任务的到达时间无关,因为调度是随时的,只要 新到达任务完成时间最近! 然而,现实中,我们无法在一个任务确实完成之前来预评估其完成时间,所以需要某种启发式算法。 啊哈,又是启发式算法。但是,实施某种启发式算法之前,考虑另外一个问题。 当人们认可并选择了分时系统时,人们其实是为满足了一个特定的刚需而celebrate。 人们更多的希望坐在终端前交互式操作计算机并随时得到反馈,而不是运行批处理,此时人们要求计算机能快速响应操作! 于是 时间片轮转 便是一种呼之而来的算法,显而易见。 于是,时间片轮转 和 抢占式最早完成任务优先 结合,形成了一套还不错的理论,超级好,以至于人们希望赶紧实现它! 然而,回到最初的问题,人们无法预估任务的完成时间,于是现实的算法成了: 时间片轮转+启发式算法 这就是 多级反馈优先级队列 了。可以说,多级反馈队列由时间片轮转+启发式算法演化而来的,它试图满足最短任务优先的要求 。 多级反馈队列算法将新任务放在最高优先级上,如果一个任务是一个短任务,那么它将很快结束,符合 最短任务优先 的约束,如果这是一个长任务,那么它将逐步下降到更低的优先级,最终被判定为长任务,依然符合约束,因此,多级反馈队列算法是一个自适应的算法。 通过研究各种主流的操作系统调度算法,我们发现,Window的调度算法更像是标准的多级反馈队列,而linux内核不是。 干嘛非要把一个进程的生命周期看作是一个任务的始终?把一个进程的两次不得已的IO间隔当成一个任务不更好吗?嗯,Windows看来就是这么做的。 最早完成任务优先+抢占式时间片轮转调度 ,它们和Windows的GUI交互行为结合在一起,辅助以 优先级提升 ,便成就了 GUI操作超级流畅不卡顿的Windows 。 反观Linux内核调度,虽然也伴随有 动态优先级提升或者降低 ,但是却丝毫没有表现出 多级反馈队列 的特征,不信请去自行分析O(1) O(1)O(1)算法源码。 如果以任意任务连续两次IO之间的执行绪作为一个任务,最早结束任务优先,以两次IO间隔为期,大概率,交互应用基本都是在IO完成后迅速完成,这种行为完全符合上述的算法描述,加上时间片(加入一个进程肯定要拖慢处理器其时间单位,这无可厚非)轮转,这已经基本将处理器对待任务的周转时间榨取到了极致。唯一无法预测的问题,就是交互进程下一次IO何时进行,因此,多级反馈队列是预测好不好的问题,而不是对不对的问题。 嗯,像以上的描述,调度理论基本上就统一在了一个一致的需求框架里,Linux和Windows竟然能和该框架完全匹配的上,不错。
|