Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1290769
  • 博文数量: 175
  • 博客积分: 2743
  • 博客等级: 少校
  • 技术积分: 4024
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-30 01:41
文章分类

全部博文(175)

文章存档

2015年(1)

2013年(53)

2012年(71)

2011年(50)

分类: LINUX

2013-04-17 16:43:26

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);这个方法就用来快速的找到优先级最高的队列。看看它的实现可以方便我们 理解这个优先级位的设计:

  1. static inline int sched_find_first_bit(unsigned long *b)
  2. {
  3.     if (unlikely(b[0]))
  4.     return __ffs(b[0]);
  5.     if (unlikely(b[1]))
  6.     return __ffs(b[1]) + 32;
  7.     if (unlikely(b[2]))
  8.     return __ffs(b[2]) + 64;
  9.     if (b[3])
  10.     return __ffs(b[3]) + 96;
  11.     return __ffs(b[4]) + 128;
  12. }

那么__ffs是干什么的?

  1. static inline int __ffs(int x)
  2. {
  3.     int r = 0;
  4.     if (!x)
  5.     return 0;
  6.     if (!(x & 0xffff)) {
  7.         x >>= 16;
  8.         r += 16;
  9.     }
  10.     if (!(x & 0xff)) {
  11.         x >>= 8;
  12.         r += 8;
  13.     }
  14.     if (!(x & 0xf)) {
  15.         x >>= 4;
  16.         r += 4;
  17.     }
  18.     if (!(x & 3)) {
  19.         x >>= 2;
  20.         r += 2;
  21.     }
  22.     if (!(x & 1)) {
  23.         x >>= 1;
  24.         r += 1;
  25.     }
  26.     return r;
  27. }

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

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

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是时间片耗尽必须重新分配时间片的进程队列。

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

  1. array = rq->active;
  2. if (unlikely(!array->nr_active)) {
  3.      /*
  4.     * Switch the active and expired arrays.
  5.     */
  6.     schedstat_inc(rq, sched_switch);
  7.     rq->active = rq->expired;
  8.     rq->expired = array;
  9.     array = rq->active;
  10.     rq->expired_timestamp = 0;
  11.     rq->best_expired_prio = MAX_PRIO;
  12. } else
  13. schedstat_inc(rq, sched_noswitch);

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

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



  1. asmlinkage void __sched schedule(void)

  2. {

  3.     long *switch_count;

  4.     task_t *prev, *next;

  5.     runqueue_t *rq;

  6.     prio_array_t *array;

  7.     struct list_head *queue;

  8.     unsigned long long now;

  9.     unsigned long run_time;

  10.     int cpu, idx;

  11.     /*

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

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

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

  15.     */

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

  17.      if (unlikely(in_atomic())) {

  18.         printk(KERN_ERR "scheduling while atomic: "

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

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

  21.         dump_stack();

  22.     }

  23. }

  24. profile_hit(SCHED_PROFILING, __builtin_return_address(0));

  25. need_resched:

  26. preempt_disable();

  27. prev = current;

  28. release_kernel_lock(prev);

  29. need_resched_nonpreemptible:

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

  31. /*

  32. * The idle thread is not allowed to

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

  34. */

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

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

  37.     dump_stack();

  38. }

  39. schedstat_inc(rq, sched_cnt);

  40. now = sched_clock();

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

  42. run_time = now - prev->timestamp;

  43. else

  44. run_time = NS_MAX_SLEEP_AVG;

  45. /*

  46. * Tasks with interactive credits get charged less run_time

  47. * at high sleep_avg to delay them losing their interactive

  48. * status

  49. */

  50. if (HIGH_CREDIT(prev))

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

  52. spin_lock_irq(&rq->lock);

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

  54. current->state = EXIT_DEAD;

  55. /*

  56. * if entering off of a kernel preemption go straight

  57. * to picking the next task.

  58. */

  59. switch_count = &prev->nivcsw;

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

  61.     switch_count = &prev->nvcsw;

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

  63.     unlikely(signal_pending(prev))))

  64.     prev->state = TASK_RUNNING;

  65.     else {

  66.         if (prev->state == TASK_UNINTERRUPTIBLE)

  67.         rq->nr_uninterruptible++;

  68.         deactivate_task(prev, rq);

  69.     }

  70. }

  71. cpu = smp_processor_id();

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

  73.     go_idle:

  74.     idle_balance(cpu, rq);

  75.     if (!rq->nr_running) {

  76.         next = rq->idle;

  77.         rq->expired_timestamp = 0;

  78.         wake_sleeping_dependent(cpu, rq);

  79.         /*

  80.          * wake_sleeping_dependent() might have released

  81.         * the runqueue, so break out if we got new

  82.         * tasks meanwhile:

  83.         */

  84.         if (!rq->nr_running)

  85.         goto switch_tasks;

  86.     }

  87. } else {

  88.     if (dependent_sleeper(cpu, rq)) {

  89.         next = rq->idle;

  90.         goto switch_tasks;

  91.     }

  92.     /*

  93.     * dependent_sleeper() releases and reacquires the runqueue

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

  95.     * empty meanwhile:

  96.     */

  97.     if (unlikely(!rq->nr_running))

  98.     goto go_idle;

  99. }

  100. array = rq->active;

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

  102.     /*

  103.     * Switch the active and expired arrays.

  104.     */

  105.     schedstat_inc(rq, sched_switch);

  106.     rq->active = rq->expired;

  107.     rq->expired = array;

  108.     array = rq->active;

  109.     rq->expired_timestamp = 0;

  110.     rq->best_expired_prio = MAX_PRIO;

  111. } else

  112. schedstat_inc(rq, sched_noswitch);

  113. idx = sched_find_first_bit(array->bitmap); 找到优先级最高的队列

  114. queue = array->queue + idx;

  115. next = list_entry(queue->next, task_t, run_list);

  116. if (!rt_task(next) && next->activated > 0) {

  117.     unsigned long long delta = now - next->timestamp;

  118.     if (next->activated == 1)

  119.     delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;

  120.     array = next->array;

  121.     dequeue_task(next, array);

  122.     recalc_task_prio(next, next->timestamp + delta);

  123.     enqueue_task(next, array);

  124. }

  125. next->activated = 0;

  126. switch_tasks:

  127. if (next == rq->idle)

  128. schedstat_inc(rq, sched_goidle);

  129. prefetch(next);

  130. clear_tsk_need_resched(prev);

  131. rcu_qsctr_inc(task_cpu(prev));

  132. prev->sleep_avg -= run_time;

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

  134.      prev->sleep_avg = 0;

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

  136.     prev->interactive_credit--;

  137. }

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

  139. sched_info_switch(prev, next);

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

  141.     next->timestamp = now;

  142.     rq->nr_switches++;

  143.     rq->curr = next;

  144.     ++*switch_count;

  145.     prepare_arch_switch(rq, next);

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

  147.      barrier();

  148.     finish_task_switch(prev);

  149. } else

  150. spin_unlock_irq(&rq->lock);

  151. prev = current;

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

  153. goto need_resched_nonpreemptible;

  154. preempt_enable_no_resched();

  155. if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))

  156. goto need_resched;

  157. }


当然,在我们程序中,也可以通过执行以下系统调用来改变自己进程的优先级。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成员。


struct task_struct {

    int prio, static_prio;

......}

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


#define MAX_USER_RT_PRIO 100

#define MAX_RT_PRIO MAX_USER_RT_PRIO

#define MAX_PRIO (MAX_RT_PRIO + 40)

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


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

#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))

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

#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时间片计算函数:


#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 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间的负载均衡,可以这么试试哈。


阅读(1748) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~