图2:现在的current
4. 新的运行时间片表现
2.6 中,time_slice 变量代替了 2.4 中的 counter 变量来表示进程剩余运行时间片。time_slice 尽管拥有和 counter 相同的含义,但在内核中的表现行为已经大相径庭,下面分三个方面讨论新的运行时间片表现:
1) time_slice 基准值
和 counter 类似,进程的缺省时间片与进程的静态优先级(在 2.4 中是 nice 值)相关,使用如下公式得出:
MIN_TIMESLICE + ((MAX_TIMESLICE - MIN_TIMESLICE) *
(MAX_PRIO-1 - (p)->static_prio) / (MAX_USER_PRIO-1))
代入各个宏的值后,结果如图所示:
可见,核心将 100~139 的优先级映射到 200ms~10ms 的时间片上去,优先级数值越大,则分配的时间片越小。
和 2.4 中进程的缺省时间片比较,当 nice 为 0 时,2.6 的基准值 100ms 要大于 2.4 的 60ms。
进程的平均时间片
核心定义进程的平均时间片 AVG_TIMESLICE 为 nice 值为 0 的时间片长度,根据上述公式计算所得大约是 102ms。这一数值将作为进程运行时间的一个基准值参与优先级计算。
2) time_slice 的变化
进程的 time_slice 值代表进程的运行时间片剩余大小,在进程创建时与父进程平分时间片,在运行过程中递减,一旦归 0,则按 static_prio 值重新赋予上述基准值,并请求调度。时间片的递减和重置在时钟中断中进行(sched_tick()),除此之外,time_slice 值的变化主要在创建进程和进程退出过程中:
a) 进程创建
和 2.4 类似,为了防止进程通过反复 fork 来偷取时间片,子进程被创建时并不分配自己的时间片,而是与父进程平分父进程的剩余时间片。也就是说,fork 结束后,两者时间片之和与原先父进程的时间片相等。
b) 进程退出
进程退出时(sched_exit()),根据 first_time_slice 的值判断自己是否从未重新分配过时间片,如果是,则将自己的剩余时间片返还给父进程(保证不超过 MAX_TIMESLICE)。这个动作使进程不会因创建短期子进程而受到惩罚(与不至于因创建子进程而受到"奖励"相对应)。如果进程已经用完了从父进程那分得的时间片,就没有必要返还了(这一点在 2.4 中没有考虑)。
3) time_slice 对调度的影响
在 2.4 中,进程剩余时间片是除 nice 值以外对动态优先级影响最大的因素,并且休眠次数多的进程,它的时间片会不断叠加,从而算出的优先级也更大,调度器正是用这种方式来体现对交互式进程的优先策略。但实际上休眠次数多并不表示该进程就是交互式的,只能说明它是 IO 密集型的,因此,这种方法精度很低,有时因为误将频繁访问磁盘的数据库应用当作交互式进程,反而造成真正的用户终端响应迟缓。
2.6 的调度器以时间片是否耗尽为标准将就绪进程分成 active、expired 两大类,分别对应不同的就绪队列,前者相对于后者拥有绝对的调度优先权--仅当active 进程时间片都耗尽,expired 进程才有机会运行。但在 active 中挑选进程时,调度器不再将进程剩余时间片作为影响调度优先级的一个因素,并且为了满足内核可剥夺的要求,时间片太长的非实时交互式进程还会被人为地分成好几段(每一段称为一个运行粒度,定义见下)运行,每一段运行结束后,它都从 cpu 上被剥夺下来,放置到对应的 active 就绪队列的末尾,为其他具有同等优先级的进程提供运行的机会。
这一操作在 schedule_tick() 对时间片递减之后进行。此时,即使进程的时间片没耗完,只要该进程同时满足以下四个条件,它就会被强制从 cpu 上剥夺下来,重新入队等候下一次调度:
进程当前在 active 就绪队列中;
该进程是交互式进程(TASK_INTERACTIVE()返回真,见"更精确的交互式进程优先",nice 大于 12 时,该宏返回恒假);
该进程已经耗掉的时间片(时间片基准值减去剩余时间片)正好是运行粒度的整数倍;
剩余时间片不小于运行粒度
运行粒度的定义运行粒度 TIMESLICE_GRANULARITY 被定义为与进程的 sleep_avg 和系统总 CPU 数相关的宏。因为 sleep_avg 实际上代表着进程的非运行时间与运行时间的差值,与交互程度判断关系密切,所以,运行粒度的定义说明了内核的以下两个调度策略:
进程交互程度越高,运行粒度越小,这是交互式进程的运行特点所允许的;与之对应,CPU-bound 的进程为了避免 Cache 刷新,不应该分片;
系统 CPU 数越多,运行粒度越大。
5. 优化了的优先级计算方法
在 2.4 内核中,优先级的计算和候选进程的选择集中在调度器中进行,无法保证调度器的执行时间,这一点在前面介绍 runqueue 数据结构的时候已经提及。2.6 内核中候选进程是直接从已按算法排序的优先级队列数组中选取出来的,而优先级的计算则分散到多处进行。这一节分成两个部分对这种新的优先级计算方法进行描述,一部分是优先级计算过程,一部分是优先级计算(以及进程入队)的时机。
1) 优先级计算过程
动态优先级的计算主要由 effect_prio() 函数完成,该函数实现相当简单,从中可见非实时进程的优先级仅决定于静态优先级(static_prio)和进程的sleep_avg 值两个因素,而实时进程的优先级实际上是在 setscheduler() 中设置的(详见"调度系统的实时性能",以下仅考虑非实时进程),且一经设定就不再改变。相比较而言,2.4 的 goodness() 函数甚至要更加复杂,它考虑的 CPU Cache 失效开销和内存切换的开销这里都已经不再考虑。
2.6 的动态优先级算法的实现关键在 sleep_avg 变量上,在 effective_prio() 中,sleep_avg 的范围是 0~MAX_SLEEP_AVG,经过以下公式转换后变成-MAX_BONUS/2~MAX_BONUS/2 之间的 bonus: