2.3.2 O(n)调度器 - 时间分配

对普通任务而言,为了保证系统在时间分配上的公平性,O(n)调度器按照“轮”来计量调度周期,在每轮调度中,调度器根据各个任务的优先级为其分配时间,当所有任务的时间片都消耗完了之后,本轮度结束,系统开启下一轮调度。在 goodness() 中我们可以看到,如果一个普通任务已经没有了剩余时间,则会直接被忽略。

实时任务不受调度周期的限制,因为实时任务的高优先级最高,调度器要确保只要实时任务处于Ready状态,那么就立刻被调度到,本质上不存在预先分配时间的必须性。

调度器只是用运行时间来完成对 SCHED_RR 任务的轮询控制,函数 schedule() 在遍历就绪队列之前存在如下逻辑:

asmlinkage void schedule(void) {
    if (unlikely(prev->policy == SCHED_RR))
        if (!prev->counter) {
            prev->counter = NICE_TO_TICKS(prev->nice);
            move_last_runqueue(prev);
        }

如果当前 SCHED_RR 任务的运行时间已经耗尽,则重新通过 NICE_TO_TICKS(prev->nice) 重新为其分配时间,并将其移动到队列尾部,这样如果队列中有相同优先级的 SCHED_RR 任务的话,本次就会被选中,从而达到轮询的目的。

如果在遍历队列时没有找到合适的任务,那么说明所有任务的运行时间都已经耗尽,本次调度周期已经结束了,调度器需要为任务分配时间、开启新的一轮调度周期。处理该逻辑的代码如下:

/* file: kernel/sched.c */

asmlinkage void schedule(void) {
    /* 删除前面的代码 */

    /* 1. 遍历队列 */
repeat_schedule:
    next = idle_task(this_cpu);
    c = -1000;
    list_for_each(tmp, &runqueue_head) {
        p = list_entry(tmp, struct task_struct, run_list);
        if (can_schedule(p, this_cpu)) { /* 判断 p 是否可以在 this_cpu 上运行 */
            int weight = goodness(p, this_cpu, prev->active_mm);
            if (weight > c)
                c = weight, next = p;
        }
    }

    /* 2. 通过 c 判断是否有合适的任务被选中 */
    if (unlikely(!c)) {
        struct task_struct *p;

        spin_unlock_irq(&runqueue_lock);
        read_lock(&tasklist_lock);
        /* 遍历系统的所有任务,并为其分配运行时间 */
        for_each_task(p) p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
        read_unlock(&tasklist_lock);
        spin_lock_irq(&runqueue_lock);
        /* 分配完运行时间后重新调度 */
        goto repeat_schedule;
    }
}

/* 删除后面的代码 */

在下一轮调度周期中,任务的运行时间为 (p->counter >> 1) + NICE_TO_TICKS(p->nice);, 这里涉及两个方面:

  1. p->counter >> 1 上面遍历时不是没有找到合适的任务吗,那这里为何还要关心 p->counter 呢?因为上面遍历的是就绪队列(runqueue_head),所以调度器只是没有找到合适的就绪任务,而这里为任务分配时间片时,针对的是系统所有的任务。 此时系统中处于睡眠状态的任务就可能还有剩余时间,因此我们需要对这些任务的运行时间进行累积,让他们醒过来之后能够有更多的机会运行。为了避免此类任务的运行时间无限累积下去,每次都只累计一半的剩余时间。

  2. NICE_TO_TICKS(p->nice) ticks 就代表着运行时间,宏 NICE_TO_TICKS 根据任务的静态优先级(nice)为其分配运行时间。O(n)调度器的时间分配与CPU 频率也有关,具体逻辑如下:

    /* file: kernel/sched.c */
    
    #if HZ < 200
    #define TICK_SCALE(x) ((x) >> 2)
    #elif HZ < 400
    #define TICK_SCALE(x) ((x) >> 1)
    #elif HZ < 800
    #define TICK_SCALE(x) (x)
    #elif HZ < 1600
    #define TICK_SCALE(x) ((x) << 1)
    #else
    #define TICK_SCALE(x) ((x) << 2)
    #endif
    
    #define NICE_TO_TICKS(nice) (TICK_SCALE(20 - (nice)) + 1)

    从直觉上来说,给任务分配时间应该只与优先级相关,与CPU的频率无关。系统做这种处理的目的是为了将运行时间根据CPU的频率做一个伸缩(scale), 否则如果一个低频CPU给每个任务分配的运行时间很长的话,就会导致一次调度周期很久都不会结束。

Last updated