求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
linux内核调度算法
 

作者:russell_tao,发布于2012-3-2

 

linux内核调度算法(1)--快速找到最高优先级进程

为什么要了解内核的调度策略呢?呵呵,因为它值得我们学习,不算是废话吧。内核调度程序很先进很强大,管理你的LINUX上跑的大量的乱七八糟的进程,同时还保持着对用户操作的高灵敏响应,如果可能,为什么不把这种思想放到自己的应用程序里呢?或者,有没有可能更好的实现自己的应用,使得操作系统能够以自己的意志来分配资源给自己的进程?

带着这两个问题来看看KERNEL。首先回顾上我们开发应用程序,基本上就两种类型,1、IO消耗型:比如hadoop上的trunk服务,很明显它的消耗主要在IO上,包括网络IO磁盘IO等等。2、CPU消耗型,比如mapreduce或者其他的需要对大量数据进行计算处理的组件,就象对高清视频压缩成适合手机观看分辨率的进程,他们的消耗主要在CPU上。当两类进程都在一台SERVER上运行时,操作系统会如何调度它们呢?现在的服务器都是SMP多核的,那么一个进程在多CPU时会来回切换吗?如果我有一个程序,既有IO消耗又有CPU消耗,怎么让多核更好的调度我的程序呢?

又多了几个问题。来看看内核调度程序吧,我们先从它的优先队列谈起吧。调度程序代码就在内核源码的kernel/sched.c的schedule函数中。

首先看下面的优先级队列,每一个runqueue都有。runqueue是什么?下面会详细说下,现在大家可以理解为,内核为每一颗CPU分配了一个runqueue,用于维护这颗CPU可以运行的进程。runqueue里,有几个成员是prio_array类型,这个东东就是优先队列,先看看它的定义:

[cpp] view plaincopyprint?struct prio_array {

    unsigned int nr_active; 表示等待执行的进程总数

    unsigned long bitmap[BITMAP_SIZE]; 一个unsigned long在内核中只有32位哈,大家要跟64位OS上的C程序中的long区分开,那个是64位的。那么这个bitmap是干什么的呢?它是用位的方式,表示某个优先级上有没有待处理的队列,是实现快速找到最高待处理优先进程的关键。如果我定义了四种优先级,我只需要四位就能表示某个优先级上有没有进程要运行,例如优先级是2和3上有进程,那么就应该是0110.......非常省空间,效率也快,不是吗?

    struct list_head queue[MAX_PRIO]; 与上面的bitmap是对应的,它存储所有等待运行的进程。

};

struct prio_array {

    unsigned int nr_active; 表示等待执行的进程总数

    unsigned long bitmap[BITMAP_SIZE]; 一个unsigned long在内核中只有32位哈,大家要跟64位OS上的C程序中的long区分开,那个是64位的。那么这个bitmap是干什么的呢?它是用位的方式,表示某个优先级上有没有待处理的队列,是实现快速找到最高待处理优先进程的关键。如果我定义了四种优先级,我只需要四位就能表示某个优先级上有没有进程要运行,例如优先级是2和3上有进程,那么就应该是0110.......非常省空间,效率也快,不是吗?

    struct list_head queue[MAX_PRIO]; 与上面的bitmap是对应的,它存储所有等待运行的进程。

};

看看BITMAP_SIZE是怎么算出来的:#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))

那么,LINUX默认配置(如果你用默认选项编译内核的话)MAX_PRIO是140,就是说一共内核对进程一共定义了140种优先级。等待某个CPU来处理的进程中,可能包含许多种优先级的进程,但,LINUX是个抢占式调度算法的操作系统,就是说,需要调度时一定是找到最高优先级的进程执行。上面的BITMAP_SIZE值根据MAX_PRIO算出来为5,那么bitmap实际是32*5=160位,这样就包含了MAX_PRIO的140位。优先级队列是怎么使用的?看2649行代码:idx = sched_find_first_bit(array->bitmap);这个方法就用来快速的找到优先级最高的队列。看看它的实现可以方便我们理解这个优先级位的设计:

[cpp] view plaincopyprint?static inline int sched_find_first_bit(unsigned long *b)

{

    if (unlikely(b[0]))

    return __ffs(b[0]);

    if (unlikely(b[1]))

    return __ffs(b[1]) + 32;

    if (unlikely(b[2]))

    return __ffs(b[2]) + 64;

    if (b[3])

    return __ffs(b[3]) + 96;

    return __ffs(b[4]) + 128;

}

static inline int sched_find_first_bit(unsigned long *b)

{

    if (unlikely(b[0]))

    return __ffs(b[0]);

    if (unlikely(b[1]))

    return __ffs(b[1]) + 32;

    if (unlikely(b[2]))

    return __ffs(b[2]) + 64;

    if (b[3])

    return __ffs(b[3]) + 96;

    return __ffs(b[4]) + 128;

}

那么__ffs是干什么的?

[cpp] view plaincopyprint?static inline int __ffs(int x)

{

    int r = 0;

    if (!x)

    return 0;

    if (!(x & 0xffff)) {

        x >>= 16;

        r += 16;

    }

    if (!(x & 0xff)) {

        x >>= 8;

        r += 8;

    }

    if (!(x & 0xf)) {

        x >>= 4;

        r += 4;

    }

    if (!(x & 3)) {

        x >>= 2;

    r += 2;

    }

    if (!(x & 1)) {

        x >>= 1;

        r += 1;

    }

    return r;

}

static inline int __ffs(int x)

{

    int r = 0;

    if (!x)

    return 0;

    if (!(x & 0xffff)) {

        x >>= 16;

        r += 16;

    }

    if (!(x & 0xff)) {

        x >>= 8;

        r += 8;

    }

    if (!(x & 0xf)) {

        x >>= 4;

        r += 4;

    }

    if (!(x & 3)) {

        x >>= 2;

        r += 2;

    }

    if (!(x & 1)) {

        x >>= 1;

        r += 1;

    }

    return r;

}

sched_find_first_bit返回值就是最高优先级所在队列的序号,与queue是对应使用的哈,queue = array->queue + idx;这样就取到了要处理的进程队列。这个设计在查找优先级时是非常快的,非常值得我们学习。

好,优先级队列搞明白了,现在来看看runqueue,每个runqueue包含三个优先级队列。

[cpp] view plaincopyprint?struct runqueue {

    spinlock_t lock; 这是个自旋锁,nginx里解决惊群现象时也是用这个。与普通锁的区别就是,使用普通锁时,你去试图拿一把锁,结果发现已经被别人拿走了,你就在那睡觉,等别人锁用完了叫你起来。所以如果有一个人拿住锁了,一百个人都在门前睡觉等。当之前的人用完锁回来后,会叫醒所有100个等锁的人,然后这些人开始互相抢,抢到的人拿锁进去,其他的人继续等。自旋锁不同,当他去拿锁发现锁被别人拿走了,他在那不睡觉的等,稍打个盹就看看自己主动看看锁有没有还回来。大家比较出优劣了吗?

    /*

    * nr_running and cpu_load should be in the same cacheline because

    * remote CPUs use both these fields when doing load calculation.

    */

    unsigned long nr_running;

    #ifdef CONFIG_SMP

    unsigned long cpu_load;

    #endif

    unsigned long long nr_switches;

    /*

    * This is part of a global counter where only the total sum

    * over all CPUs matters. A task can increase this counter on

    * one CPU and if it got migrated afterwards it may decrease

    * it on another CPU. Always updated under the runqueue lock:

    */

    unsigned long nr_uninterruptible;

    unsigned long expired_timestamp;

    unsigned long long timestamp_last_tick;

    task_t *curr, *idle;

    struct mm_struct *prev_mm;

    prio_array_t *active, *expired, arrays[2];上面说了半天的优先级队列在这里,但是在runqueue里,为什么不只一个呢?这个在下面讲。

    int best_expired_prio;

    atomic_t nr_iowait;

     ... ...

};

struct runqueue {

    spinlock_t lock; 这是个自旋锁,nginx里解决惊群现象时也是用这个。与普通锁的区别就是,使用普通锁时,你去试图拿一把锁,结果发现已经被别人拿走了,你就在那睡觉,等别人锁用完了叫你起来。所以如果有一个人拿住锁了,一百个人都在门前睡觉等。当之前的人用完锁回来后,会叫醒所有100个等锁的人,然后这些人开始互相抢,抢到的人拿锁进去,其他的人继续等。自旋锁不同,当他去拿锁发现锁被别人拿走了,他在那不睡觉的等,稍打个盹就看看自己主动看看锁有没有还回来。大家比较出优劣了吗?

     /*

    * nr_running and cpu_load should be in the same cacheline because

    * remote CPUs use both these fields when doing load calculation.

    */

    unsigned long nr_running;

    #ifdef CONFIG_SMP

    unsigned long cpu_load;

    #endif

    unsigned long long nr_switches;

    /*

    * This is part of a global counter where only the total sum

    * over all CPUs matters. A task can increase this counter on

    * one CPU and if it got migrated afterwards it may decrease

    * it on another CPU. Always updated under the runqueue lock:

    */

    unsigned long nr_uninterruptible;

    unsigned long expired_timestamp;

    unsigned long long timestamp_last_tick;

    task_t *curr, *idle;

    struct mm_struct *prev_mm;

    prio_array_t *active, *expired, arrays[2];上面说了半天的优先级队列在这里,但是在runqueue里,为什么不只一个呢?这个在下面讲。

    int best_expired_prio;

    atomic_t nr_iowait;

    ... ...

};

LINUX是一个时间多路复用的系统,就是说,通过把CPU执行时间分成许多片,再分配给进程们使用,造成即使单CPU系统,也貌似允许多个任务在同时执行。那么,时间片大小假设为100ms,过短过长,过长了有些不灵敏,过短了,连切换进程时可能都要消耗几毫秒的时间。分给100个进程执行,在所有进程都用完自己的时间片后,需要重新给所有的进程重新分配时间片,怎么分配呢?for循环遍历所有的run状态进程,重设时间片?这个性能无法容忍!太慢了,跟当前系统进程数相关。那么2.6内核怎么做的呢?它用了上面提到的两个优先级队列active和expired,顾名思义,active是还有时间片的进程队列,而expired是时间片耗尽必须重新分配时间片的进程队列。

这么设计的好处就是不用再循环一遍所有进程重设时间片了,看看调度函数是怎么玩的:

[cpp] view plaincopyprint?array = rq->active;

if (unlikely(!array->nr_active)) {

    /*

    * Switch the active and expired arrays.

    */

    schedstat_inc(rq, sched_switch);

    rq->active = rq->expired;

    rq->expired = array;

    array = rq->active;

    rq->expired_timestamp = 0;

    rq->best_expired_prio = MAX_PRIO;

} else

schedstat_inc(rq, sched_noswitch);

array = rq->active;

if (unlikely(!array->nr_active)) {

     /*

    * Switch the active and expired arrays.

    */

    schedstat_inc(rq, sched_switch);

    rq->active = rq->expired;

    rq->expired = array;

    array = rq->active;

    rq->expired_timestamp = 0;

    rq->best_expired_prio = MAX_PRIO;

} else

schedstat_inc(rq, sched_noswitch);

当所有运行进程的时间片都用完时,就把active和expired队列互换指针,没有遍历哦,而时间片耗尽的进程在出acitve队列入expired队列时,已经单独的重新分配好新时间片了。

再看一下schedule(void)调度函数,当某个进程休眠或者被抢占时,系统就开始调试schedule(void)决定接下来运行哪个进程。上面说过的东东都在这个函数里有体现哈。

[cpp] view plaincopyprint?asmlinkage void __sched schedule(void)

{

    long *switch_count;

     task_t *prev, *next;

    runqueue_t *rq;

    prio_array_t *array;

    struct list_head *queue;

    unsigned long long now;

    unsigned long run_time;

    int cpu, idx;

     /*

    * Test if we are atomic. Since do_exit() needs to call into

    * schedule() atomically, we ignore that path for now.

    * Otherwise, whine if we are scheduling when we should not be.

    */

    if (likely(!(current->exit_state & (EXIT_DEAD | EXIT_ZOMBIE)))) {先看看当前运行进程的状态

    if (unlikely(in_atomic())) {

        printk(KERN_ERR "scheduling while atomic: "

        "%s/0x%08x/%d\n",

        current->comm, preempt_count(), current->pid);

        dump_stack();

    }

}

profile_hit(SCHED_PROFILING, __builtin_return_address(0));

need_resched:

preempt_disable();

prev = current;

release_kernel_lock(prev);

need_resched_nonpreemptible:

rq = this_rq(); 这行找到这个CPU对应的runqueue,再次强调,每个CPU有一个自己的runqueue

/*

* The idle thread is not allowed to schedule!

* Remove this check after it has been exercised a bit.

*/

if (unlikely(current == rq->idle) && current->state != TASK_RUNNING) {

    printk(KERN_ERR "bad: scheduling from the idle thread!\n");

    dump_stack();

}

schedstat_inc(rq, sched_cnt);

now = sched_clock();

if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG))

run_time = now - prev->timestamp;

else

run_time = NS_MAX_SLEEP_AVG;

/*

* Tasks with interactive credits get charged less run_time

* at high sleep_avg to delay them losing their interactive

* status

*/

if (HIGH_CREDIT(prev))

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

spin_lock_irq(&rq->lock);

if (unlikely(current->flags & PF_DEAD))

current->state = EXIT_DEAD;

/*

* if entering off of a kernel preemption go straight

* to picking the next task.

*/

switch_count = &prev->nivcsw;

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

    switch_count = &prev->nvcsw;

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

    unlikely(signal_pending(prev))))

    prev->state = TASK_RUNNING;

    else {

        if (prev->state == TASK_UNINTERRUPTIBLE)

        rq->nr_uninterruptible++;

        deactivate_task(prev, rq);

    }

}

cpu = smp_processor_id();

if (unlikely(!rq->nr_running)) {

    go_idle:

    idle_balance(cpu, rq);

    if (!rq->nr_running) {

        next = rq->idle;

        rq->expired_timestamp = 0;

        wake_sleeping_dependent(cpu, rq);

        /*

        * wake_sleeping_dependent() might have released

        * the runqueue, so break out if we got new

        * tasks meanwhile:

        */

        if (!rq->nr_running)

        goto switch_tasks;

    }

} else {

    if (dependent_sleeper(cpu, rq)) {

        next = rq->idle;

        goto switch_tasks;

    }

    /*

    * dependent_sleeper() releases and reacquires the runqueue

    * lock, hence go into the idle loop if the rq went

    * empty meanwhile:

     */

     if (unlikely(!rq->nr_running))

    goto go_idle;

}

array = rq->active;

if (unlikely(!array->nr_active)) { 上面说过的,需要重新计算时间片时,就用已经计算好的expired队列了

    /*

    * Switch the active and expired arrays.

    */

    schedstat_inc(rq, sched_switch);

    rq->active = rq->expired;

    rq->expired = array;

    array = rq->active;

    rq->expired_timestamp = 0;

    rq->best_expired_prio = MAX_PRIO;

} else

schedstat_inc(rq, sched_noswitch);

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:

if (next == rq->idle)

schedstat_inc(rq, sched_goidle);

prefetch(next);

clear_tsk_need_resched(prev);

rcu_qsctr_inc(task_cpu(prev));

prev->sleep_avg -= run_time;

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

     prev->sleep_avg = 0;

     if (!(HIGH_CREDIT(prev) || LOW_CREDIT(prev)))

    prev->interactive_credit--;

}

prev->timestamp = prev->last_ran = now;

sched_info_switch(prev, next);

if (likely(prev != next)) { 表面现在正在执行的进程,不是选出来的优先级最高的进程

    next->timestamp = now;

    rq->nr_switches++;

    rq->curr = next;

    ++*switch_count;

    prepare_arch_switch(rq, next);

    prev = context_switch(rq, prev, next); 所以需要完成进程上下文切换,把之前的进程信息CACHE住

    barrier();

     finish_task_switch(prev);

} else

spin_unlock_irq(&rq->lock);

prev = current;

if (unlikely(reacquire_kernel_lock(prev) < 0))

goto need_resched_nonpreemptible;

preempt_enable_no_resched();

if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))

goto need_resched;

}

asmlinkage void __sched schedule(void)

{

    long *switch_count;

    task_t *prev, *next;

    runqueue_t *rq;

    prio_array_t *array;

    struct list_head *queue;

    unsigned long long now;

    unsigned long run_time;

    int cpu, idx;

    /*

    * Test if we are atomic. Since do_exit() needs to call into

    * schedule() atomically, we ignore that path for now.

    * Otherwise, whine if we are scheduling when we should not be.

    */

    if (likely(!(current->exit_state & (EXIT_DEAD | EXIT_ZOMBIE)))) {先看看当前运行进程的状态

     if (unlikely(in_atomic())) {

        printk(KERN_ERR "scheduling while atomic: "

        "%s/0x%08x/%d\n",

        current->comm, preempt_count(), current->pid);

        dump_stack();

    }

}

profile_hit(SCHED_PROFILING, __builtin_return_address(0));

need_resched:

preempt_disable();

prev = current;

release_kernel_lock(prev);

need_resched_nonpreemptible:

rq = this_rq(); 这行找到这个CPU对应的runqueue,再次强调,每个CPU有一个自己的runqueue

/*

* The idle thread is not allowed to schedule!

* Remove this check after it has been exercised a bit.

*/

if (unlikely(current == rq->idle) && current->state != TASK_RUNNING) {

    printk(KERN_ERR "bad: scheduling from the idle thread!\n");

    dump_stack();

}

schedstat_inc(rq, sched_cnt);

now = sched_clock();

if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG))

run_time = now - prev->timestamp;

else

run_time = NS_MAX_SLEEP_AVG;

/*

* Tasks with interactive credits get charged less run_time

* at high sleep_avg to delay them losing their interactive

* status

*/

if (HIGH_CREDIT(prev))

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

spin_lock_irq(&rq->lock);

if (unlikely(current->flags & PF_DEAD))

current->state = EXIT_DEAD;

/*

* if entering off of a kernel preemption go straight

* to picking the next task.

*/

switch_count = &prev->nivcsw;

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

    switch_count = &prev->nvcsw;

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

    unlikely(signal_pending(prev))))

    prev->state = TASK_RUNNING;

    else {

        if (prev->state == TASK_UNINTERRUPTIBLE)

        rq->nr_uninterruptible++;

        deactivate_task(prev, rq);

    }

}

cpu = smp_processor_id();

if (unlikely(!rq->nr_running)) {

    go_idle:

    idle_balance(cpu, rq);

    if (!rq->nr_running) {

        next = rq->idle;

        rq->expired_timestamp = 0;

        wake_sleeping_dependent(cpu, rq);

        /*

         * wake_sleeping_dependent() might have released

        * the runqueue, so break out if we got new

        * tasks meanwhile:

        */

        if (!rq->nr_running)

        goto switch_tasks;

    }

} else {

    if (dependent_sleeper(cpu, rq)) {

        next = rq->idle;

        goto switch_tasks;

    }

    /*

    * dependent_sleeper() releases and reacquires the runqueue

    * lock, hence go into the idle loop if the rq went

    * empty meanwhile:

    */

    if (unlikely(!rq->nr_running))

    goto go_idle;

}

array = rq->active;

if (unlikely(!array->nr_active)) { 上面说过的,需要重新计算时间片时,就用已经计算好的expired队列了

    /*

    * Switch the active and expired arrays.

    */

    schedstat_inc(rq, sched_switch);

    rq->active = rq->expired;

    rq->expired = array;

    array = rq->active;

    rq->expired_timestamp = 0;

    rq->best_expired_prio = MAX_PRIO;

} else

schedstat_inc(rq, sched_noswitch);

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:

if (next == rq->idle)

schedstat_inc(rq, sched_goidle);

prefetch(next);

clear_tsk_need_resched(prev);

rcu_qsctr_inc(task_cpu(prev));

prev->sleep_avg -= run_time;

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

     prev->sleep_avg = 0;

    if (!(HIGH_CREDIT(prev) || LOW_CREDIT(prev)))

    prev->interactive_credit--;

}

prev->timestamp = prev->last_ran = now;

sched_info_switch(prev, next);

if (likely(prev != next)) { 表面现在正在执行的进程,不是选出来的优先级最高的进程

    next->timestamp = now;

    rq->nr_switches++;

    rq->curr = next;

    ++*switch_count;

    prepare_arch_switch(rq, next);

    prev = context_switch(rq, prev, next); 所以需要完成进程上下文切换,把之前的进程信息CACHE住

     barrier();

    finish_task_switch(prev);

} else

spin_unlock_irq(&rq->lock);

prev = current;

if (unlikely(reacquire_kernel_lock(prev) < 0))

goto need_resched_nonpreemptible;

preempt_enable_no_resched();

if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))

goto need_resched;

}

当然,在我们程序中,也可以通过执行以下系统调用来改变自己进程的优先级。nice系统调用可以改变某个进程的基本优先级,setpriority可以改变一组进程的优先级。

linux内核调度算法(2)--CPU时间片如何分配

内核在微观上,把CPU的运行时间分成许多分,然后安排给各个进程轮流运行,造成宏观上所有的进程仿佛同时在执行。双核CPU,实际上最多只能有两个进程在同时运行,大家在top、vmstat命令里看到的正在运行的进程,并不是真的在占有着CPU哈。

所以,一些设计良好的高性能进程,比如nginx,都是实际上有几颗CPU,就配几个工作进程,道理就在这。比如你的服务器有8颗CPU,那么nginx worker应当只有8个,当你多于8个时,内核可能会放超过多个nginx worker进程到1个runqueue里,会发生什么呢?就是在这颗CPU上,会比较均匀的把时间分配给这几个nginx worker,每个worker进程运行完一个时间片后,内核需要做进程切换,把正在运行的进程上下文保存下来。假设内核分配的时间片是100ms,做进程切换的时间是5ms,那么进程性能下降还是很明显的,跟你配置的worker有关,越多下降得越厉害。

当然,这是跟nginx的设计有关的。nginx是事件驱动的全异步进程,本身设计上就几乎不存在阻塞和中断,nginx的设计者就希望每一个nginx worker可以独占CPU的几乎全部时间片,这点就是nginx worker数量配置的依据所在。

当然,实际的运行进程里,大部分并不是nginx这种希望独占CPU全部时间片的进程,许多进程,比如vi,它在很多时间是在等待用户输入,这时vi在等待IO中断,是不占用时间片的,内核面对多样化的进程,就需要技巧性的分配CPU时间片了。

内核分配时间片是有策略和倾向性的。换句话说,内核是偏心的,它喜欢的是IO消耗型进程,因为这类进程如果不能及时响应,用户就会很不爽,所以它总会下意识的多分配CPU运行时间给这类进程。而CPU消耗进程内核就不太关心了。这有道理吗?太有了,CPU消耗型慢一点用户感知不出来,电信号和生物信号运转速度差距巨大。虽然内核尽量多的分配时间片给IO消耗型进程,但IO消耗进程常常在睡觉,给它的时间片根本用不掉。很合理吧?

那么内核具体是怎么实现这种偏心呢?通过动态调整进程的优先级,以及分配不同长短的CPU时间处来实现。先说内核如何决定时间片的长度。

对每一个进程,有一个整型static_prio表示用户设置的静态优先级,内核里它与nice值是对应的。看看进程描述结构里的static_prio成员。

[cpp] view plaincopyprint?struct task_struct {

    int prio, static_prio;

......}

struct task_struct {

    int prio, static_prio;

......}

nice值是什么?其实就是优先级针对用户进程的另一种表示法,nice的取值范围是-20到+19,-20优先级最高,+19最低。上篇曾经说过,内核优先级共有140,而用户能够设置的NICE优先级如何与这140个优先级对应起来呢?看代码:

[cpp] view plaincopyprint?#define MAX_USER_RT_PRIO 100

#define MAX_RT_PRIO MAX_USER_RT_PRIO

#define MAX_PRIO (MAX_RT_PRIO + 40)

#define MAX_USER_RT_PRIO 100

#define MAX_RT_PRIO MAX_USER_RT_PRIO

#define MAX_PRIO (MAX_RT_PRIO + 40)

可以看到,MAX_PRIO就是140,也就是内核定义的最大优先级了。

[cpp] view plaincopyprint?#define USER_PRIO(p) ((p)-MAX_RT_PRIO)

#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))

#define USER_PRIO(p) ((p)-MAX_RT_PRIO)

#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))

而MAX_USER_PRIO就是40,意指,普通进程指定的优先级别最多40,就像前面我们讲的那样-20到+19。

[cpp] view plaincopyprint?#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)

#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)

nice值是-20表示最高,对应着static_prio是多少呢?NICE_TO_PRIO(0)就是120,NICE_TO_PRIO(-20)就是100。

当该进程刚被其父进程fork出来时,是平分其父进程的剩余时间片的。这个时间片执行完后,就会根据它的初始优先级来重新分配时间片,优先级为+19时最低,只分配最小时间片5ms,优先级为0时是100ms,优先级是-20时是最大时间片800ms。我们看看内核是如何计算时间片长度的,大家先看下task_timeslice时间片计算函数:

[cpp] view plaincopyprint?#define SCALE_PRIO(x, prio) \

max(x * (MAX_PRIO - prio) / (MAX_USER_PRIO/2), MIN_TIMESLICE)

static unsigned int task_timeslice(task_t *p)

{

    if (p->static_prio < NICE_TO_PRIO(0))

    return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);

    else

    return SCALE_PRIO(DEF_TIMESLICE, p->static_prio);

}

#define SCALE_PRIO(x, prio) \

max(x * (MAX_PRIO - prio) / (MAX_USER_PRIO/2), MIN_TIMESLICE)

static unsigned int task_timeslice(task_t *p)

{

    if (p->static_prio < NICE_TO_PRIO(0))

    return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);

    else

    return SCALE_PRIO(DEF_TIMESLICE, p->static_prio);

}

这里有一堆宏,我们把宏依次列出看看它们的值:[cpp] view plaincopyprint?# define HZ 1000

#define DEF_TIMESLICE (100 * HZ / 1000)

# define HZ 1000

#define DEF_TIMESLICE (100 * HZ / 1000)

所以,DEF_TIMESLICE是100。假设nice值是-20,那么static_prio就是100,那么SCALE_PRIO(100*4, 100)就等于800,意味着最高优先级-20情形下,可以分到时间片是800ms,如果nice值是+19,则只能分到最小时间片5ms,nice值是默认的0则能分到100ms。

貌似时间片只与nice值有关系。实际上,内核会对初始的nice值有一个-5到+5的动态调整。这个动态调整的依据是什么呢?很简单,如果CPU用得多的进程,就把nice值调高点,等价于优先级调低点。CPU用得少的进程,认为它是交互性为主的进程,则会把nice值调低点,也就是优先级调高点。这样的好处很明显,因为1、一个进程的初始优先值的设定未必是准确的,内核根据该进程的实时表现来调整它的执行情况。2、进程的表现不是始终如一的,比如一开始只是监听80端口,此时进程大部分时间在sleep,时间片用得少,于是nice值动态降低来提高优先级。这时有client接入80端口后,进程开始大量使用CPU,这以后nice值会动态增加来降低优先级。

思想明白了,代码实现上,优先级的动态补偿到底依据什么呢?effective_prio返回动态补偿后的优先级,注释非常详细,大家先看下。

[cpp] view plaincopyprint?/*

* effective_prio - return the priority that is based on the static

* priority but is modified by bonuses/penalties.

*

* We scale the actual sleep average [0 .... MAX_SLEEP_AVG]

* into the -5 ... 0 ... +5 bonus/penalty range.

*

* We use 25% of the full 0...39 priority range so that:

*

* 1) nice +19 interactive tasks do not preempt nice 0 CPU hogs.

* 2) nice -20 CPU hogs do not get preempted by nice 0 tasks.

*

* Both properties are important to certain workloads.

*/

static int effective_prio(task_t *p)

{

    int bonus, prio;

    if (rt_task(p))

    return p->prio;

    bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;

    prio = p->static_prio - bonus;

    if (prio < MAX_RT_PRIO)

    prio = MAX_RT_PRIO;

    if (prio > MAX_PRIO-1)

    prio = MAX_PRIO-1;

    return prio;

}

/*

* effective_prio - return the priority that is based on the static

* priority but is modified by bonuses/penalties.

*

* We scale the actual sleep average [0 .... MAX_SLEEP_AVG]

* into the -5 ... 0 ... +5 bonus/penalty range.

*

* We use 25% of the full 0...39 priority range so that:

*

* 1) nice +19 interactive tasks do not preempt nice 0 CPU hogs.

* 2) nice -20 CPU hogs do not get preempted by nice 0 tasks.

*

* Both properties are important to certain workloads.

*/

static int effective_prio(task_t *p)

{

    int bonus, prio;

    if (rt_task(p))

    return p->prio;

    bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;

    prio = p->static_prio - bonus;

    if (prio < MAX_RT_PRIO)

    prio = MAX_RT_PRIO;

    if (prio > MAX_PRIO-1)

    prio = MAX_PRIO-1;

    return prio;

}

可以看到bonus会对初始优先级做补偿。怎么计算出这个BONUS的呢?

[cpp] view plaincopyprint?#define CURRENT_BONUS(p) \

(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / \

MAX_SLEEP_AVG)

#define CURRENT_BONUS(p) \

(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / \

MAX_SLEEP_AVG)

可以看到,进程描述符里还有个sleep_avg,动态补偿完全是根据它的值来运作的。sleep_avg就是关键了,它表示进程睡眠和运行的时间,当进程由休眠转到运行时,sleep_avg会加上这次休眠用的时间。在运行时,每运行一个时钟节拍sleep_avg就递减直到0为止。所以,sleep_avg越大,那么就会给到越大的动态优先级补偿,达到MAX_SLEEP_AVG时会有nice值-5的补偿。

内核就是这么偏爱交互型进程,从上面的优先级和时间片分配上都能看出来。实际上,内核还有方法对交互型进程搞优待。上篇说过,runqueue里的active和expired队列,一般的进程时间片用完后进expired队列,而对IO消耗的交互型进程来说,则会直接进入active队列中,保证高灵敏的响应,可见什么叫万千宠爱于一身了。

linux内核调度算法(3)--多核系统的负载均衡

多核CPU现在很常见,那么问题来了,一个程序在运行时,只在一个CPU核上运行?还是交替在多个CPU核上运行呢?LINUX内核是如何在多核间调度进程的呢?又是内核又是CPU核,两个核有点绕,下面称CPU处理器来代替CPU核。

实际上,如果你没有对你的进程做过特殊处理的话,LINUX内核是有可能把它放到多个CPU处理器上运行的,这是内核的负载均衡。上文说过,每个处理器上有一个runqueue队列,表示这颗处理器上处于run状态的进程链表,在多处理器的内核中,就会有多个runqueue,而如果他们的大小很不均衡,就会触发内核的load_balance函数。这个函数会把某个CPU处理器上过多的进程移到runqueue元素相对少的CPU处理器上。

举个例子来简单说明这个过程吧。当我们刚fork出一个子进程时,子进程也还在当前CPU处理器的runqueue里,它与父进程均分父进程的时间片。当然,时间片与多处理器间的负载均衡没有关系。假设我们的系统是双核的,父进程运行在cpu0上,那么这个fork出来的进程也是在cpu0的runqueue中。

那么,什么时候会发生负载均衡呢?

1、当cpu1上的runqueue里一个可运行进程都没有的时候。这点很好理解,cpu1无事可作了,这时在cpu1上会调用load_balance,发现在cpu0上还有许多进程等待运行,那么它会从cpu0上的可运行进程里找到优先级最高的进程,拿到自己的runqueue里开始执行。

2、第1种情形不适用于运行队列一直不为空的情况。例如,cpu0上一直有10个可运行进程,cpu1上一直有1个可运行进程,显然,cpu0上的进程们得到了不公平的对待,它们拿到cpu的时间要小得多,第1种情形下的load_balance也一直不会调用。所以,实际上,每经过一个时钟节拍,内核会调用scheduler_tick函数,而这个函数会做许多事,例如减少当前正在执行的进程的时间片,在函数结尾处则会调用rebalance_tick函数。rebalance_tick函数决定以什么样的频率执行负载均衡。

[cpp] view plaincopyprint?static void rebalance_tick(int this_cpu, runqueue_t *this_rq,

enum idle_type idle)

{

    unsigned long old_load, this_load;

    unsigned long j = jiffies + CPU_OFFSET(this_cpu);

    struct sched_domain *sd;

    /* Update our load */

    old_load = this_rq->cpu_load;

    this_load = this_rq->nr_running * SCHED_LOAD_SCALE;

    /*

    * Round up the averaging division if load is increasing. This

    * prevents us from getting stuck on 9 if the load is 10, for

    * example.

    */

    if (this_load > old_load)

    old_load++;

    this_rq->cpu_load = (old_load + this_load) / 2;

    for_each_domain(this_cpu, sd) {

        unsigned long interval;

        if (!(sd->flags & SD_LOAD_BALANCE))

        continue;

        interval = sd->balance_interval;

        if (idle != SCHED_IDLE)

        interval *= sd->busy_factor;

        /* scale ms to jiffies */

        interval = msecs_to_jiffies(interval);

        if (unlikely(!interval))

        interval = 1;

        if (j - sd->last_balance >= interval) {

            if (load_balance(this_cpu, this_rq, sd, idle)) {

                /* We've pulled tasks over so no longer idle */

                idle = NOT_IDLE;

            }

            sd->last_balance += interval;

        }

    }

}

static void rebalance_tick(int this_cpu, runqueue_t *this_rq,

enum idle_type idle)

{

    unsigned long old_load, this_load;

    unsigned long j = jiffies + CPU_OFFSET(this_cpu);

    struct sched_domain *sd;

     /* Update our load */

    old_load = this_rq->cpu_load;

    this_load = this_rq->nr_running * SCHED_LOAD_SCALE;

     /*

    * Round up the averaging division if load is increasing. This

    * prevents us from getting stuck on 9 if the load is 10, for

    * example.

    */

    if (this_load > old_load)

    old_load++;

    this_rq->cpu_load = (old_load + this_load) / 2;

     for_each_domain(this_cpu, sd) {

        unsigned long interval;

        if (!(sd->flags & SD_LOAD_BALANCE))

        continue;

        interval = sd->balance_interval;

        if (idle != SCHED_IDLE)

        interval *= sd->busy_factor;

        /* scale ms to jiffies */

        interval = msecs_to_jiffies(interval);

         if (unlikely(!interval))

         interval = 1;

        if (j - sd->last_balance >= interval) {

            if (load_balance(this_cpu, this_rq, sd, idle)) {

                /* We've pulled tasks over so no longer idle */

                idle = NOT_IDLE;

            }

            sd->last_balance += interval;

        }

    }

}

当idle标志位是SCHED_IDLE时,表示当前CPU处理器空闲,就会以很高的频繁来调用load_balance(1、2个时钟节拍),反之表示当前CPU并不空闲,会以很低的频繁调用load_balance(10-100ms)。具体的数值要看上面的interval了。

当然,多核CPU也有许多种,例如INTEL的超线程技术,而LINUX内核对一个INTEL超线程CPU会看成多个不同的CPU处理器。

上面说过,如果你没有对你的进程做过特殊处理的话,LINUX内核是有可能把它放到多个CPU处理器上运行的,但是,有时我们如果希望我们的进程一直运行在某个CPU处理器上,可以做到吗?内核提供了这样的系统调用。系统调用sched_getaffinity会返回当前进程使用的cpu掩码,而sched_setaffinity则可以设定该进程只能在哪几颗cpu处理器上执行。当我们对某些进程有强烈的期待,或者想自己来考虑CPU间的负载均衡,可以这么试试哈。

 
分享到
 
 
     


基于模型的整车电子电气架构设计
嵌入式设备上的 Linux 系统开发
Linux 的并发可管理工作队列
ARM嵌入式系统的问题总结分析
嵌入式系统设计与实例开发
WinCE6.0的EBOOT概要
更多...   


UML +RoseRealtime+嵌入式
C++嵌入式系统开发
嵌入式白盒测试
手机软件测试
嵌入式软件测试
嵌入式操作系统VxWorks


中国航空 嵌入式C高质量编程
使用EA和UML进行嵌入式系统分析设计
基于SysML和EA的嵌入式系统建模
上海汽车 嵌入式软件架构设计
北京 嵌入式C高质量编程
北京 高质高效嵌入式开发
Nagra linux内核与设备驱动原理
更多...