前面我们学习了调度器的设计需要关注的几个点,在这里复习下:
Linux中调度器的设计,引入的概念
本节我们先来学习Linux早期的调度算法的设计,先从最早的调度器算法开始,此调度器时间复杂度是O(n),所以也可以称为O(n)调度算法。我们选择的内核版本是linux-2.4.19。
O(n)代表的是寻找一个合适的进程的时间复杂度。调度器定义了个runqueue的运行队列,将进程的状态变为Running的都会添加到此运行队列中,当然了不管是实时进程,还是普通进程都会添加到这个运行队列中。当需要从运行队列中需要一个合适的进程运行时,则就需要从队列的头遍历到尾部,所以说寻找一个合适进程的时间复杂度是O(n),当运行队列中的进程数目逐渐增大,则调度器的效率就会出现明显的下降。
运行队列中的进程是没有次序的,实时进程和普通进程是杂乱无章的在里面排序的。当需要调度器选择下一个进程的时候,则就需要从头遍历,比较每个进程的优先级,优先级高的先运行。当然了只有当实时进程运行完毕才可能轮到普通进程运行的。
struct task_struct {
long counter;
long nice;
unsigned long policy;
int processor;
unsigned long cpus_runnable, cpus_allowed;
}
O(n)调度器采用的是TICK的方式,根据对应进程的nice值来计算对应的时间片的。
#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)
nice值的取值范围是-20 ~ +19, 取值越小优先级越高。进程的默认nice值是0,则进程默认的静态优先级就等于20.
我们以100HZ来计算下,各个nice值下一个进程可以占用的时间片。
nice值 | -20 | -10 | 0 | +10 | +19 |
---|---|---|---|---|---|
100HZ | 11tick | 8tick | 6tick | 3tick | 1tick |
时间片 | 110ms | 80ms | 60ms | 30ms | 10ms |
当然了这些时间片是根据静态的优先级计算出来的,当进程运行起来后会对睡眠的进程做一个补偿的。
从运行队列中选择一个优先级最高的进程
list_for_each(tmp, &runqueue_head) {
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运行队列中逐个遍历,can_schedule函数用于判断当前进程是否可以在this_cpu上运行,是针对SMP系统的。
最主要的核心算法是在goodness函数中找出优先级最高的一个进程。
static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm)
{
/*
* Non-RT process - normal case first.
*/
if (p->policy == SCHED_OTHER) {
weight = p->counter;
if (!weight)
goto out;
#ifdef CONFIG_SMP
/* Give a largish advantage to the same processor... */
/* (this is equivalent to penalizing other processors) */
if (p->processor == this_cpu)
weight += PROC_CHANGE_PENALTY;
#endif
/* .. and a slight advantage to the current MM */
if (p->mm == this_mm || !p->mm)
weight += 1;
weight += 20 - p->nice;
goto out;
}
}
实时进程则是简单粗暴,直接是在实时进程的静态优先级上加上1000,因为每个实时进程的静态优先级是不一样的。
weight = 1000 + p->rt_priority;
随着时间的推移,所有的进程的时间片可能都会运行,这时候就需要对所有的进程进行一次时间片的初始化动作
/* Do we need to re-calculate counters? */
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;
}
也就是当从运行队列中没有找到可以运行的进程时,这时候会对所有的进程重新初始化counter。当然了一直的睡眠的进程可能时间片没有运行完,则需要将睡眠进程剩余的时间片加上。但是为了防止睡眠的IO消耗型进程优先级累计过高,则需要对半分。
系统中的tick中断会来更新当前进程的时间片的。
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;
update_one_process(p, user_tick, system, cpu);
if (p->pid) {
if (--p->counter <= 0) {
p->counter = 0;
p->need_resched = 1;
}
if (p->nice > 0)
kstat.per_cpu_nice[cpu] += user_tick;
else
kstat.per_cpu_user[cpu] += user_tick;
kstat.per_cpu_system[cpu] += system;
} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)
kstat.per_cpu_system[cpu] += system;
}
当每次tick中断到来之时,会对counter减1的。如果counter的值为0,则表示时间片已经用光,则需要设置need_resced的标志,在调度点会去判断当前进程是否设置此值,如果设置则进行调度。