一个进程结束运行时,如果它的交互程度比父进程低(sleep_avg 较小),那么核心将在 sched_exit() 中对其父进程的 sleep_avg 进行调整,调整公式如下(以 child_sleep_avg 表示子进程的 sleep_avg):
sleep_avg = sleep_avg*EXIT_WEIGHT/(EXIT_WEIGHT+1) + child_sleep_avg/(EXIT_WEIGHT+1)
其中 EXIT_WEIGHT 等于 3,所以父进程的 sleep_avg 将减少自身 sleep_avg 的 1/4,再补偿子进程 sleep_avg 的 1/4,优先级也将随之有所下降,子进程的交互程度与父进程相差越大,则优先级的惩罚也越明显。利用进程平均等待时间来衡量进程的优先级,使得宏观上相同静态优先级的所有进程的等待时间和运行时间的比值趋向一致,反映了 Linux 要求各进程分时共享 cpu 的公平性。另一方面,sleep_avg 还是进程交互式程度的衡量标准。
7. 更精确的交互式进程优先
交互式进程优先策略的实际效果在 2.4 内核中受到广泛批评,在 2.6 内核中,这一点得到了很大改进,总体来说,内核有四处对交互式进程的优先考虑:
1) sleep_avg
上文已经详细分析了 sleep_avg 对进程优先级的影响,从中可以看出,交互式进程因为休眠次数多、时间长,它们的 sleep_avg 也会相应地
更大一些,所以计算出来的优先级也会相应高一些。
2) interactive_credit
系统引入了一个 interactive_credit 的进程属性(见"改进后的 task_struct"),用来表征该进程是否是交互式进程:只要
interactive_credit 超过了 CREDIT_LIMIT 的阀值(HIGH_CREDIT()返回真),该进程就被认为是交互式进程。
interactive_credit 的初始值为 0,在两种情况下它会加 1,这两种场合都在 recalc_task_prio() 函数中:
用户进程(p->mm!=NULL),如果不是从 TASK_UNINTERRUPTIBLE 休眠中被唤醒的(p->activated!=-1),且等待的时间(包括在休眠中
等待和在就绪队列中等待,)超过了一定限度(sleep_time>INTERACTIVE_SLEEP(p));
除以上情况外,sleep_avg 经过 sleep_time 调整后,如果大于 NS_MAX_SLEEP_AVG。
无论哪种情况,一旦 interactive_credit 超过(大于)CREDIT_LIMIT 了,它都不再增加,因此 interactive_credit 最大值就是
CREDIT_LIMIT+1。
interactive_credit 的递减发生在 schedule() 函数中。当调度器用运行时间修正被切换下来的进程的 sleep_avg 之后,如果 sleep_avg 小
于等于 0,且interactive_credit 在 -CREDIT_LIMIT 和 CREDIT_LIMIT 之间(-100<=interactive_credit<=100),则 interactive_credit
减 1。可见interactive_credit 最小值为 -101,且一旦它达到 CREDIT_LIMIT+1 的最大值就不会再被减下来--它将保持在 CREDIT_LIMIT+1
的高值上。
这就是说,只有进程多次休眠,且休眠的时间足够长(长于运行的时间,长于"交互式休眠"时间),进程才有可能被列为交互式进程;而一旦
被认为是交互式进程,则永远按交互式进程对待。
采用 HIGH_CREDIT() 标准断言的交互式进程主要在以下两处得到优先级计算上的奖励:
当进程从 cpu 上调度下来的时侯,如果是交互式进程,则它参与优先级计算的运行时间会比实际运行时间小,以此获得较高的优先级
(见"进程平均等待时间 sleep_avg");
交互式进程处于 TASK_UNINTERRUPTIBLE 状态下的休眠时间也会叠加到 sleep_avg 上,从而获得优先级奖励(见"进程平均等待时间
sleep_avg");
3) TASK_INTERACTIVE()
核心另有一处不采用 HIGH_CREDIT() 这种累积方式来判断的交互式进程优先机制,那里使用的是 TASK_INTERACTIVE() 宏(布尔值):
prio <= static_prio-DELTA(p)
当进程时间片耗尽时,如果该宏返回真(同时 expired 队列没有等待过长的时间,见"新的数据结构 runqueue""expired_timestamp"条),则
该进程不进入 expired 队列而是保留在 active 队列中,以便尽快调度到这一交互式进程。动态优先级在调度到该进程时在 effective_prio
() 中算出:prio=static_prio-bonus(sleep_avg)(bonus(sleep_avg) 表示 bonus 是关于 sleep_avg 的函数,见"优先级计算过程"),而
DELTA(p) 与进程的 nice 值有关,可表示为delta(nice)。bonus 与 sleep_avg 的关系在"优先级计算过程"一节中已经用图说明了,delta 和
nice 之间的关系见下图:
nice 值的范围是 -20~19,DELTA(p)将其转换到 -5~+5 再加上一个INTERACTIVE_DELTA常量的范围内:TASK_NICE(p) * MAX_BONUS/40 +
INTERACTIVE_DELTA,其中 INTERACTIVE_DELTA 等于 2。
经过转换,TASK_INTERACTIVE(p) 变为 "delta(nice) 是否不大于 bonus(sleep_avg)"。将 sleep_avg 表示为 JIFFIES 的形式,并代入常数
,delta(nice)<=bonus(sleep_avg) 可以表示为:
nice/4+2 <= bonus,-5<=bonus<=+5
从中可以看出,nice 大于 12 时,此不等式恒假,也就是说此时进程永远不会被当作交互式进程看待;而进程静态优先级越高,要被当作交互
式进程所需要的 sleep_avg 上限也越低,即静态优先级高的进程获得这种奖励的机会更大。
4) 就绪等待时间的奖励
因为经常处于 TASK_INTERRUPTIBLE 状态的进程最有可能是交互式的,因此,这一类进程从休眠中醒来后在就绪队列上等待调度的时间长短也
将影响进程的动态优先级。
这一工作在调度器(schedule())选择上这一类型的进程之后进行,并且考虑到交互式进程通常都是在中断中被唤醒的,所以核心还记录了这
一信息,对不由中断唤醒的进程实行奖励约束(详见"进程平均等待时间sleep_avg")。
8. 调度器
有了以上的准备工作之后,现在我们可以看看调度器的主流程了。
和 2.4 的调度器相比,2.6 的 schedule()函数要更加简单一些,减少了锁操作,优先级计算也拿到调度器外进行了。为减少进程在 cpu 间跳
跃,2.4 中将被切换下来的进程重新调度到另一个 cpu 上的动作也省略了。调度器的基本流程仍然可以概括为相同的五步:
清理当前运行中的进程(prev)
选择下一个投入运行的进程(next)
设置新进程的运行环境
执行进程上下文切换
后期整理
2.6 的调度器工作流程保留了很多 2.4 系统中的动作,进程切换的细节也与 2.4 基本相同(由 context_switch() 开始)。为了不与 2.4 系
统的调度器分析重复,我们按照调度器对各个数据结构的影响来分析调度器的工作流程,重点在与 2.4 调度器不同的部分,与之相同或相似的
部分相信读者结合代码和上文的技术分析很容易理解。同时,2.6 的调度器中还增加了对负载平衡和内核抢占运行的支持,因为内容比较独立
,我们也将其放在单独的章节中。
1) 相关锁
主要是因为就绪队列分布到各个 cpu 上了,2.6 调度器中仅涉及两个锁的操作:就绪队列锁 runqueue::lock,全局核心锁 kernel_flag。对
就绪队列锁的操作保证了就绪队列的操作唯一性,核心锁的意义与 2.4 中相同:调度器在执行切换之前应将核心锁解开
(release_kernel_lock()),完成调度后恢复锁状态(reacquire_kernel_lock())。进程的锁状态依然保存在task_struct::lock_depth属性
中。
因为调度器中没有任何全局的锁操作,2.6 调度器本身的运行障碍几乎不存在了。
2) prev
调度器主要影响 prev 进程的两个属性:
sleep_avg 减去了本进程的运行时间(详见"进程平均等待时间 sleep_avg"的"被切换下来的进程");
timestamp 更新为当前时间,记录被切换下去的时间,用于计算进程等待时间。
prev被切换下来后,即使修改了 sleep_avg,它在就绪队列中的位置也不会改变,它将一直以此优先级参加调度直至发生状态改变(比如休眠
)。
3) next
在前面介绍 runqueue 数据结构的时候,我们已经分析了 active/expired 两个按优先级排序的就绪进程队列的功能,2.6 的调度器对候选进
程的定位有三种可能:
active 就绪队列中优先级最高且等待时间最久的进程;