还是神奇的进程调度问题引发的,参看​​Linux进程组调度机制分析​​,组调度机制是看清楚了,发现在重启过程中,很多内核调用栈阻塞在了double_rq_lock函数上,而double_rq_lock则是load_balance触发的,怀疑当时的核间调度出现了问题,在某个负责场景下产生了多核互锁,后面看了一下CPU负载平衡下的代码实现,写一下总结。

内核代码版本:kernel-3.0.13-0.27。

内核代码函数起自load_balance函数,从load_balance函数看引用它的函数可以一直找到​​schedule函数​​这里,便从这里开始往下看,在__schedule中有下面一句话。



 




1


2




if (unlikely(!rq->nr_running))


    idle_balance(cpu, rq);



从上面可以看出什么时候内核会尝试进行CPU负载平衡:即当前CPU运行队列为NULL的时候。

CPU负载平衡有两种方式:pull和push,即空闲CPU从其他忙的CPU队列中拉一个进程到当前CPU队列;或者忙的CPU队列将一个进程推送到空闲的​​CPU队列​​中。idle_balance干的则是pull的事情,具体push下面会提到。

在idle_balance里面,有一个proc阀门控制当前CPU是否pull:



 




1


2




if (this_rq->avg_idle < sysctl_sched_migration_cost)


    return;



sysctl_sched_migration_cost对应proc控制文件是/proc/sys/kernel/sched_migration_cost,开关代表如果CPU队列空闲了500us(sysctl_sched_migration_cost默认值)以上,则进行pull,否则则返回。

for_each_domain(this_cpu, sd) 则是遍历当前CPU所在的调度域,可以直观的理解成一个CPU组,​​类似task_group​​,核间平衡指组内的平衡。负载平衡有一个矛盾就是:负载平衡的频度和CPU cache的命中率是矛盾的,CPU调度域就是将各个CPU分成层次不同的组,低层次搞定的平衡就绝不上升到高层次处理,避免影响cache的命中率。

图例如下;

LINUX内核CPU负载均衡机制【转】_进程迁移

最终通过load_balance进入正题。

首先通过find_busiest_group获取当前调度域中的最忙的调度组,首先update_sd_lb_stats更新sd的状态,也就是遍历对应的sd,将sds里面的结构体数据填满,如下:



 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22




struct sd_lb_stats {


    struct sched_group *busiest; /* Busiest group in this sd */


    struct sched_group *this;  /* Local group in this sd */


    unsigned long total_load;  /* Total load of all groups in sd */


    unsigned long total_pwr;   /*    Total power of all groups in sd */


    unsigned long avg_load;       /* Average load across all groups in sd */


 


    /** Statistics of this group */


    unsigned long this_load; //当前调度组的负载


    unsigned long this_load_per_task; //当前调度组的平均负载


    unsigned long this_nr_running; //当前调度组内运行队列中进程的总数


    unsigned long this_has_capacity;


    unsigned int  this_idle_cpus;


 


    /* Statistics of the busiest group */


    unsigned int  busiest_idle_cpus;


    unsigned long max_load; //最忙的组的负载量


    unsigned long busiest_load_per_task; //最忙的组中平均每个任务的负载量


    unsigned long busiest_nr_running; //最忙的组中所有运行队列中进程的个数


    unsigned long busiest_group_capacity;


    unsigned long busiest_has_capacity;


    unsigned int  busiest_group_weight;



 



 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26




do


{


local_group = cpumask_test_cpu(this_cpu, sched_group_cpus(sg));


         if (local_group) {


                      //如果是当前CPU上的group,则进行赋值


            sds->this_load = sgs.avg_load;


            sds->this = sg;


            sds->this_nr_running = sgs.sum_nr_running;


            sds->this_load_per_task = sgs.sum_weighted_load;


            sds->this_has_capacity = sgs.group_has_capacity;


            sds->this_idle_cpus = sgs.idle_cpus;


        } else if (update_sd_pick_busiest(sd, sds, sg, &sgs, this_cpu)) {


                     //在update_sd_pick_busiest判断当前sgs的是否超过了之前的最大值,如果是


                     //则将sgs值赋给sds


            sds->max_load = sgs.avg_load;


            sds->busiest = sg;


            sds->busiest_nr_running = sgs.sum_nr_running;


            sds->busiest_idle_cpus = sgs.idle_cpus;


            sds->busiest_group_capacity = sgs.group_capacity;


            sds->busiest_load_per_task = sgs.sum_weighted_load;


            sds->busiest_has_capacity = sgs.group_has_capacity;


            sds->busiest_group_weight = sgs.group_weight;


            sds->group_imb = sgs.group_imb;


        }


        sg = sg->next;


} while (sg != sd->groups);



决定选择调度域中最忙的组的参照标准是该组内所有 CPU上负载(load) 的和, 找到组中找到忙的运行队列的参照标准是该CPU运行队列的长度, 即负载,并且 load 值越大就表示越忙。在平衡的过程中,通过比较当前队列与以前记录的busiest 的负载情况,及时更新这些变量,让 busiest 始终指向域内最忙的一组,以便于查找。

调度域的平均负载计算



 




1


2


3




sds.avg_load = (SCHED_POWER_SCALE * sds.total_load) / sds.total_pwr;


if (sds.this_load >= sds.avg_load)


    goto out_balanced;



在比较负载大小的过程中, 当发现当前运行的CPU所在的组中busiest为空时,或者当前正在运行的 CPU队列就是最忙的时, 或者当前 CPU队列的负载不小于本组内的平均负载时,或者不平衡的额度不大时,都会返回 NULL 值,即组组之间不需要进行平衡;当最忙的组的负载小于该调度域的平均负载时,只需要进行小范围的负载平衡;当要转移的任务量小于每个进程的平均负载时,如此便拿到了最忙的调度组。

然后find_busiest_queue中找到最忙的​​调度队列​​,遍历该组中的所有 CPU 队列,经过依次比较各个队列的负载,找到最忙的那个队列。



 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


28


29


30


31




for_each_cpu(i, sched_group_cpus(group)) {


/*rq->cpu_power表示所在处理器的计算能力,在函式sched_init初始化时,会把这值设定为SCHED_LOAD_SCALE (=Nice 0的Load Weight=1024).并可透过函式update_cpu_power (in kernel/sched_fair.c)更新这个值.*/


        unsigned long power = power_of(i);


        unsigned long capacity = DIV_ROUND_CLOSEST(power,SCHED_POWER_SCALE);


        unsigned long wl;


        if (!cpumask_test_cpu(i, cpus))


            continue;


 


        rq = cpu_rq(i);


/*获取队列负载cpu_rq(cpu)->load.weight;*/


        wl = weighted_cpuload(i);


 


        /*


         * When comparing with imbalance, use weighted_cpuload()


         * which is not scaled with the cpu power.


         */


        if (capacity && rq->nr_running == 1 && wl > imbalance)


            continue;


 


        /*


         * For the load comparisons with the other cpu's, consider


         * the weighted_cpuload() scaled with the cpu power, so that


         * the load can be moved away from the cpu that is potentially


         * running at a lower capacity.


         */


        wl = (wl * SCHED_POWER_SCALE) / power;


 


        if (wl > max_load) {


            max_load = wl;


            busiest = rq;


        }



通过上面的计算,便拿到了最忙队列。

当busiest->nr_running运行数大于1的时候,进行pull操作,pull前对move_tasks,先进行double_rq_lock加锁处理。



 




1


2


3


4




double_rq_lock(this_rq, busiest);


ld_moved = move_tasks(this_rq, this_cpu, busiest,


        imbalance, sd, idle, &all_pinned);


double_rq_unlock(this_rq, busiest);



move_tasks进程pull task是允许失败的,即move_tasks->balance_tasks,在此处,有sysctl_sched_nr_migrate开关控制进程迁移个数,对应proc的是/proc/sys/kernel/sched_nr_migrate。

下面有can_migrate_task函数检查选定的进程是否可以进行迁移,迁移失败的原因有3个,1.迁移的进程处于运行状态;2.​​进程被绑核​​了,不能迁移到目标CPU上;3.进程的cache仍然是hot,此处也是为了保证cache命中率。



 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17




    /*关于cache cold的情况下,如果迁移失败的个数太多,仍然进行迁移


     * Aggressive migration if:


     * 1) task is cache cold, or


     * 2) too many balance attempts have failed.


     */


 


    tsk_cache_hot = task_hot(p, rq->clock_task, sd);


    if (!tsk_cache_hot ||


        sd->nr_balance_failed > sd->cache_nice_tries) {


#ifdef CONFIG_SCHEDSTATS


        if (tsk_cache_hot) {


            schedstat_inc(sd, lb_hot_gained[idle]);


            schedstat_inc(p, se.statistics.nr_forced_migrations);


        }


#endif


        return 1;


    }



判断进程cache是否有效,判断条件,进程的运行的时间大于proc控制开关sysctl_sched_migration_cost,对应目录/proc/sys/kernel/sched_migration_cost_ns



 




1


2


3


4


5


6


7




static int


task_hot(struct task_struct *p, u64 now, struct sched_domain *sd)


{


        s64 delta;


    delta = now - p->se.exec_start;


    return delta < (s64)sysctl_sched_migration_cost;


}



在load_balance中,move_tasks返回失败也就是ld_moved==0,其中sd->nr_balance_failed++对应can_migrate_task中的"too many balance attempts have failed",然后busiest->active_balance = 1设置,active_balance = 1。



 




1


2


3


4


5




if (active_balance)


//如果pull失败了,开始触发push操作


stop_one_cpu_nowait(cpu_of(busiest),


    active_load_balance_cpu_stop, busiest,


    &busiest->active_balance_work);



push整个触发操作代码机制比较绕,stop_one_cpu_nowait把active_load_balance_cpu_stop添加到cpu_stopper每CPU变量的任务队列里面,如下:



 




1


2


3


4


5


6




void stop_one_cpu_nowait(unsigned int cpu, cpu_stop_fn_t fn, void *arg,


            struct cpu_stop_work *work_buf)


{


    *work_buf = (struct cpu_stop_work){ .fn = fn, .arg = arg, };


    cpu_stop_queue_work(&per_cpu(cpu_stopper, cpu), work_buf);


}



而cpu_stopper则是cpu_stop_init函数通过cpu_stop_cpu_callback创建的migration​​内核线程​​,触发任务队列调度。因为migration内核线程是绑定每个核心上的,进程迁移失败的1和3问题就可以通过push解决。active_load_balance_cpu_stop则调用move_one_task函数迁移指定的进程。

上面描述的则是整个pull和push的过程,需要补充的pull触发除了schedule后触发,还有scheduler_tick通过触发中断,调用run_rebalance_domains再调用rebalance_domains触发,不再细数。



 




1


2


3


4




void __init sched_init(void)


{


      open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);


}



 

​Linux内核CPU负载均衡机制​​来自于​​OenHan​

链接为:http://oenhan.com/cpu-load-balance


【作者】​​张昺华​

【知乎】 ​​http://www.zhihu.com/people/zhang-bing-hua​

【我的作品---旋转倒立摆】 ​​http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2​

【我的作品---自平衡自动循迹车】 ​​http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2​


【新浪微博】 张昺华--sky


【twitter】 @sky2030_


【微信公众号】 张昺华