0%

Linux内核调度中负载的计算

基本逻辑

首先看下CPU利用率以及权重的定义。CPU利用率描述的对象是CPU,task只要放到CPU上跑就
要占用一定的CPU资源,没有task跑的时候CPU就是空闲的,所以CPU利用率在0到100%之间。
权重是线程优先级折算出的一个系数。

CPU负载指的是CPU上运行task的情况,内核不断的调度不同的task到CPU上运行,那我们怎
么度量CPU上的负载。负载度量的是CPU上需要运行的task的量,一般逻辑上,内核里用加入
rq里的时间和task存在的总时间的比值做负载的度量:

1
2
3
 task加入rq里的时间(runnable)
------------------------------ x 1024
task存在的时间(periods)

可以看到对于一个计算密集性的task,这个比值是接近1的,为了避免小数,把这个比值乘
以1024,最终这个值是接近1024的。当一个core上有多个计算密集性的task时,负载接近
task的个数乘以1024。需要注意的是,这种基于每个调度实体的负载计算方法是在v3.8的内
核版本引入的,名字叫per-entity load tracking(PELT)。需要注意的时候,本文中在不讨
论具体度量值含义时,一般笼统的叫各种度量值为“负载”,在具体讨论度量值的含义时会再
做具体的解释。

Linux内核在计算一个task或者一个调度实体的负载时,考虑了过去一段时间的负载对未来
的可能影响。直观上看,内核计算本时刻的负载,是为了调度服务,如果本时刻的负载很高,
调度器把task安排到合适的core上执行,下个时刻负载变的很低,这种安排就是不合理的,
所以,考虑过去一段时间的负载,是为了通过过去负载数据,得到这个调度实体的负载基本
情况。

具体上看,内核把负载的统计时间段(period)设定为1024us(大概是1ms,1024是为了方便计
算),调度实体某个时间的负载定义,是过去各个时间段的负载和一个逐渐减小的衰减系数
乘积之和:

1
load_sum = v0 + v1 * y^1 + v2 * y^2 + ... vn * y^n, 其中定义y^32 ~= 0.5。

在一个period内,如果调度实体一直在rq,那么对应的vn值就是1024。如上这个等比数列有
最大值存在,内核里对这个值的定义是:#define LOAD_AVG_MAX 47742。

我们可以写个demo的死循环程序,运行一段时间后观察对应task的load_sum的值。可以看到
如下的se.avg.load_sum的值比较接近最大值。从内核的计算代码上看,runnable_sum每次
计算会乘上1024,util_sum也会每次计算会乘上1024,但是util_sum只在task在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
# cat /proc/120/sched
a.out (120, #threads: 1)
-------------------------------------------------------------------
se.exec_start : 115459.735792
se.vruntime : 81051.015808
se.sum_exec_runtime : 80699.256128
se.nr_migrations : 0
nr_switches : 603
nr_voluntary_switches : 43
nr_involuntary_switches : 560
se.load.weight : 1048576
se.avg.load_sum : 47719 <---
se.avg.runnable_sum : 48870356
se.avg.util_sum : 44864269
se.avg.load_avg : 1023
se.avg.runnable_avg : 1023
se.avg.util_avg : 940
se.avg.last_update_time : 115459735552
se.avg.util_est : 664
policy : 0
prio : 120
clock-delta : 672
mm->numa_scan_seq : 0
numa_pages_migrated : 0
numa_preferred_nid : -1
total_numa_faults : 0
current_node=0, numa_group_id=0
numa_faults node=0 task_private=0 task_shared=0 group_private=0 group_shared=0

todo: load_avg/runnable_avg/util_avg的计算没有看懂。

代码分析

负载相关的数据结构为struct sched_avg,调度实体中会内嵌一个sched_avg。如下struct
load_weight指的是权重,不是各种负载的统计,struct sched_avg中各个域段才是负载的
描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sched_entity {

struct load_weight load;
+-> weight
+-> inv_weight

struct sched_avg avg;
+-> load_sum
+-> runnable_sum
+-> util_sum
+-> load_avg
+-> runnable_avg
+-> util_avg
}

负载相关的更新点有很多,但是,最终都会调用到update_load_avg。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/* linux/kernel/sched/fair.c */
update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
+-> __update_load_avg_se(now, cfs_rq, se)

/*
* 计算调度实体的load_sum/runnable_sum/util_sum,xxx_sum就是上面提到的各个
* period时间段加权统计量的和。xxx_sum的计算使用了一些技巧,我们把其中的逻辑
* 单独抽出来分析。
*/
if (___update_load_sum(now, &se->avg, !!se->on_rq, se_runnable(se),
cfs_rq->curr == se)) {

/* 计算各个平均量, load_avg计算中乘的load是这个se的权重 */
___update_load_avg(&se->avg, se_weight(se));

/*
* 除数为:PELT_MIN_DIVIDE + avg->period_contrib,即:
* LOAD_AVG_MAX*y + avg->period_contrib
*
* avg->period_contrib的含义是上面计算xxx_sum时,对于1024us的时间段,
* 多余出来的时间。
* inf inf
* LOAD_AVG_MAX * y = ( 1024 Sum y^n ) * y = 1024 Sum y^n
* n=0 n=1
*
* 所以,LOAD_AVG_MAX*y + avg->period_contrib为:
*
* inf
* 1024 Sum y^n + avg->period_contrib,是这个时间点sum的理论最大值。
* n=1
*
* 总结下,对于一个计算很密集的se(或task),load_avg会近似为它的权重,
* runnable_avg会近似为1024(1 << SCHED_CAPACITY_SHIFT),util_avg?
*
*/
+-> sa->load_avg = div_u64(load * sa->load_sum, divider);
+-> sa->runnable_avg = div_u64(sa->runnable_sum, divider);
+-> WRITE_ONCE(sa->util_avg, sa->util_sum / divider);

cfs_se_util_change(&se->avg);
}

/*
* 计算一个cfs_rq的各种负载统计,应该是rq上各个se的负载相加。在把一个se加
* 入rq时,会把se的负载统计加入rq的负载统计,代码路径在:(以CFS为例)
* sched_class->switched_to_fair->attach_task_cfs_rq->attach_entity_cfs_rq
* ->attach_entity_load_avg。
*
* 注意这里把cfs_rq->avg里的负载统计,也和entity->avg一样,使用衰减系数加
* 权处理了下。但是,在计算sum值时,传入的参数不一样,load为
* scale_load_down(cfs_rq->load.weight),runnable是rq上的线程数,running是
* 是否为CPU上正在运行的线程。
*
* 使用衰减系数加权的办法得到一个动态变化量的描述,cfs_rq的负载也这样计算
* 是没有问题的,需要注释的是为什么同样一套逻辑可以既处理se的负载又可以处
* 理cfs_rq的负载。
*/
+-> decayed = update_cfs_rq_load_avg(now, cfs_rq);
+-> decayed |= __update_load_avg_cfs_rq(now, cfs_rq);

/* 没有搞懂这里的逻辑?*/
+-> decayed |= propagate_entity_load_avg(se);

/*
* 如果是se挂入rq的情况, 并且是挂入一个新的CPU,可以看到se相关的负载统计
* 也一并加到rq的负载统计里。
*/
+-> if (!se->avg.last_update_time && (flags & DO_ATTACH)) {

attach_entity_load_avg(cfs_rq, se);
update_tg_load_avg(cfs_rq);

+-> } else if (flags & DO_DETACH) {

detach_entity_load_avg(cfs_rq, se);
update_tg_load_avg(cfs_rq);

+-> } else if (decayed) {

cfs_rq_util_change(cfs_rq, 0);
+-> cpufreq_update_util

if (flags & UPDATE_TG)
update_tg_load_avg(cfs_rq);
}

___update_load_sum细节逻辑展开如下。我们的目的是计算如上load_sum的值,计算load_sum
的值其实不必要知道过去每个1024us域段对应的vn值,我们直接借助内核代码里的注释说明
问题。

1
2
3
4
5
6
7
8
9
period_contrib  d1          d2           d3                                        
^ ^ ^ ^
| | | |
|<->|<->|<----------------->|<--->|
... |---x---|------| ... |------|-----x (now)
^ n=p-1 n = 1 ^
| |

上次更新时间点 本次更新时间点

本次更新时间点负载的定义以及推导如下,p_c是上面period_contrib的简称。

load_sum = v0 + v1 * y^1 + v2 * y^2 + ... vn * y^n ...
         
         = d3 + 1024 * y^1 + 1024 * y^2 + ... 1024 * y^(p - 1) + (d1 + p_c) * y^p + ...

                     p-1
         = d3 + 1024 Sum* y^n +  (d1 + p_c) * y^p + ...
                     n=1
                     p-1
         = d3 + 1024 Sum* y^n +  d1 * y^p + p_c * y^p + 1024 *y^(p + 1) + ...
                     n=1
                     p-1
         = d3 + 1024 Sum* y^n +  d1 * y^p + (p_c + 1024 * y^1 + 1024 * y^2 + ...)
                     n=1                    |<-- 就是上次更新时间点负载的定义-->|

如上的结果就是内核accumulate_sum函数的注释中一开始的公式。需要注意的是d2域段的系
数为什么都是1024,参考本文一开始的负载计算的逻辑,只要d2这段时间,se都在rq里,那
么rq时间和每一段的时间相等,比值就只剩下1024了。