每个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; }
1
2
3
4
5
6
7
8
9
10
11/* 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,非常之亲近;
/* 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; }
1
2
3
4
5
6
7
8
9
10
11/* 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,非常之亲近;