说明:如果对文章有什么疑问,可通过“阅读原文”链接在知乎上给我留言。
任务优先级
在uC/OS-II操作系统中,每个任务的优先级都各不相同,其结果相当于是在上文所讲的FCFS模式的基础上加上了抢占(preemption)。在低优先级的任务执行完成之前,可能被高优先级的任务抢占,即在高优先级任务进入ready queue后,调度器会把CPU的使用权从低优先级任务的手里夺过来,交给高优先级任务。不过抢占并不是什么时候都能发生的,它需要一定的契机,即调度器运行的时候,那调度器什么时候会运行呢?比如中断处理执行完后,或者某个共享资源被释放的时候。高优先级的任务通常是对实时性要求比较高的任务,需要在确定时间内返回确定的结果。如果高优先级任务没有因资源获取不到而block,或主动yield让出CPU,那么理论上低优先级的任务将一直得不到执行(当然在一个设计合理的系统中,高优先级任务事实上不可能一直运行)。uC/OS-II支持64个优先级,因此最多同时运行64个任务,这对于它面向的嵌入式应用来说通常是足够的。但一定要让所有任务的优先级都不同的做法,在一定程度上限制了它的灵活性。在后来的uC/OS-III中,开始支持不同的任务享有相同的优先级,每个任务被分配一定的timeslice。调度器始终选择ready queue中优先级最高的任务来执行,当一个任务的timeslice耗光后,同优先级的其他任务将被调度执行。如果该优先级没有任何任务需要运行,才会考虑更低优先级的任务。
【优先级反转】
在区分优先级的系统中,将会面临一个被称为优先级反转(Priority Inversion)的问题。比如有三个任务A, B, C,它们的优先级依次降低。现在任务C持有资源M,而后C被优先级更高的A所抢占,A也需要持有M才能继续运行,由于M在任务C的手里,因为A会被阻塞。对此,其中的解决办法之一是优先级继承(Priority Inheritance),即在A被阻塞后,把持有共享资源的C的优先级临时提高至和A一样(继承了A的优先级),这样C可以顺利执行完毕以释放M,而不至于被认为B所抢占,A可以先于B执行。之后,C的优先级将回退到和之前一样。以上介绍的这种优先级是一旦设定,就不会再更改,即“静态优先级”。看起来非常省事,可是在大型系统中,设定一个任务的初始优先级并不容易,有可能实际的运行结果与设定值相悖。一个更好的做法是等任务实际运行一段时间后,根据其过去这段时间的行为,来确立它的优先级。比如现在有两个任务,一个是视频编码,它执行期间占用的CPU时间较长,属于'processor-bound'型的任务,另一个是文本编辑,其大部分时间都在等待用户的键盘输入,属于'I/O-bound'型的任务。一旦用户完成了字符输入,不管是从交互性还是从公平的角度,都应该优先处理用户的这个输入。因此,'I/O-bound'型的任务通常被赋予较高的优先级,而'CPU-bound'型任务的优先级则相对更低。区别的方法是:任务初始时都被赋予最高的优先级和相同的timeslice,如果用完了自己的timeslice,那么是'CPU-bound'的可能性较大,因而优先级被降低,而如果在用完timeslice之前让出了CPU,则很可能是'I/O-bound'型的,优先级就保持不变。假设现在只有3个优先级Q2, Q1, Q0,任务A刚开始执行时优先级是Q2,经过一个10ms的timeslice后优先级被降为Q1,再经过一个timeslice优先级继续降为最低的Q0。然后在100ms的时候来了一个任务B(用灰色部分表示),它的初始优先级也是Q2,timeslice也是10ms,但它在每个timeslice内只会运行2ms,因此优先级始终保持不变,因此当它需要运行时,总是可以抢占正在运行的任务A。虽然它的直接目的并不是像上文介绍的SJF那样,让预期执行时间最短的任务优先执行,但是它通过让过去执行时间较短的任务获得更高的优先级,逼近了理想的SJF所希望达到的效果。过去的行为作为一种feedback,矫正了之后的任务调度,因此这种策略被称为Multilevel Feedback Queue(简称'MLFQ')。
这是个多么简单纯洁的机制啊,不过可能因为它太简单了吧,有一些“居心叵测”的任务很可能打着“合理利用规则”的口号来钻空子。不是只要没用满timeslice就可以豁免被调低优先级吗,那我每次在刚刚要用满timeslice的时候,就假装让出CPU(比如timeslice是10ms,那我每次只用9ms,最后1ms离开)。如果把优先级比作信任感的话,就好像虽然他欺骗了你,但你完全没有察觉出来,依然非常信任他,也就是说,对,你被“耍”了。不过,人都是在经历中慢慢成长的,被“耍”的次数多了,也就慢慢知道以后该如何应对了。只要把观察的时间窗口拉大,记录一个任务过去总的执行时间,而不是单个timeslice内的时间,以上的这种雕虫小技不就原形毕露了吗。正所谓“路遥知马力,日久见人心”。
虽然一个任务占据过多的CPU资源是不妥的,不过也算是通过调低优先级的方式作出惩罚了。试想一下,如果现在系统中有3个任务,一个任务(设为C)被调到了最低的优先级Q0,另两个任务(设为A和B)还是Q2,且这两个任务分别运行50%的timeslice的时间,那么任务C岂不是永远没有执行的机会,活活饿死?不就是过去的饭量大了点么,总不能因此就一直不给人家翻身的机会,所以在适当的时候,调度器会对这些低优先级的任务进行普调(boost)。但什么时候调,调到多高比较合适,又是一个头疼的问题。看来,这种调度策略还是不够理想,它还有优化的空间,详情请看下文分解。