O(1)调度器的饥饿判断与交互判断

众所周知O(1)调度器的优先级调整,交互判断以及饥饿判断非常复杂,事实上抵消了pick- next算法的高效性,以至于最终被cfs调度器所取代,交互判断是有目共睹的,代码表示也十分明确,唯一不足的就是那些经验值实在是很难理解。关于饥饿判断,事实上在代码上也是很简单的,也是很容易理解的,基于2.6.11内核代码,首先看一个宏定义:
#define EXPIRED_STARVING(rq) /
((STARVATION_LIMIT && ((rq)->expired_timestamp && /
(jiffies - (rq)->expired_timestamp >= /
STARVATION_LIMIT * ((rq)->nr_running) + 1))) || /
((rq)->curr->static_prio > (rq)->best_expired_prio))
这 个宏定义最后返回一个布尔值,如果为真,那么就说明有进程已经饥饿了,这就是说无论如何也不能再照顾当前的进程了,这个判断一般在交互判断等照顾行为之后作为第二条限制被调用,也就是说需要被照顾的进程即便达到了被照顾的标准,比如它是交互进程,那么还要看别的进程又没有人有意见,只有在自己的客观条件满 足被照顾,并且主观上别的进程也没有意见的时候,照顾才会成为事实,否则渴望被照顾只是一厢情愿罢了。在scheduler_tick中,一个进程耗尽了 自己的时间片的时候会进行这个判断:
if (!rq->expired_timestamp)
rq->expired_timestamp = jiffies;
if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) {
enqueue_task(p, rq->expired);
if (p->static_prio best_expired_prio)
rq->best_expired_prio = p->static_prio;
} else
enqueue_task(p, rq->active);
从 以上的判断可以看到在饥饿判断中的另一个变量,那就是运行队列中的字段expired_timestamp,该字段的意义重大,什么时候设置 expired_timestamp?是第一个进程耗尽时间片的时候设置,不管它是什么进程(非实时范畴内),之后一直到该队列所有的进程全部进入过期队列或者该队列没有活动进程一直不再复位为0。expired_starving判断时考虑的仅仅是当前的jiffies和当前队列的 expired_timestamp的差值是否超过一个界限以及XXX(优先级相关的),如果超过了就无论如何都不再将该耗尽时间片的进程重新入队,交互判断只是一个很松散的判断,要想重新入队还要过饥饿判断的关,而饥饿判断和交互判断没有双向依赖,它们是各自为政的,负责的是两个方面。 expired_timestamp是每个对列一个,记录的是该队列投入运行后的第一个耗尽时间片的进程在耗尽时间片的时候的jiffies,如果是交互 进程第一个耗尽了时间片,那么该队列的expired_timestamp就应该是此时的jiffies,这很正常啊,如果它再一次用完了时间片,调度器 通过判断EXPIRED_STARVING宏可能就把它放到过期队列了,调度器对交互进程的照顾和对饥饿进程的照顾是调度器的两个几乎不怎么相干的机制。
只要有饥饿,就不再将interactive进程重新入队,不管EXPIRED_STARVING是否容易为真,看看那两个公式,饥饿判断的公式和交互判 断的公式,这些值都是经验值,作者经过测试后的经验值,因此一些极端的不公平情况会发生,但是很不容易发生,是小概率事件,饥饿判断的逻辑很简单,就是那个EXPIRED_STARVING公式。这些机制也是O(1)中很复杂的逻辑,那些经验值是很难被理解的,因此才有了cfs。
除了expired_timestamp字段之外,运行队列还有一个字段--优先级也是饥饿判断的一个参考数据,一开始运行队列的 best_expired_prio字段被设置为最低的优先级,一旦有进程耗尽时间片并且被加入到过期队列,那么此时就需要将运行队列的 best_expired_prio和该进程的基本优先级做比较,如果该进程的优先级更高,那么就将运行队列的best_expired_prio设置为 该进程的基本优先级。在饥饿判断的时候,刚才说的expired_timestamp超时仅仅是一种情况,另一种情况就是 EXPIRED_STARVING(rq)的后半部分的意思:
||rq->curr->static_prio > rq->best_expired_prio)
这 个意思是说,如果有一个更高优先级的进程已经处于过期队列,那么无论如何也不能再照顾当前进程了,即使它是交互进程也不能,那岂不是说运行队列的低优先级的进程永远得不到照顾了,不是的,仔细看看判断的语句,比较的是static_prio,而不是实际的优先级,实际的优先级是要经过复杂计算的动态优先级。
如何的动态提升进程的优先级呢?在recalc_task_prio函数中体现:
static void recalc_task_prio(task_t *p, unsigned long long now)
{
unsigned long long __sleep_time = now - p->timestamp; //时间间隔
unsigned long sleep_time;
if (__sleep_time > NS_MAX_SLEEP_AVG)
sleep_time = NS_MAX_SLEEP_AVG;
else
sleep_time = (unsigned long)__sleep_time;
if (likely(sleep_time > 0)) {
if (p->mm && p->activated != -1 && sleep_time > INTERACTIVE_SLEEP(p)) {
p->sleep_avg = JIFFIES_TO_NS(MAX_SLEEP_AVG - DEF_TIMESLICE);
} else {
sleep_time *= (MAX_BONUS - CURRENT_BONUS(p)) ? : 1;
if (p->activated == -1 && p->mm) { //由于IO而进行的睡眠不归为交互进程,这里的依据就是一般的等待IO睡眠都是不可中断的睡眠,当然不是很严谨
if (p->sleep_avg >= INTERACTIVE_SLEEP(p))
sleep_time = 0;
else if (p->sleep_avg + sleep_time >=
INTERACTIVE_SLEEP(p)) {
p->sleep_avg = INTERACTIVE_SLEEP(p);
sleep_time = 0;
}
}
p->sleep_avg += sleep_time; //如果顺利将非0的sleep_time叠加于sleep_avg之上,就说明该进程是交互进程,不管怎样都要根据sleep_avg提高它的优先级 了,最终的动态优先级由effective_prio返回。
if (p->sleep_avg > NS_MAX_SLEEP_AVG)
p->sleep_avg = NS_MAX_SLEEP_AVG;
}
}
p->prio = effective_prio(p); //返回最终的优先级
}
可以看到task_struct的一个sleep_avg字段决定了进程优先级的调整,sleep_avg体现了平均的睡眠时间,平均睡眠时间越长,说明该进程越要得到补偿,因此在effective_prio中有以下逻辑:
#define CURRENT_BONUS(p) /
(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / /
MAX_SLEEP_AVG)
bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;
prio = p->static_prio - bonus;
再看上面的recalc_task_prio函数,一个判断参量p->activated是怎么一回事呢?它十分重要,先看看设置它的地方:
[try_to_wake_up]
if (old_state == TASK_UNINTERRUPTIBLE) { //如果是不可中断的睡眠,那么在唤醒的时候将进程的activated设置为-1。这样就不会将该进程归为交互进程,交互进程一般都是可中断睡眠,因为往往中断将其打断,比如使用鼠标,键盘的进程
rq->nr_uninterruptible--;
p->activated = -1;
}
activate_task(p, rq, cpu == this_cpu);
[activate_task]
recalc_task_prio(p, now);
if (!p->activated) { //如果进程从运行以来没有睡眠过并且被唤醒过
if (in_interrupt()) //如果中断中唤醒,那么很有可能就是交互进程
p->activated = 2;
else { //如果是一般的唤醒,为交互进程的可能没有中断中唤醒的可能性大,但是仍然补偿一下它到运行队列但是没有投入运行这一段的等待时间,补偿的效果当然没有交互进程那么猛
p->activated = 1;
}
}
[schedule]
if (!rt_task(next) && next->activated > 0) { //优先级提升的条件,在前一次加入运行队列的时候activated大于0,也就是说不是一般的耗尽时间片导致的数组交换而来的运行权,而是被唤醒而得到的运行权,既然是被唤醒的,那么一定要补偿它,毕竟它在运行队列等待了一段不该等待的时间,因为如果它没有睡眠,它本不应该等待这段时间的。如果是中断 唤醒的交互进程,补偿的时间除了在运行队列的等待时间还要加上为了增加响应性而补偿的时间,也就是说对于交互进程,它睡眠的时间也要补偿,就是为了让系统更快的响应
unsigned long long delta = now - next->timestamp;
if (next->activated == 1) //该进程为一般的唤醒,不是中断唤醒的,优先级提升额度将降低
delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
array = next->array;
dequeue_task(next, array);
recalc_task_prio(next, next->timestamp + delta); //重新计算优先级
enqueue_task(next, array);
}
next->activated = 0; //任何进程在被调度的时候都会将其activated复位
activated 字段的作用想必从上面的代码中已经可以看出全貌了,这里也就没有必要输出更多的文字信息了。总的来说,O(1)调度器的这一类的算法十分复杂,有的人可能 说这些算法十分的智能,但是在内核中,这类算法可谓花哨,正是这些花哨的东西彻底出卖了O(1)调度器,最终cfs调度器取代了它。

你可能感兴趣的:(调度器)