分享

深度解读:为什么Win/iOS很流畅而Linux/Android却很卡顿?怎样才能不卡顿?

 InfoRich 2019-09-03
先说是不是,再问为什么。
我就知道有人会这么说,然而那样就成了一篇议论文了,而我只是想写一篇随笔。所以,不管事实是不是那样,反正我就是觉得Windows,MacOS,iOS都很流畅,而Linux,Android却很卡。当然了,这里说的是GUI,如果考量点换成是Web服务的吞吐和时延,那估计结论要反过来了,不过那是客户端程序感觉到的事,作为人,who care!
我写这篇文章还有一个意思,那就是想牵引一个话题,如果我们想把Linux,Android(当然,Android内核也是Linux)优化到GUI不再卡顿,我们应该怎么做。
大概是去年,一个炎热的午后,吃过午饭我和同事们在公司附近晃悠,就讨论 “为什么苹果手机就不卡,安卓手机不管多贵都很卡。”记得一位同事说,iOS在GUI方面做了很多的优化,而Android却没有。
这话说对了!不过更为重要的一点是, 不谈具体场景谈优化,都是瞎折腾!
Windows也好,iOS也好,都知道自己的应用场景,因此针对自己的应用场景做了优化之后,妥妥在自己拿手的场景下甩Linux在该场景下的表现几条街了。
下面开始正式的技术层面的分析之前,先声明几点。
本文并不是在说Linux系统总体上很卡顿,而只是说Linux系统桌面版的GUI程序相比Winddows很卡顿,如果真觉得本文是在喷Linux,那就当是喷Linux桌面的吧。

本文不准备讨论X window和Windows窗口子系统一个在用户态一个在内核之间的差异,这无关紧要。我的想法是,即便是你将X window扔进内核,现有的Linux内核处理GUI,该卡顿还是卡顿。

本文仅从调度算法的角度来评价为什么Windows/iOS不卡顿而Linux却卡顿,当然还有别的视角,但并不是本文主题。

Windows内核调度的线程而不是进程,但是本文统一采用进程这个术语,没有别的原因,只是因为进程的概念是和现代操作系统概念相始终的,而线程是后来的概念。
先看服务对象,仅此就将Windows,MacOS/iOS和Linux的使用场景区分开来:
Windows/MacOS/iOS系统,主要是被人操作,用来提供写文档,游戏,做报表,画图,上网浏览,视频播放等服务。

Linux系统,
主要提供网络服务,用来支撑各种远程的客户端,为其提供数据处理和查询,数据生成,数据存储等服务。

事实证明,Linux在其专业的领域已经做的足够好,但是问题是,为什么它在GUI处理方面却总是一直很糟糕呢?这就要看具体场景的差异了。
对于网络服务而言,其场景的行为是可预期的,我们可以将这些场景简单归结为:
1、公平快速处理网络并发请求。
2、公平快速处理并发磁盘IO。
3、高吞吐CPU密集型数据处理与计算。

Linux优秀的O(1)O(1)调度器以及后来的CFS调度器可以非常完美的cover上述三个场景,至于说为什么,不必多说,简单归纳如下:
1、无论是O(1)O(1)的基于优先级的时间片轮转还是CFS的基于权重的时间配额,均可以既满足优先级的差别服务需求又保证高吞吐率,这些都来自于调度器本身而不是依靠频繁的切换。

2、额外的简单启发式奖惩机制可以让网络IO以及磁盘IO的响应度更高,同时又不影响CPU密集型计算服务的高吞吐。

上面的第二点是一个额外的辅助,照顾IO过程快速获得响应,这是一个非常棒的辅助,但是注意,再棒的启发式算法也总是辅助性的,提高响应度就是个辅助性的锦上添花的功能,以高吞吐为目标才是根本。
IO过程对于一台Linux服务器而言是与外界交互的唯一渠道,通过该渠道可以将处理好的数据送出到网络或者磁盘,同时从网络或者磁盘获取新的数据,换句话说, IO过程类似一道门。但也仅仅是一道门。
照顾IO过程获得高响应度这件事是为了让门开得更大,通行效率更高!
熟悉Linux内核调度器变迁的都应该知道O(1)O(1)到CFS过渡的这段历史,即2.6.0内核开始一直到2.6.22为止的这些版本,采用Linux内核划时代的O(1)O(1)调度器,随后由于两个原因:
1、O(1)O(1)调度器动态范围太大或者太小。
2、IO补偿机制不到位,时间片分配不公平。

为了解决这些问题,Linux内核切换到了CFS调度器。
切换到了CFS调度器,事实上,人们更多指望的是CFS能够让进程时间片分配更加公平,多个进程运行更加平滑,如此一来,上GUI界面的话,岂不是就不卡顿了。
然而还是卡顿,本质原因是,场景根本就不对路子。
在Linux服务器的场景中,优先级和时间片是正相关的,无论是O(1)O(1)调度器的静态线性映射的时间片,还是CFS的动态时间配额,都是优先级越高的进程其每次运行的时间也就越久,但是实际上,这两者并不是一回事。
在更复杂的场景中,正确的做法应该是参考 时间管理的四象限法则 来设计进程调度器。其中:
优先级表示紧急性。
时间片表示重要性。

于是,如果不是因为Linux服务器场景过于单一简单,CPU的时间管理要复杂得多,比如调度器应该按照四象限法则设计成下面的样子:
1、处理重要且紧急事件的进程,需要赋予高优先级分配长时间片去抢占当前进程。
2、处理重要但是不紧急事件的进程,保持固有优先级分配长时间片就绪等待。
3、处理不重要但紧急事件的进程,提升优先级但不分配长时间片,处理完毕立即返回固有优先级。
4、既不重要也不紧急的后台进程,低优先级短时间片,系统闲了再调度。
后面我们会看到,Windows的调度器就是这般设计的。

我们先总体看看GUI系统的场景。
它的服务对象是人,和Linux的服务场景的行为可预期相反,人的操作是 不可预期 的!
Windows,MacOS/iOS这种Desktop系统的GUI进程,很多时候都是在等待人的进一步操作而睡眠,要么在等鼠标,要么在等键盘,要么在等声卡,显卡的输出,或者就是在将用户输入的信息往磁盘里写而等待IO完成,Desktop系统更多关注的是要对以上这些事件提供高效率的响应服务,而不是系统的数据吞吐。
Desktop在乎的是时延,而不是总吞吐,同时,这个时延还是区分对待的,有些时延的可容忍区间很大,比如网卡(网卡IO之所以优先级提升并不是很多,是因为首先网卡是有队列缓存的,而大多数的报文都是burst而来的,队列缓存可以平滑掉首包延迟,其次,由于光速极限,相比于网络延迟,主机调度延迟真的可以忽略不计。),有些却很小,比如键盘鼠标。所以说,Windows之类的Desktop系统 必须能够区分一个进程当前的紧急性和重要性。
Linux内核能做到这种区分吗?
Linux可以通过计算一个进程的平均睡眠时间判定它是不是一个交互式IO进程,从而决定要不是给它一定的优先级提升,但是也仅能做到这个地步,因为Linux内核无法得到更进一步的信息。
Linux内核不知道一个进程到底是不是IO进程还是说仅仅在一个时间段内有IO行为的CPU密集型进程,Linux内核也不知道一个进程被唤醒是因为键盘的数据到了,还是无关紧要的信号到了,所以这一切,Linux内核只能 启发式预测。
Linux内核仅仅跟踪一个睡眠时间而且还是平均的睡眠时间,是区别不出进程当前的紧急性和重要性的。没有外界的信息输入,仅靠启发预测,当前的AI算法貌似还没有到这个境界吧。换句话说,启发算法是不准确的。你看看Linux内核O(1)O(1)调度器的sleep_avg是如何计算并如何参与动态优先级调整的,就会明白我上面说的意思。
既然Windows系统的GUI操作比Linux流畅,那么想必Windows肯定是做到了进程当前的紧急性和重要性的区分咯?那是当然。它是如何做到的呢?
虽然Windows的调度器也是基于优先级的,也是抢占式的,也是同优先级轮转的,这看起来和Linux并没有什么区别,甚至从4.3BSD开始,几乎所有的操作系统的调度器基本都是按这个思路设计出来的,仅仅从 如何选出下一个投入运行的进程 这个算法上看,几乎所有的操作系统调度器都是一样的。Windows与众不同的原因在于 其对优先级的不同处理方式。
自4.3BSD以来,所有的基于优先级的抢占式调度器的优先级计算都包括两部分因子,即固有优先级和动态优先级:

可以看出,Windows对于不同的事件定义了不同的优先级提升的具体数值, 将动态优先级的值和具体的事件做了精确的关联。
这些数值的定义上,甚至精细而贴心,详细的数值参见ntddk.h:

仔细看,你会注意到对于声卡而言,其IO完成时,优先级提升会很大,而磁盘,显卡这种却并不是很多,这充分体现了设计者的贴心。这充分考虑到了人耳的灵敏度和人眼的分辨率之间的对比,声音是作为流顺序输出的,耳朵很容易分辨出声音的卡顿,而对于图像而言,完全可以慢慢双缓冲刷层,人眼相比之下没有那么高的分辨率识别到,因此声卡事件必须优先处理。同时,对于磁盘,网卡之类的,人就更是感觉不到了。除了声卡之外,键盘鼠标操作的IO完成对于优先级提升的数值也很可观,因为键盘鼠标如果卡顿,人的输入会明显感觉到延迟,鼠标则显拖沓,这都是很容易识别的卡顿事件,所以Windows给予了进程更高的动态优先级来尽快处理这些事件。
对于窗口子系统而言,当一个窗口获得焦点时,对应的处理进程的优先级也会得到提升,这会给人一种 你操作的界面总是很流畅 的感觉,毕竟你操作的界面就是前台窗口,至于说此时后台窗口的处理进程,即便是僵死了你也不会有感觉,因为你并不操作它们呀,当你操作它们时,对应的处理进程的优先级就会提升。
所有的优先级提升都伴随着时间片的重新计算,但是和Linux不同的是,Windows并没有直接将进程优先级和时间片按照正相关关联起来,时间片是独立计算的,大多数时候,Windows对于所有的进程,不管优先级是多少,均采用同一个时间片。
如此看来,Windows虽然也是优先级调度的系统,但是其优先级却是 操作行为驱动的 ,这便是其与众不同之处。
Linux内核调度系统会精细区分磁盘事件的wakeup和键盘鼠标声卡事件的wakeup吗?不会。
说完了Windows为什么操作GUI会很流畅,该说点不好的了,Windows经常会死机,为什么呢?
这很大程度上也和上面描述的调度器有关。
仔细看这个操作行为驱动的动态优先级调度器,很大的一个问题就是容易饿死低优先级的进程,特别是P base很低的进程。
Windows的解决方案是采用一个后台进程(学名叫做平衡集管理线程)轮询的方式,将超过秒级都没有被调度的进程的优先级拉升到很高的位置参与抢占。
这个机制有啥问题呢?问题在于Windows需要第三方线程来缓解饥饿,而不是靠调度器自身,这便增加了调解失败的可能:
第三方线程本身的问题没有按照预期工作。
饥饿进程过多。
饥饿进程优先级提升后又被抢占。


除了死机问题之外,Windows系统对于服务器版本的调度器调整做的也不够优雅,Windows仅仅是调整了服务器版本的系统参数,而几乎没有对调度的算法做任何修改。对于服务器版本,Windows只是将时间片延长了而已,同时几乎不再动态计算时间片,而是选择始终使用相同的一个足够长的值,以减少进程切换提高吞吐率。显然这种方式并不妥当,因为动态优先级根据事件的提升,还是会造成进程间不断抢占,从而影响吞吐。
不过,毕竟Windows是一个Desktop系统,本身就不是为高吞吐而生的,这种针对服务器版本的策略调整便是无可厚非了。正如Linux服务器虽然可以很完美应对高吞吐场景,其Desktop版本比如Ubuntu,Suse不也是心有余而力不足吗?虽然Linux内核也有动态优先级提升这一说。
该简单总结一下了。
在人机关联上,Windows更加靠近人这一端,适应了人的操作行为,为操作该机器的人提供了良好的短时延体验,Linux相反,它靠近机器一端,让CPU可以尽可能开足马力跑task而不是频繁切换,从而为客户端提供最大的数据吞吐。

Windows的设计甚是精妙,考虑到了人的行为的每一个细节(除了对于死机的耐受力),除了动态优先级和具体时间精确关联之外,对于待机恢复时间deadline在7秒内也是很值得拍案,这个7秒的阈值考虑到了人的短期记忆的极限,如果有人突然想到了一个点子,需要打开电脑将其记录下来,那么打开电脑的时间如果超过了7秒,那么可能这个点子就溜走了,所以待机恢复时间必须限制在7秒以内,哇塞不哇塞。
对于MacOS/iOS没有过多的研究,但是可以想见应该也如Windows这般了。因为它们都处在人机关联的人的这一端。随便看了下MacOS的开发手册,找到了下面的段落:




当我找和GUI和调度相关的东西时,就在上面这段的下面,有这个定义(如下图)看来内核也是能看到所谓的前台窗口的。


不管怎么说,Windows,MacOS/iOS这些系统,共同的特点就是 大多数情况下,同时只有一个焦点窗口在前端接受输入输出。毕竟把窗口缩小排满一屏幕的很少见。然后呢?然后这就是一个典型的场景啊!
你看看Win10,不就可以设置为平板模式吗?
倾其机器和操作系统内核所有资源和机制照顾这少数的,几乎是唯一的前台焦点窗口的处理进程,这几乎就是单进程处理啊!然后处理好用户的窗口切换即可,比如Windows的Ctrl-Tab。
Linux如若按照这个思路,单独再写一个调度器,替换掉CFS,而不是增加一个调度类,如此一来将系统中所有的进程统一按照 优先级和事件相关联 的方式对待,我想问题应该能优化不少。
Linux内核O(1)O(1)调度器的历史其实很短暂,2.6初始到2.6.22,但是非常经典的Linux内核方面的书,都是在描述这期间的Linux内核版本,这在当时就给了人们一个假象,O(1)O(1)调度器是无敌的,是划时代的,于是,当有了新的CFS调度器的时候,人们哇塞一声,O(1)O(1)只是银河系级别的,而CFS是宇宙级别的。
但其实,O(1)O(1)的意义只是优化了 如何快速找到下一个要运行的进程 ,虽然它也涉及了动态优先级的计算,但是这并不是它的重点。说实话,你若看看Windows的调度器,4.4BSD,SystemV4的调度器,基本上都是位图加优先级队列的形式,思路几乎是同一个,这么说来都是O(1)O(1)咯,而且人家这些调度器早在Linux还是O(n)O(n)调度器的时候就已经存在好几年了,却无人问津。
Windows内核的调度算法不为人知的原因除了其闭源之外,还有一个原因就是Windows内核方面的技术总体上推广的人太少,国内除了潘爱民一直在致力于这方面的推广之外,在没有别人了。估计是因为大家觉得Windows内核方面,Debug之外的东西,学了也没啥用吧。
你说Linux开源没错,BSD不也开源吗?怎么就没有人注意BSD的调度器实现呢?哈哈,开不开源无所谓,关键得能造势搞事情,而且获取方便,让大家用起来你的东西才真真的啊。Linux2.4版本说实话及其垃圾,但关键是很多人用起来了,这就是全部了。Solaris虽然设计完美优雅,可是有壁垒,没人用,最终也还是凉凉。同样的事情参考以太网。
通篇都在比较Windows和Linux的调度器如何影响人们的操作体验。最后说说iOS和Android吧,题外话,不涉及技术。
Android就是卡,不接受反驳。
再贵的Android机器也卡,三星的,华为的照卡不误,只是相比别的稍微好一点点而已。这意味着它们成不了街机。因为手机是买来用的,不是买来debug的,除了程序员没人在乎Android机慢的原因,即便是程序员也很少有折腾明白的,只是因为这份职业让他不用Android就不正确。不过现在互联网公司的程序员用iPhone的也多了,因为好用啊。再者说了,互联网公司程序员大概率以做业务逻辑为主,底层技术欠缺,无力debug,当然是什么好用用什么,iPhone贵,但是互联网程序员收入高啊。
最终,Android机的唯一优势就是价格,你让Android卖的和iPhone一样贵试试,分分钟被绞杀。要说还有唯一点五的,就是品牌,三星也不是吃素的,就算三星做的再烂,就凭它这牌子,也不缺市场,比如我就是三星用户,我并不是觉得三星的Android比小米的Android好,而是我喜欢三星这个公司,这个品牌,仅此而已。
请继续阅读:

Linux桌面GUI系统的调度器怎么才能不卡顿?


关于太古老的故事,我就长话短说,主要是留下个
UNIX进程调度器从何开始 的印象,这样方便我们理解为什么Linux的进程调度器会是现在的这个效果。

最初,计算机不是分时系统,那时的计算机只能每次运行一个程序,一直到该程序运行结束,中间不能中断。如果谁想利用一台计算机做点事情,那么就必须排队,是的,就像景区游乐设施排队的那种,有时候自己的程序明明只需要执行5分钟,排队可能要排一天,因为有太多的人需要用这台计算机运行5分钟左右的程序了,当然,也有运行8分钟程序的,可能还有运行半小时的,都有。

人们拿着打在孔卡上的程序等待…

优化措施当然是不言而喻,这里面没什么哲学思想,就是普通的套路。让程序自己排队,而不是人拿着程序排队。如此优化之所以可行,有个前提,那就是
程序会自己运行到结束输出结果,程序的逻辑都是编程的人在编程阶段就确定好了的 ,程序运行期间不需要人的干涉。批处理程序就是那时产生的。

程序自己会排队之后,随着程序可能会越来越大,执行时间越来越长,程序之间的运行时间差异变得巨大,这时便有了
程序调度 的需求!比如,让短的程序先执行完,避免它等待太久。

插播一句,我们在大型超市买单结账的地方也总是会看到很多不同的收银台,比如小件物品通道,大宗购物通道等等,这就是调度。
分时系统是如何出现的,这里不细说,反正它就是出现了,时间被分成了很细很细的时间槽,每一个程序占据一个时间槽,用完了时间片就执行下一个程序。在大家把程序都录入系统中后,计算机启动,按照某个调度策略,决定优先执行哪个程序,然后下一个时间槽切换到哪个程序,如此一直到所有程序结束,拿结果,关机。

就这样,历史一直发展到UNIX的出现,进程的概念被抽象了出来, 进程调度系统 作为现代UNIX操作系统的一个独立的子系统正式出现。携带着分时批处理系统的程序调度的基因 , 携带着分时批处理系统的程序调度的基因 。







嗯,UNIX的
基于优先级时间片轮转的抢占式调度器 影响了几乎所有的操作系统的调度器的设计,包括Windows!

我们先看什么是进程优先级。

进程优先级描述了一个进程在
调度时刻的紧急程度 ,一定要强调 “在调度时刻” 这个修饰语,不然在一个进程运行过程中,另一个进程以更高优先级出现,如果不调度的话,即便是更高的优先级也不会有任何作用。

再看时间片,时间片最初的含义是一个进程在
一轮调度周期 所运行的时间,一轮调度周期的意思是 把系统中所有的进程都轮转调度一遍的事件

如果每一个进程的时间片都是相同的,那么它将平滑掉优先级的意义,优先级将变得仅仅影响进程第一次运行的先后顺序,后面的调度轮次中,进程优先级将起不到任何标识差异的作用。

于是,4.3BSD采用了1秒抢占制,强行插入
调度时刻 ,每间隔1秒的时间,用优先级来强制差异化。如此一来,优先级高的进程将会在多次间隔1秒的调度时刻获得更多的运行机会。


再往后,4.4BSD/SRV4 UNIX则采用了更加规整的方式来进行细粒度的优先级调度,采用了优先级阶梯下降的调度策略。标准的策略为:


这种阶梯下降策略的效果就是,优先级越高,其初始位置优先级越高,同一个调度周期内,它执行的次数就越多。


进程执行时间和其优先级成正比。如果你想让调度的动态范围变得更大,那么时间片不再均等便是了, 比如SRV4的时间片就是直接和进程优先级正相关的。

后来的Linux 2.6内核的O(1)调度器更是直接,直接将优先级线性映射到了时间片,以表示差异化。

再后来的Linux 2.6.23之后的CFS调度器则直接将进程优先级映射到了CPU时间份额,以表示差异化。

说来说去,UNIX/Linux的所有调度器,均旨在
让优先级高的进程运行得更久一些!这便是分时批处理系统的调度器的基因!该基因一直被继承到了Linux 5.3!

该调度器基因来自于批处理分时系统的程序的性质以及此类系统的首要目标
,批处理系统的目标在于,最大化系统的吞吐率!

至于说UNIX/Linux各个系统版本的调度器之间的差异,无非是做了些许小的调整:

如何避免饥饿。
如何更加平滑。
如何根据进程的行为动态奖惩其时间片。

…此类无大事…


现在,终于到了说说抢占的时候了。

前面有所述,说起优先级,必然要给一个时间点,优先级才有实际的意义,而抢占正是发生在这个时间点,所谓的抢占就是在该时间点,高优先级的进程抢占掉正在运行的低优先级的进程获得CPU资源。

现在让我们看看UNIX/Linux系统中哪里会有这样的时间点。

我们发现,很少有这样的时间点,几乎没有。Why?

首先,UNIX直接始发于分时批处理系统,系统中的进程都是会
自己运行到结束 的,进程优先级在进程创建时就已经确定,调度完全按照优先级来决定一个调度周期内该进程能运行多久的时间,除了nice系统调用可以中途改变进程优先级外,进程的优先级几乎不变,那么除了新进程进入的fork系统调用以及时钟中断之外,没有任何其它地方会有进程调度的时机,也就是没有任何抢占的时刻,所以抢占是不必要的。

但是且慢。动态优先级呢?不是还有动态优先级吗?什么时候动态优先级会改变呢?

答案是在I/O中断中。本来I/O进程为了等待I/O完成就已经睡眠了,当I/O完成的中断到来时,为了让I/O进程尽快恢复执行处理数据,一般会暂时提高I/O完成进程的动态优先级,这个时刻就是一个调度的时刻,如果提高了优先级后的I/O进程比当前的进程的优先级高,那么抢占将在此刻发生。

无疑,这是一个非常棒的优化,但是发生抢占真的会有收益吗?要知道抢占的代价是刷掉当前进程的CPU cache,TLB信息等等,至少在X86平台是这样,这种代价仅仅换取I/O完成的进程赶紧执行,真的好吗?这非常有争议!

我们发现,大多数的Linux服务器都是把抢占给关掉的,以Linux CentOS 7.2为例:


可见,抢占更多的是噱头,代价巨大,华而不实。服务器的目标嘛,最大化吞吐率,至于不让I/O完成进程发生抢占的原因很简单,只要做切换这种额外的进程管理工作,就会占用CPU时间,同时刷cache,tlb会降低访存效率,这些就降低系统的总吞吐,让I/O完成进程至多再等一个调度周期呗。


可见,批处理分时系统的后代们,对CPU时间是多么地吝啬,不会花费哪怕几个微秒来做一些华而不实的事情,一切以最大化吞吐为目标,至于其它的,都是辅助。

现在看
桌面操作系统 ,这种是以人的操作为基本操作,以快速响应人的操作为目标,不管是鼠标的移动,还是键盘输入,或者移动窗口,均是I/O中断驱动,如果桌面操作系统直接使用传统UNIX/Linux调度器的话,UNIX/Linux天生对这类有关 响应度 的目标不敏感,或者说压根就不感兴趣,志不同不相为谋。

不说UNIX,只说Linux,作为UNIX-Like系统,它继承了批处理分时系统的全部基因,它本来就不是为桌面而生的。

也许你会反驳说,完成下面的三件事是不是就意味着Linux可以应对桌面了呢?

1、打开内核抢占。
2、打开HZ1000。
3、切换到CFS调度器。


很抱歉,非也。还是那句话,Linux根本就没有应对桌面场景的基因。

CFS只是让CPU分配时间的时候,更加均匀,平滑和公平,避免了饥饿问题,说实话,在提高I/O响应度方面,它其实并没有O(1)做得好。

对于O(1)调度器,根据进程的I/O睡眠情况动态调整其优先级以获得抢占当前进程的资格,这是该调度器中最最复杂的部分,这被称作
O(1)调度器的交互启发式算法 。该算法的目标是,在一个进程被唤醒的时候,根据其本次睡眠的时间以及睡眠的平均时间,为其计算一个新的优先级,以试图在可能的情况下抢占掉当前的进程。

简单review一下这个Linux内核史上最最复杂的启发式交互判断算法,不涉及细节,因为那样会让人迷失。

当一个进程从睡眠中被唤醒的时候,调度器根据该进程的睡眠时间会为其计算出一个
奖励值 ,根据该奖励值会提升或者降低该进程的优先级,以获得抢占的机会。

但是,这样的计算准确吗?我们指望它来提高桌面系统GUI的响应度来降低人能感受的延迟,可行吗?

假若我在拖拽一个窗口移除视线的遮挡,之后切换到另一个进程的窗口中用键盘打字,然后去调整音乐播放器的进度条,系统调度器凭什么能让所有这一切均流畅呢?换句话说,这些行为背后均伴随有I/O的完成,睡眠的结束,进程优先级的重新计算,CPU时间的奖励,但是,系统是如何排这几件事所在程序的进程优先级的呢?仅凭平均睡眠时间,够吗?

远远不够。

我们知道,优先级是一个在进程调度的时刻瞬时有效的值,系统没有办法对时间进行区分,以使得在I/O完成的当下时刻完成合适的抢占。换句话说,
事情的紧急程度是随着时间而不断变化的!在每一个确定的时刻,优先级的效果都是oneshot的。

没有人频繁操作机器,没有大量种类繁多不确定的I/O完成事件,需要反馈的事件并不多,所以抢占点也并不多,Linux在这种场景下,工作的很不错,可以说是异常优秀,能保证最大化吞吐。然而,桌面环境,频繁的GUI操作,频繁的人工输入和输出,正是相反的场景。

那么该怎么办?

具体来讲,如何可以实现
当点击鼠标,敲击键盘时,系统快速响应,当磁盘I/O结束时,系统可以稍微等一等,当…当… 只要是有外界事件传入,均要设置抢占点,在必要的时候发生抢占。抢占的概率因事件的不同而不同,延迟敏感的事件要更容易发生抢占,而延迟不敏感的事件则可以稍缓。

其实即便时Linux甚至老式UNIX系统一直都在这样做,只是未曾察觉而已。

考虑一下中断的处理,它的优先级就比普通进程的任何优先级都要高。Linux内核十分明确,中断的处理是可以抢占一切的,并且确实也发生了抢占,中断可以抢占任何进程的执行进入中断处理函数。

把中断优先级也看成一种
进程优先级 把中断处理看做一种进程,就看出其中的道道了。

两个思路訇然而出:

1、为什么不把和中断相关的进程处理和该中断关联,让中断返回后,该进程继续保持优高先级从而继续中断相关的后续呢?
其实Linux的softirq颇有这个意思,但是为什么不把这种事继续传递给更上层的应用程序进程呢?既然已经传递到下半部了,再上一层又何妨?
2、为什么不把类似拖拽窗口,键盘鼠标这种事件相关联的处理进程也看作是另一种中断呢?

继续下去就是,我们要把中断相关的优先级直接传递给进程,我们try_to_wakeup函数增加一个参数,指示唤醒进程应该增加的优先级数值:

// 鼠标操作对延迟敏感,动态优先级提升大mouse_irq_handle(...){ ... try_to_wakeup_prio(p, ..., 7); ...}
// 键盘对延迟更敏感,动态优先级提升更大keyboard_irq_handle(...){ ... try_to_wakeup_prio(p, ..., 8); ...}
// 磁盘I/O对延迟不敏感,对人的感觉延迟更是不敏感,不提升或者少动态优先级diskio_irq_handle(...){ ... try_to_wakeup_prio(p, ..., 0 or 2); ...}

如何规定某个外设的I/O完成到底提升多少优先级呢?靠启发式算法吗?No!靠拍脑袋?No!

靠统计,靠额外的分析,甚至人体工学领域的分析,调查人眼,人耳的分辨率,手敲键盘的肉体敏感程序。

是的,在编码的时候根据这些额外的调查结论,直接指定唤醒进程的优先级增量,而不是靠什么非常容易误算的平均睡眠时间之类靠非常不靠谱的额外启发式算法来猜测。

现在的问题是,是不是只要是I/O完成,都需要用明确的优先级增量唤醒进程呢?并不是,而是只有在该进程是
前台窗口 进程时,也就是获得了键盘鼠标焦点的窗口处理进程才需要如此,否则,常规的就好,不是焦点窗口,操作者并不care,他也不会操作到非焦点窗口的进程,人只在乎当前前台窗口的反应是否流畅:

mouse_irq_handle(...){    ...    if (p->focus)        try_to_wakeup_prio(p, ..., 7);    else        try_to_wakeup(p, ...);    ...}...

执国索因,问题是,如何确定focus字段的值呢?

也不难,就在窗口处理逻辑中设置就好。如果把整个窗口子系统放在内核态实现,当鼠标进入某个窗口并点击时,其处理进程获得焦点:

mouse_irq_handle(...){ ... if (event->button.right_press) get_curr_window(zone_area)->window->p->focus = 1; ... if (p->focus) try_to_wakeup_prio(p, ..., 7); else try_to_wakeup(p, ...); ...}

如果是类似X window的机制,那就直接在库函数里设置:

msg_receive(...){    ...    if (event->button.right_press) {        set_window_focus(get_curr_window(zone_area)->window, 1);    }    ...}

这样就OK咯。

事实上,Windows采用与此非常类似的做法:

· Windows将睡眠事件和优先级增量进行对应
· Windows在I/O完成事件后,用特定I/O事件对应的增量重设进程优先级,唤醒进程。
· 唤醒进程后,用新优先级实施抢占。
· 用等额时间片方式实施类似SRV4的优先级阶梯下降算法。

所以说,Windows内核的调度器是无条件随时抢占的。

Windows其实就是把窗口焦点得失事件作为像I/O事件一样作为中断一样的事件来处理的,同时,既然中断的概念被泛化,那么和纯硬件中断相反,泛化后的中断必须分层,这就有了IRQL的概念,任何一个时刻,Windows系统总是处在某一个中断级别:

· PASSIVE_LEVEL-用户态进程运行级别的中断模拟
· APC_LEVEL
· DISPATCH_LEVEL
· DIRQL-真正的硬件中断


在这一整套框架内,调度器内部的任何函数调用,都像在处理中断一般,每一个XX_LEVEL均会屏蔽其下的LEVEL的执行。每一类的中断,均有相关处理进程的优先级提升级别与之对应。将进程唤醒在那个优先级上运行,之后执行阶梯下降算法。

换句话说,Windows的进程动态优先级是I/O中断精确驱动,随时调整的,这个是和Linux采用启发式算法
微调 之间的最大之不同。


理解一个技术的机制原理非常容易,但是理解为什么会这样颇费工夫。

那么为什么Windows可以设计出和Linux截然不同的调度器,并且在应对桌面GUI处理方面绝佳呢?

这是和UNIX/Linux源自批处理分时系统的基因完全不同的UI基因使然。下面我们就稍微说一下这个与众不同的基因。

Windows诞生在个人计算机兴起的年代,彼时,大家都在购买,组装自己的
个人计算机电脑

所谓的个人计算机,字面意思无疑就是不能和大家共享的计算机,用户购买或者组装个人计算机的目的也不是去运行什么批处理数据分析,更多的是娱乐和处理日常!要知道比尔盖茨当时可是将安装Windows的PC机定位为20年后的大众消费品的,想象一下大众的需求就明白Windows善于做什么事了。

彼时的个人电脑,一直到现在,显示器,键盘鼠标都是必不可少的套件,主机甚至都能退而求其次。这些键盘鼠标显示器等外设,正是驱动进程优先级调整的主体,在这种软件架构下,操作系统无疑必须设计成那个样子。

最后,到了现在,
主机甚至消失了 ,当人们在说一体机,在谈Surface Pro的时候,看到更多是屏幕,而不是屏幕后面的那个已经被压缩成饼的主机。

对比Linux系统,除了初始安装时配置网络之外,是不需要显示器鼠标键盘的,甚至初始配置都可以不接显示器,很多网络启动,IPMI之类的都能搞定初始配置这件事,最终Linux服务器剩下的就是一台连着网线的主机,没有什么外设。这是和个人电脑截然相反的特征。

人们用这台个人电脑写文档,做报表,玩游戏,听音乐,聊天…无一不是靠频繁的I/O事件来驱动的,并且,
系统无法预测人的下一个动作是什么,是敲键盘呢,还是移动鼠标拖拽,或者直接砸了电脑?

个人电脑是需要
人不断用鼠标键盘绘图板操作的电脑 的,同时 用耳朵,用眼睛不断接收电脑的反馈,几乎没有人会用Windows电脑去做什么批处理,个人电脑概括起来,那就是 :

· 人给电脑一个输入。
· 电脑快速响应并提供计算服务。
· 电脑快速展示输出。


所以说,在个人电脑看来,没有什么进程的固定优先级会发挥重要的作用,所有进程的优先级都是随着时间随时调整的,和I/O事件以及GUI事件关联动态优先级才是根本。

嗯,这就是Windows系统的基因。它的目标是短时延,操作者做什么,结果就要最快的速度反馈回来。

嗯,来挖一下根。回到20世纪80年代的DoS时期。

DoS时代,任何调度的需求都不会存在,因为不需要调度,DoS出生伊始就是单用户单任务的系统,这便是其根!当只有一个进程在跑的时候,调度的意义就从进程调度转化为
事件调度 了:

· 如果DoS跑了一个批处理进程,那么不管它,让它跑完。
· 如果DoS跑了一个交互进程,那么捕获交互事件并快速响应事件。

后来的Windows 3.1,Windows 95,基于NT4.0的Windows XP这些,虽说都是现代多任务操作系统,但是DoS的基因却被继承,调度事件而不是调度进程。后来的Windows系统最终无非也就是DoS系统的升级,加入了现代操作系统多进程多线程虚拟内存的特征。DoS系统最开始就是 要被人操作的!所以后续的Windows XX自然继承了原是基因。

再次反观Linux,哪有什么键盘鼠标前的操作者,哪有什么盯着显示器的操作者,一般而言,只要将其TCP 22端口打开,接上一根网线(或者Wifi),远程SSH上去那是最正规的操作,由此而来,Linux系统最常见的I/O事件就是网卡I/O和磁盘I/O等单一的事件,问题是,这种事件是可以等的,可以简单计算,只要超过1000米的距离,数据的网络传输时延就能让主机的调度时延可以忽略不计。

反正我是不会用Linux来玩桌面环境,即便不得已装了一个Ubuntu,Suse,第一件事也是卸掉X,以腾出大量的磁盘空间。

如果真的想用Linux运行桌面环境,那么优化它的调度器便是首先要做的了。

方案就是, Linux按照Windows NT4.0+设计调度器 ,其桌面版GUI操作自然就会流畅很多。

· Windows-来自DoS习俗,操作系统是给人操作的。
· UNIX/Linux-来自分时批处理,操作系统是跑多任务的。

插播一段,关于Linux调度器,我为什么觉得2.6版本之前的O(n)调度器并没有那么糟糕?
是这样的。下面的这句话总结,不要随便拿出O(n)O(n)这种度量算法好坏的标准来抨击穷举。因为好多所谓优良的算法在nn很小的时候都很慢,而在大多数情况下nn都很小。
正如CFS调度器并没有让人哇塞一样,O(n)调度器也不会被人嘘。
优化是一场精细的跷跷板游戏,大多数情况下都是顾此失彼的,时间并不一定能换等价值的空间,空间也不一定能换等价值的时间,总要有损耗的。你把查询换成了索引,以为用空间换了等价值时间,但是有没有想过更大的内存块造成更多的cache miss的代价,所以说,算法分析的O并非优化的唯一准绳,还有体系结构。
我一开始是不喜欢Windows的,我和David Miller有一点很类似,就是不喜欢定制化的东西,纯认可通用的东西,然而我并不懂Windows内核,所以一直都保持着对Windows鄙视的观点,最终,我发现,Windows系统显然是个针对个人的高定操作系统啊!
来源:CSDN

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多