您的位置:首页 > 运维架构 > Linux

Linux内核调度策略与算法分析 (2)

2012-03-07 23:25 344 查看
转自
http://hi.baidu.com/mychaoyue2011/blog/item/6df45895c3d63243d0135e01.html
4 Linux内核调度策略与算法分析

c = -1000;

list_for_each(tmp, &runqueue_head) { /* 从runqueue中选择一个最佳进程来运行 */

p = list_entry(tmp, struct task_struct, run_list);

if (can_schedule(p, this_cpu)) { /* 这个进程可以运行 */

/* 计算它的适应度,适应度最高的进程将被选择 */

int weight = goodness(p, this_cpu, prev->active_mm);

if (weight > c)

c = weight, next = p;

}

}

算法复杂度一目了然。

·可执行队列 runqueue

Linux为每个CPU定义了一个runqueue. 此处仅讨论一个CPU,并给出runqueue结构中一些关键的内容:

struct runqueue {

unsigned long nr_running; /* 处于TASK_RUNNING的进程数 */

task_t *curr, *idle; /* 该CPU的当前进程和空闲进程*/

prio_array_t *active, *expired, arrays[2];

/* 活跃和过期prio_array */

/* arrays[2]为他们实际占用的空间 */

/* other members ... */

};

这里最关键的成员是两个prio_array_t, 以及两个此类型的指针。其中active指针指向活跃进程的优先级数组,而expired指向过期进程的优先级数组。当一个进程时间片用完并且没有立刻重新激活的特权时,只需重新计算时间片,并且将它移入expired优先级数组即可。在 active数组为空时,只要将两个指针交换,由于expired数组中的时间片都已计算好,只要放到active的位置立刻可以执行,而空空如也的 active数组则恰好充当新的expired数组:

/* 以下代码位于void schedule(void)中 */

array = rq->active;

if (!array->nr_active) { /* active数组空了 */

rq->active = rq->expired;

rq->expired = array;

array = rq->active;

}

对比linux-2.4内核中时间片用净后重新计算的过程:

/* 以下代码位于linux 2.4的void schedule(void)中 */

/* Do we need to re-calculate counters? */

if (unlikely(!c)) {

struct task_struct *p;

/* ... */

for_each_task(p)

p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);

/* ... */

goto repeat_schedule;

}

又用了一个for循环.

有了上述准备,便可以完整地看一下scheduler_tick()和schedule()函数了:

·scheduler_tick()

Linux以频率HZ通过时钟中断调用一次会调用schedule_tick函数。它负责监控并调整当前进程的状态,并在适当的时候提出要求系统schedule的要求。以下是简化后的源代码:

void scheduler_tick(void)

{

runqueue_t *rq = this_rq(); /* 取得当前可执行队列 */

task_t *p = current; /* 取得当前进程 */

/* 进程可能已经过期了,但还没被schedule下来。

* 什么原因呢?我能想到的例子是:

* 可能从设置它为expired到schedule将它撤下前中断过于频繁,

* 导致执行scheduler_tick的时间又到了

* 肯定还有别的情况,以至于要set_tsk_need_resched(p)

*/

if (p->array != rq->active) {

set_tsk_need_resched(p); /* 标识当前进程为需要重新调度 */

return;

}

if (rt_task(p)) { /* 实时进程 */

/* 调度策略为SCHED_RR的需要计算时间片,而SCHED_FIFO类型的则不必 */

if ((p->policy == SCHED_RR) && !--p->time_slice) {

/* 如果时间片已用完 */

5 Linux内核调度策略与算法分析

p->time_slice = task_timeslice(p); /* 重新计算时间片 */

set_tsk_need_resched(p); /* 标识当前进程为需要重新调度 */

requeue_task(p, rq->active); /* 移到该优先级组的队列的后端 */

}

return; /* 实时进程调度完毕 */

}

/* 此处必为非实时进程 */

if (!--p->time_slice) { /* 如果时间片已用完 */

dequeue_task(p, rq->active); /* 移出当前优先数组 */

set_tsk_need_resched(p); /* 标识当前进程为需要重新调度 */

p->prio = effective_prio(p); /* 根据交互性表现重新计算优先级 */

p->time_slice = task_timeslice(p); /* 根据新的优先级计算时间片 */

/* 如果不是交互进程或者过期队列饿了 */

if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) {

/* 没有重新激活的特权,只好到过期数组等着了 */

enqueue_task(p, rq->expired);

} else /* 可以立刻重新激活的交互进程 */

enqueue_task(p, rq->active); /* 重新激活 */

} else { /* 非实时进程时间片尚未用完 */

/* 进行第二层轮转 */

if (TASK_INTERACTIVE(p) && !((task_timeslice(p) -

p->time_slice) % TIMESLICE_GRANULARITY(p)) &&

(p->time_slice >= TIMESLICE_GRANULARITY(p)) &&

(p->array == rq->active)) {

/* 如果当前进程 是交互的 且 小时间片已到 且 */

/* 剩余时间片还够一次小时间片调度 且 进程还活跃 */

requeue_task(p, rq->active); /* 从头部移出放在尾部 */

set_tsk_need_resched(p); /* 标识为需要重新调度 */

}

}

}

可以看到此处实现了前面策略中关于交互进程可以被立即再激活,以及交互进程的第二层轮转的处理。

在set_tsk_need_resched(p)后,系统会在一些特定的时刻执行schedule来完成调度。

·schedule()

schedule()函数是整个调度算法的核心。当有进程需要被调度时,系统会调用schedule函数。它被调用的方法主要有两类:

a) 其他内核代码主动调用

最显然的例子就是当进程需要被休眠时,便会在设置当前状态到非TASK_RUNNING后,主动调用schedule().

b) 在中断处理返回时调用

当系统从中断返回时,会检查当前进程是否need reschedule, 如果是则调用schedule()来调度当前进程:

# 以下代码出自arch/i386/kernel/entry.S

ret_from_intr: # 从中断返回

# ...

jz resume_kernel # 如果是从内核态中断的,则返回内核态

ENTRY(resume_kernel) # 返回内核态

# ...

need_resched:

movl TI_flags(%ebp), %ecx

testb $_TIF_NEED_RESCHED, %cl # 测试need_resched是否被设置?

jz restore_all # 如果被设置,那么去restore_all

#...

restore_all: # schedule将在那里被执行

#...

work_resched:

call schedule # 最终在这里执行

在被调用后,schedule()完成实际的调度工作,以下是简化后的源码(但仍然不短):

asmlinkage void __sched schedule(void)

{

task_t *prev, *next;

runqueue_t *rq;

prio_array_t *array;

struct list_head *queue;

unsigned long long now;

unsigned long run_time;

6 Linux内核调度策略与算法分析

int idx;

need_resched:

/* 取得现在正在运行的进程。

* 需要注意的是,它未必再当前的runqueue里,

* 而是可能已经被scheduler_tick()挪走了

*/

prev = current;

release_kernel_lock(prev);

need_resched_nonpreemptible:

rq = this_rq();

/* 下面统计本次运行的时间 */

now = sched_clock();

if (now - prev->timestamp < NS_MAX_SLEEP_AVG)

run_time = now - prev->timestamp;

else

run_time = NS_MAX_SLEEP_AVG;

/* run_time根据CURRENT_BONUS按比例缩小,以免一个交互性进程由

* 于这一次运行时间太长就失去了交互属性,起到“过滤高频噪声”

* 的作用。如果它真的开始大量占用CPU,那么在几次调整后便会平

* 滑地变成非交互进程。

*/

run_time /= (CURRENT_BONUS(prev) ? : 1);

spin_lock_irq(&rq->lock); /* 修改rq先上锁 */

/* 如果刚才运行的进程的状态已经不再是runnable,

* 并且不是明确指明不能抢占内核(下文祥叙)

*/

if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {

if ((prev->state & TASK_INTERRUPTIBLE) &&

signal_pending(prev))

/* 最后的机会:如果可以被信号唤醒而且信号也确实来了 */

prev->state = TASK_RUNNING; /* 那就继续运行吧 */

else {

/* 回天乏术 */

deactivate_task(prev, rq); /* 只好休息去了 */

}

}

if (!rq->nr_running) { /* 整个rq都没有东西在运行? */

next = rq->idle; /* 执行idle进程 */

goto switch_tasks;

}

/* 至此看来有东西可以运行 */

/* 下面判断是否需要交换active和expired优先级数组 */

array = rq->active;

if (!array->nr_active) {

rq->active = rq->expired; /* 交换 */

rq->expired = array;

array = rq->active;

}

/* 取得下一个应该运行的进程 */

idx = sched_find_first_bit(array->bitmap); /* 最高优先级的 */

queue = array->queue + idx;

next = list_entry(queue->next, task_t, run_list); /* 第一个进程 */

if (!rt_task(next) && next->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;

switch_tasks:

clear_tsk_need_resched(prev); /* 已调度,清空进程的need_resched标志 */

prev->sleep_avg -= run_time; /* 调整sleep_avg */

if ((long)prev->sleep_avg <= 0)

prev->sleep_avg = 0;

prev->timestamp = now; /* 记录时间,用于计算sleep_avg */

if (prev != next) { /* 确实需要切换进程 */

next->timestamp = now; /* 记录新进程开始时间 */

rq->curr = next;

/* 以下进行上下文切换 */

prepare_arch_switch(rq, next);

prev = context_switch(rq, prev, next);

barrier();

finish_task_switch(prev);

} else

spin_unlock_irq(&rq->lock);

prev = current;

if (reacquire_kernel_lock(prev) < 0)

goto need_resched_nonpreemptible;

/* 由于有上述主动调用法,所以推测schedule是可能被打断的,

* 从而有必要在最后判断一下是否又需要schedule了,

* 以免选择了一个不想或者不能被运行的进程

*/

if (test_thread_flag(TIF_NEED_RESCHED))

goto need_resched; /* sigh, 又要schedule了 */

}

注: 中间有一段提到了PREEMPT_ACTIVE, 而当这个值会被preempt_schedule在调用schedule前设置,然后在schedule返回后撤销。根据 preempt_schedule前的这段注释:

/*

* this is is the entry point to schedule() from in-kernel preemption

* off of preempt_enable. Kernel preemptions off return from interrupt

* occur there and call schedule directly.

*/

asmlinkage void __sched preempt_schedule(void)

也就是说这个函数会在内核抢占关闭,但又是返回到内核的时候被调用,那么PREEMPT_ACTIVE表示的是不能抢占内核。而当直接调用 schedule(),内核抢占会显式地发生。但从函数和常量的名字看好像意思恰恰相反,有些不解。

上面的代码实现了进程的调度和切换、优先级数组的切换。并且可以看到这里没有一个循环(那些goto只以很小的概率发生),是一个高效率的O(1)调度算法。尤其在前面对比了linux-2.4的实现后发现它的调度速度会快得多(尤其linux-2.4的schedule函数中有两个循环,而2.6中竟一个都没有),而且调度所需的时间也非常稳定,不会出现某次调度所需时间特别长的情况。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: