linux内核SMP负载均衡浅析( 三 )


这就好比我们用top命令查看CPU占用率一样,top命令默认1秒刷新一次,每次刷新你将看到这1秒内所有进程各自对CPU的占用情况 。这里的占用率是个统计值,假设有一个进程在这1秒内持续运行了100毫秒,那么我们认为它占用了10%的CPU 。如果把1秒刷新一次改成1毫秒刷新一次呢?那么我们将有90%的机率看到这个进程占用0%的CPU、10%的机率占用100%的CPU 。而无论是0%、还是100%,都不是这个进程真实的CPU占用率的体现 。必须把一段时间以内的CPU占用率综合起来看,才能得到我们需要的那个值 。
run_queue的load值也是这样 。有些进程可能频繁地在TASK_RUNNING和非TASK_RUNNING状态之间变换,导致run_queue的load值不断抖动 。光看某一时刻的load值,我们是体会不到run_queue的负载情况的,必须将一段时间内的load值综合起来看才行 。于是,run_queue结构中维护了一个保存load值的数组:
unsigned long cpu_load[CPU_LOAD_IDX_MAX] (目前CPU_LOAD_IDX_MAX值为5)
每个CPU上,每个tick的时钟中断会调用到update_cpu_load函数,来更新该CPU所对应的run_queue的cpu_load值 。这个函数值得罗列一下:
 
/* this_load就是run_queue实时的load值 */unsigned long this_load = this_rq->load.weight;for (i = 0, scale = 1; i < CPU_LOAD_IDX_MAX; i++, scale += scale) {    unsigned long old_load = this_rq->cpu_load[i];    unsigned long new_load = this_load;    /* 因为最终结果是要除以scale的,这里相当于上取整 */    if (new_load > old_load)        new_load += scale-1;    /* cpu_load[i] = old_load + (new_load - old_load) / 2^i */    this_rq->cpu_load[i] = (old_load*(scale-1) + new_load) >> i;}cpu_load[i] = old_load + (new_load - old_load) / 2^i 。i值越大,cpu_load[i]受load的实时值的影响越小,代表着越长时间内的平均负载情况 。而cpu_load[0]就是实时的load 。
 
尽管我们需要的是一段时间内的综合的负载情况,但是,为什么不是保存一个最合适的统计值,而要保存这么多的值呢?这是为了便于在不同场景下选择不同的load 。如果希望进行进程迁移,那么应该选择较小的i值,因为此时的cpu_load[i]抖动比较大,容易发现不均衡;反之,如果希望保持稳定,那么应该选择较大的i值 。
那么,什么时候倾向于进行迁移、什么时候又倾向于保持稳定呢?这要从两个维度来看:
第一个维度,是当前CPU的状态 。这里会考虑三种CPU状态:
1、CPU刚进入IDLE(比如说CPU上唯一的TASK_RUNNING状态的进程睡眠去了),这时候是很渴望马上弄一个进程过来运行的,应该选择较小的i值;
2、CPU处于IDLE,这时候还是很渴望弄一个进程过来运行的,但是可能已经尝试过几次都无果了,故选择略大一点的i值;
3、CPU非IDLE,有进程正在运行,这时候就不太希望进程迁移了,会选择较大的i值;
第二个维度,是CPU的亲缘性 。离得越近的CPU,进程迁移所造成的缓存失效的影响越小,应该选择较小的i值 。比如两个CPU是同一物理CPU的同一核心通过SMT(超线程技术)虚拟出来的,那么它们的缓存大部分是共享的 。进程在它们之间迁移代价较小 。反之则应该选择较大的i值 。(后面将会看到linux通过调度域来管理CPU的亲缘性 。)
至于具体的i的取值,就是具体策略的问题了,应该是根据经验或实验结果得出来的,这里就不赘述了 。
调度域前面已经多次提到了调度域(sched_domain) 。在复杂的SMP系统中,为了描述CPU与CPU之间的亲缘关系,引入了调度域 。
两个CPU之间的亲缘关系主要有以下几种:
1、超线程 。超线程CPU是一个可以“同时”执行几个线程的CPU 。就像操作系统通过进程调度能够让多个进程“同时”在一个CPU上运行一样,超线程CPU也是通过这样的分时复用技术来实现几个线程的“同时”执行的 。这样做之所以能够提高执行效率,是因为CPU的速度比内存速度快很多(一个数量级以上) 。如果cache不能命中,CPU在等待内存的时间内将无事可做,可以切换到其他线程去执行 。这样的多个线程对于操作系统来说就相当于多个CPU,它们共享着大部分的cache,非常之亲近;
2、同一物理CPU上的不同核心 。现在的多核CPU大多属于这种情况,每个CPU核心都有独立执行程序的能力,而它们之间也会共享着一些cache;
3、同一NUMA结点上的CPU;
4、不同NUMA结点上的CPU;
在NUMA(非一致性内存体系)中,CPU和RAM以“结点”为单位分组 。当CPU访问与它同在一个结点的“本地”RAM芯片时,几乎不会有竞争,访问速度通常很快 。相反的,CPU访问它所属结点之外的“远程”RAM芯片就会非常慢 。


推荐阅读