(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / MAX_SLEEP_AVG) - MAX_BONUS/2
如下图所示:
再用这个 bonus 去减静态优先级就得到进程的动态优先级(并限制在 MAX_RT_PRIO和MAX_PRIO 之间),bonus 越小,动态优先级数值越大,优先级越低。也就是说,sleep_avg 越大,优先级也越高。
MAX_BONUS 定义为 MAX_USER_PRIO*PRIO_BONUS_RATIO/100,也就是说,sleep_avg 对动态优先级的影响仅在静态优先级的用户优先级区(100~140)的1/4区间(±5)之内,相对而言,静态优先级,也就是用户指定的 nice 值在优先级计算的比重要大得多。这也是 2.6 调度系统中变化比较大的一个地方,调度器倾向于更多地由用户自行设计进程的执行优先级。
sleep_avg 反映了调度系统的两个策略:交互式进程优先和分时系统的公平共享,在下一节中我们还要专门分析。
2) 优先级计算时机
优先级的计算不再集中在调度器选择候选进程的时候进行了,只要进程状态发生改变,核心就有可能计算并设置进程的动态优先级:
a) 创建进程
在wake_up_forked_process()中,子进程继承了父进程的动态优先级,并添加到父进程所在的就绪队列中。
如果父进程不在任何就绪队列中(例如它是 IDLE 进程),那么就通过 effect_prio() 函数计算出子进程的优先级,而后根据计算结果将子进程放置到相应的就绪队列中。
b) 唤醒休眠进程
核心调用 recalc_task_prio() 设置从休眠状态中醒来的进程的动态优先级,再根据优先级放置到相应就绪队列中。
c) 调度到从 TASK_INTERRUPTIBLE 状态中被唤醒的进程
实际上此时调度器已经选定了候选进程,但考虑到这一类型的进程很有可能是交互式进程,因此此时仍然调用 recalc_task_prio() 对该进程的优先级进行修正(详见"进程平均等待时间 sleep_avg"),修正的结果将在下一次调度时体现。
d) 进程因时间片相关的原因被剥夺 cpu
在 schedule_tick() 中(由时钟中断启动),进程可能因两种原因被剥夺 cpu,一是时间片耗尽,一是因时间片过长而分段。这两种情况都会调用effect_prio() 重新计算优先级,重新入队。
e) 其它时机
这些其它时机包括 IDLE 进程初始化(init_idle())、负载平衡(move_task_away(),详见"调度器相关的负载平衡")以及修改 nice 值(set_user_nice())、修改调度策略(setscheduler())等主动要求改变优先级的情况。
由上可见,2.6 中动态优先级的计算过程在各个进程运行过程中进行,避免了类似 2.4 系统中就绪进程很多时计算过程耗时过长,从而无法预计进程的响应时间的问题。同时,影响动态优先级的因素集中反映在 sleep_avg 变量上。
6. 进程平均等待时间 sleep_avg
进程的 sleep_avg 值是决定进程动态优先级的关键,也是进程交互程度评价的关键,它的设计是 2.6 调度系统中最为复杂的一个环节,可以说,2.6 调度系统的性能改进,很大一部分应该归功于 sleep_avg 的设计。这一节,我们将专门针对 sleep_avg 的变化和它对调度的影响进行分析。
内核中主要有四个地方会对 sleep_avg 进行修改:休眠进程被唤醒时(activate_task()调用 recalc_task_prio() 函数)、TASK_INTERRUPTIBLE 状态的进程被唤醒后第一次调度到(schedule()中调用 recalc_task_prio())、进程从 CPU 上被剥夺下来(schedule()函数中)、进程创建和进程退出,其中recalc_task_prio() 是其中复杂度最高的,它通过计算进程的等待时间(或者是在休眠中等待,或者是在就绪队列中等待)对优先级的影响来重置优先级。
1) 休眠进程被唤醒时
此时 activate_task() 以唤醒的时间作为参数调用 recalc_task_prio(),计算休眠等待的时间对优先级的影响。
在 recalc_task_prio() 中,sleep_avg 可能有四种赋值,并最终都限制在 NS_MAX_SLEEP_AVG 以内:
a) 不变
从 TASK_UNINTERRUPTIBLE 状态中被唤醒(activated==-1)、交互程度不够高(!HIGH_CREDIT(p))的用户进程(p->mm!=NULL)),如果它的 sleep_avg 已经不小于 INTERACTIVE_SLEEP(p) 了,则它的 sleep_avg 不会因本次等待而改变。
b) INTERACTIVE_SLEEP(p)
从 TASK_UNINTERRUPTIBLE 状态中被唤醒(activated==-1)、交互程度不够高(!HIGH_CREDIT(p))的用户进程(p->mm!=NULL)),如果它的 sleep_avg 没有达到 INTERACTIVE_SLEEP(p),但如果加上本次休眠时间 sleep_time 就达到了,则它的 sleep_avg 就等于 INTERACTIVE_SLEEP(p)。
c) MAX_SLEEP_AVG-AVG_TIMESLICE
用户进程(p->mm!=NULL),如果不是从 TASK_UNINTERRUPTIBLE 休眠中被唤醒的(p->activated!=-1),且本次等待的时间(sleep_time)已经超过了 INTERACTIVE_SLEEP(p),则它的 sleep_avg 置为 JIFFIES_TO_NS(MAX_SLEEP_AVG-AVG_TIMESLICE)。
d) sleep_avg+sleep_time
如果不满足上面所有情况,则将 sleep_time 叠加到 sleep_avg 上。此时,sleep_time 要经过两次修正:
i. 根据 sleep_avg 的值进行修正,sleep_avg 越大,修正后的 sleep_time 越小:
sleep_time =
sleep_time * MAX_BONUS * (1-NS_TO_JIFFIES(sleep_avg)/MAX_SLEEP_AVG)
ii. 如果进程交互程度很低(LOW_CREDIT()返回真,见"更精确的交互式进程优先"),则将 sleep_time 限制在进程的基准时间片以内,以此作为对 cpu-bound 的进程的优先级惩罚。
总的来说,本次等待时间越长,sleep_avg 就应该更大,但核心排除了两种情况:
从 TASK_UNINTERRUPTIBLE 状态醒来的进程,尤其是那些休眠时间比较长的进程,很有可能是等待某种资源而休眠,它们不应该受到等待时间的优先级奖励,它的 sleep_avg 被限制在 INTERACTIVE_SLEEP(p) 范围内(或不超过太多)(INTERACTIVE_SLEEP(p) 的含义见后),--这一限制对已经认定为是交互式的进程无效。
不是从 TASK_UNITERRUPTIBLE 状态中醒来的进程,如果本次等待时间过长,也不应该受到等待时间的优先级奖励。
INTERACTIVE_SLEEP(p) 与 nice 的关系
NS_TO_JIFFIES(INTERACTIVE_SLEEP(p))=
TASK_NICE(p)*MAX_SLEEP_AVG/40+
(INTERACTIVE_DELTA+1+MAX_BONUS/2)*MAX_SLEEP_AVG/MAX_BONUS -1
这个宏与进程 nice 值成正比,说明优先级越低的进程,允许它在"交互式休眠"状态下的时间越长。
2) TASK_INTERRUPTIBLE 状态的进程被唤醒后第一次被调度到时
调度器挑选出候选进程之后,如果发现它是从 TASK_INTERRUPTIBLE 休眠中醒来后第一次被调度到(activated>0),调度器将根据它在就绪队列上等待的时长调用 recalc_task_prio() 重新调整进程的 sleep_avg。
recalc_task_prio() 调整的过程与"休眠进程被唤醒时"的情况是一模一样的,所不同的是,此时作为等待时间 sleep_time 参与计算的不是进程的休眠时间,而是进程在就绪队列上等待调度的时间。并且,如果进程不是被中断唤醒的(activated==1),sleep_time 还将受到约束(delta=delta*ON_RUNQUEUE_WEIGHT/100),因为此时该进程不是交互式进程的可能性很大。从上面对 recalc_task_prio() 的分析可知,sleep_time 减小一般来说就意味着优先级会相应降低,所以,这一奖励说明调度器在进一步减小进程的响应时间,尤其是交互式进程。
3) 被切换下来的进程
前面说过,sleep_avg 是进程的"平均"等待时间,recalc_task_prio() 计算了等待时间,在 schedule() 中,被切换下来的进程的 sleep_avg 需要减去进程本次运行的时间 run_time(并保证结果不小于 0),这就是对"平均"的体现:等待得越久,sleep_avg 越大,进程越容易被调度到;而运行得越久,sleep_avg 越小,进程越不容易调度到。
run_time 可以用系统当前时间与进程 timestamp(上一次被调度运行的时间)的差值表示,但不能超过 NS_MAX_SLEEP_AVG。对于交互式进程(HIGHT_CREDIT(p) 为真,见"更精确的交互式进程优先"),run_time 还要根据 sleep_avg 的值进行调整:
run_time = run_time / (sleep_avg*MAX_BONUS/MAX_SLEEP_AVG)
这样调整的结果是交互式进程的 run_time 小于实际运行时间,sleep_avg 越大,则 run_time 减小得越多,因此被切换下来的进程最后计算所得的 sleep_avg 也就越大,动态优先级也随之变大。交互式进程可以借此获得更多被执行的机会。
4) fork 后
在 wake_up_forked_process() 中,父进程的 sleep_avg 要乘以 PARENT_PENALTY/100,而子进程的 sleep_avg 则乘以 CHILD_PENALTY/100。实际上PARENT_PENALTY 为 100,CHILD_PENALTY 等于 95,也就是说父进程的 sleep_avg 不会变,而子进程从父进程处继承过来的 sleep_avg 会减小 5%,因此子进程最后的优先级会比父进程稍低(但子进程仍然会置于与父进程相同的就绪队列上,位置在父进程之前--也就是"前言"所说"子进程先于父进程运行")。
5) 进程退出时