Linux的两大抽象分别是进程和文件,其它一切操作都是基于或者针对这两大抽象而进行的。如何设计并实现一个最佳的进程调度器,则是所有操作系统都必须重点关注的。本文基于《Modern Operating Systems》(Andrew S.Tanenbaum)一书,对批处理系统、交互式系统(UNIXLinuxDOSWindows)、实时系统(VxWorksPSOS等)常用的调度算法进行了描述和图解,希望对很多没有时间看书,但又想要了解这方面知识的同学有所帮助:)


      本节主要介绍各种操作系统对调度程序的不同要求。我们不可能设计一个防止四海而皆准的东西,因此不得不具体问题具体分析。这个世界上有很多矛盾,调度同样如此,但我们可以尝试抓住主要矛盾并解决之。

      可能有些人认为交互+抢占代表着调度算法设计中先进的生产力,现在我们耳熟能详的系统不都是这一类吗?其实未然,德国动物学家海克尔指出了个体重复发展现象:一个胚胎重复物种的演化。历史是一个车轮,每根辐条在经历了一段时间之后,还会转到与以前相同的位置。因此,我们很有必要对历史上曾出现过的操作系统加以研究,了解调度算法背后的哲理,这对我们学习操作系统是大有裨益的。

一、批处理操作系统
       我第一次接触批处理这个词,还是学习DOS的时候。我们可以写一个扩展名叫做bat的文件,里面用一些简单的语法来描述自己需要执行那些程序,需要这些程序按照什么样的顺序运行。写好以后就在DOS的 提示符下输入这个批处理文件的名称,操作系统就按照我们的意图执行了。因为可以在一个bat文件中指定依次运行多个程序,就好比把这些程序一次全部提交给操作系统一样,这种形式被称为批处理。

       早期的计算机系统是非常昂贵的,大多数用户必须通过终端连线共用同一套系统,或者大家把自己的程序写好,做成打孔纸带交给计算机管理员,由管理员装在计算机的阅读机上成批提交,过一会儿程序员们就可以找管理员要运行结果。如何公平地分配CPU资 源是让计算机管理员头疼的事情,但公平肯定是第一原则,否则会有一群双眼熬得通红,骨瘦如柴但又满脸愤怒的程序员高喊口号手持键盘过来砸机房了!不过固然 机房被砸个稀巴烂,或许管理员头上也要鼓几个大包,这批造反者中某人取代他成为新的管理员,还是绕不过公平分配这个问题。

       联想到我们平时排队就餐、排队买票、排队坐公交车(这个在TG相当罕见),很多人对公平的理解就是:我先来的,所以我先享受服务。计算机管理员亦是如此,他只要规定,你们谁先把纸带交给我,我就先给谁结果便可以营造一种公平的气氛。这听起来非常合理,A君上午8点提交的纸带,10点出结果;B君上午810分提交的纸带,10点开始运行,11点出结果等等...... 于是没有人再来砸计算机房了,管理员的人身安全也得到了保障。可是过了一段时间之后,A君提交的纸带越来越长,以前2个小时就可以拿到的结果现在得等5个小时,A君无所谓,刚好可以去考虑一下如何写漂亮的胶片,顺便提升一下绩效。B君却不干了,你凭什么搞这么又臭又长的纸带占住计算机不放?在和A君多次商议无果的情况下,B君也冲冠一怒,下次写了个需要运行10个小时的程序,嗯,其中2/3的时间其实在死循环,这意味着A君第二天8点在来交纸带,可能9点才能开始运行,B君程序的运行时间自然顺延一个小时,周而复始,变成了恶性循环。如此一来,A君和B君积怨越来越深,组织氛围越来越不融洽,直到有一天终于爆发。A君和B君掐了起来,管理员一看,赶忙过来劝架,结果AB二人又盛怒之下,又把管理员当成了***目标:揍死你丫的,叫你收老子的钱就是不升级系统!看到管理员变猪头了,他们怒气才消了一点:你丫先去瘦瘦脸,整整容,回头买一套快一点的计算机回来。

       痛定思痛,痛何如哉?管理员经历此次兄弟反目的巨变之后,决定要奋斗,要科学。他仔细分析了一下先来先服务这种排队机制,发现了三个问题:
1. 如果一些很长的任务总是排在前面,那么势必挑战后面较短任务用户的耐心;
2. 如果有的程序占用CPU很长时间却不产生价值,对别的程序显然很不公平,比如B君写的死循环。
3. 如果某程序总共需要运行10分钟,其中前1分钟运算,中间8分钟等待I/O,最后1分钟得出结果,那么让他10分钟都占着CPU不放显然不合理。

       找到了问题的主要矛盾,解决起来就有目标了。大家在银行排队办理业务的时候应该有所感觉,如果前面排了两个人,但是每个都在5分钟之内办完,那么我们非常乐于接受。如果只有一个人,但是半个小时过去了还在慢条斯理地和营业员罗嗦,我们会恨不得想冲上去扇他。那么,如何尽量让每个用户都在自己能接受的时间范围内获得服务呢?管理员想到一个办法:把短的任务尽可能超前放。他的理由如下:

1. 这些短任务的所有者自然会很满意,对于长任务的所有者而言,自己的程序原本就要执行1个小时,前面插入了两个总共需要执行10分钟的任务,也不是什么大不了的事情嘛。
2. 因为短的任务会先执行,所以程序员们都尽力让自己的代码短小精干,不会为了凑工作量、拿高绩效或者挤兑别人而变得又臭又长,现在求B君写无聊的死循环他都不干了。
3. 不管怎么排列,同样的一批任务执行完毕需要的总时间不变。

       听起来很美好,不是吗?然而第三个矛盾仍然得不到解决。管理员心想,我的智慧不够了,这个问题留给后人吧:)经过与AB等诸君等沟通,大家纷纷表示此方法甚为和谐,无压力。从此,机房又充满了欢声笑语。

       时光如梭、岁月如歌。若干年后,原来简陋的计算机房变成了一栋气派的大楼,管理员已经当上CXO去考虑大问题,办大事儿了。新来的小伙子对一切都感到新鲜,AB而君也早已不写代码,改为攻读胶片。程序员们则被一群刚毕业的打工仔替代。CXO认为,计算机系统只给自己的程序员用,太浪费了,应该向公众开放并收取一定的费用。他让小伙子来办这件事,并语重心长地指出:
1. 按任务执行时长收费。
2. 要让交了钱的用户都认为我们的服务是公平的。

       小伙子,好好发挥你的聪敏才智吧!别让我失望。意气风发的CXO似乎早已忘记当年自己怎么变猪头的了。

       初来乍到,就接这么一个硬活,小伙子有点意外,但高风险才有高回报。他首先仔细分析了现有的这种短作业优先的调度算法,试图知道这种设计背后的原因。假设有三个顺序提交的任务:T1T2T3,执行时长分别是10分钟、5分钟和3分钟,按照T1T2T3的顺序执行的话,T110分钟后输出结果,T2需要15分钟,T3需要18分钟,总共需要43分钟。但按照T3T2T1的顺序提交,T1需要18分钟、T2需要8分钟,T3需要3分钟,总共只需要29分钟。他把从一批任务提交到输出结果的时间定义为周转时间,显然短作业优先比先来先服务有更短的周转时间,因而用户更满意。如果所有用户的程序都疯狂地占用CPU,那这样的调度算法足以应付了。但系统运行了一段时间以后,小伙子发现程序千奇百怪,有的喜欢不停地计算,有的喜欢先读一会儿磁盘再计算,还有的反复读磁盘,反复运算,大量的CPU时间被浪费了,时间就是金钱啊!CXO的谆谆教诲还回响在耳边。对于这类随机性很强的事物,直接建模是不可能的,于是小伙子不得不借助统计方法来研究。他发现,不管程序怎么变化,都可以归为两类:一类是使用CPU的时间超过使用I/O的,另一类刚好相反。他把前者称为CPU密集型任务,后者称为I/O密集型任务。如果I/O密集型任务等待的时间还能让CPU工作,那么周转时间将进一步缩短。那么怎么做到这一点呢?小伙子想到个办法,只要任务进入I/O等待,就让调度器换一个短任务继续运行,新任务运行完了再检查等待的任务是否能继续运行,如果不能就再换,如果可以就让他继续完成后面的工作。

       假设T2I/O密集型,它先用1分钟初始化,然后进入I/O等待,3分钟后再继续运行1分钟全部完成。我们仍然按照T3T2T1的顺序调度。T3的周转时间是3分钟,T2运行1分钟,然后调度器让T1开始运行,T2等待I/O结束之后再抢占”T1。于是,T2的周转时间还是8分钟,但T1变成了14分钟,总时间减少到25分钟。嘿嘿,但是我还是按每任务的长度来收费,赚钱咯。

       让权和抢占机制是一个非常棒的点子,它的引入不但解决了批处理作业调度中如何均衡I/O密集型任务和CPU密集型任务,防止CPU闲置的问题,后来还产生了一些更好的副作用

       公司因为这位年轻人的新点子赚了不少钱,CXO心情大爽,手一抖,小伙子的绩效就得了个A,自此衣食无忧矣。随着这家计算机系统供应商的口碑越来越好,名气越来越大,更多地用户源源不断地把自己的程序送过来运行,特别是有一些他爸名叫x刚的,不屑于和别人享用同样公平的服务,非要对公平提出自己不同的见解:什么是TMD的公平?公平就是TMD有多高地位,就应该享受TMD多大服务!总之,刚刚们的下代人不遗余力地争取自己的话语权,非要给公平下这样的定义。小伙子扛不住了,写了个胶片汇报到CXO那边。CXO大笔一挥:为人民服务,刚刚们也是人民的一员嘛!这......小伙子心想,办法都是人想的,一定得找个万全之计出来。毕达哥拉斯说过,万物皆数。解决调度问题,还得靠数学方法。

       假设现在有T1T2T3三个任务按照最短优先平等调度。这时候来了一个客户,他什么都不说,丢过一张名片,上面赫然写着LiGang!小伙子心下大惊,脸上陡然变色,整整衣服赶忙迎出门来。客户面部表情,只说了一句话:给你一个任务T4,啥时候给我结果,你自己看着办,别忘了平时的保护费都是我收的。小伙子满脸堆笑:您交代的,好说好说,只管放心就是。

       检查一下,发现T4运行的时间并不长,但是和其它收到的下一轮任务相比,也不是最短的。如果把任务D就这么排进去,必须要等T1T2T3运行完一轮才有机会调度到,并且还不是第一个。小伙子灵机一动,何不充分利用抢占机制呢?假如当前批量任务运行顺序是T3T2T1,现在T1运行了一半,剩下5分钟,T4标称的运行时间是4分钟,那么可以认为T4T1要短,这样就可以理直气壮地插个队了。哈哈哈,我真是太聪明了!就这样,T4大摇大摆地夺过T1CPU的控制权,连续执行4分钟再交还。

       这种情况下,T2T3的周转时间没变,插队的T4从提交到执行只用了4分钟,T1则变成了18分钟。T4的优先级得到了保证,T1虽然增加了4分钟,但相对于它较大的执行时间基数,也还能忍耐。于是皆大欢喜,小伙子奠定了自己的技术权威。他给新的调度算法起了个名字:最短剩余时间优先

       现在,我们已经看到了三种批处理操作系统的调度算法,分别是:先来先服务最短作业优先最短剩余时间优先。迄今为止,曾遇到的问题都解决了。小伙子也已经从懵懂少年成长为一代专家。但所谓和谐社会花似锦,科学发展势如涛,江山代有才人出,各领风骚数百年。当时代发展到必须出现英雄的时候,就一定会出现。这家计算机系统供应商固然赚了大把的刀勒。可随着77元廉租房神话的破灭,其运维成本直线飙升。终于有一天,老大沉不住气了,把这位曾给他的公司带来数年辉煌的技术专家请了过去,大吐苦水:现在租金这么贵,不但要交各种税,还要给员工交社保,好不容易省吃俭用地出国买了个ipad,不伪装成遗像框都带不进来。特别是最近这个内存不停涨价,接得作业却越来越多,越来越庞大,公司快没有余粮了啊!技术专家紧紧握住老大的手,双眼含着泪光,千言万语汇成一句话:啥都别说了,优化!

       技术问题必须靠技术手段才能彻底解决。已有的三种调度方法解决的都是如何按照某种公平的定义来分配CPU的问题。但内存涨价是个新问题,所以得另外想办法,专家召集一众技术人员开始紧急攻关。现在的突出矛盾是:程序很多,内存很贵。目标:如何用最少的内存运行足够多的程序。算法设计最重要的方法是分而治之,既然程序很多,那么我们在一批调度中可以挑选足够支撑的数量进入调度队列;既然内存很贵,那么我们只在内存中保留一个当前运行的程序,把别的程序都放到廉价的磁盘或者磁带机上。如此一来,调度算法就被很自然地分成了三部分:负责CPU资源调配的算法、负责内存资源调配的算法和负责内存与低速海量存储器之间任务交还的算法。这就是分级调度算法,它在分配CPU资源的时候仍然采用前述三种基本算法之一。

       关于批处理操作系统的调度算法介绍完毕,我们由此看到,所有算法的产生都有其深刻的历史原因。梦工厂的动画片《机器人总动员》中,代表先进生产力的大焊先生说过一句话:需求是灵感的源泉。我们设计算法的时候,一定要把自己放在客户的角度,理解了需求,就能激发创意。算法并不统统是高深莫测,只有寥寥数人才能搞定的神话。