文章说明:
-
Linux内核版本:5.0
-
架构:ARM64
-
参考资料及图片来源:《奔跑吧Linux内核》
-
Linux 5.0内核源码注释仓库地址:
1. 历史累计衰减负载
负载的计算应当避免把历史工作负载和当前工作负载平等对待,因为历史工作负载在时间轴上会有衰减效应。从Linux 3.8内核以后进程的负载计算不仅考虑权重,而且跟踪每个调度实体的历史负载情况,该算法称为PELT(Per-entity Load Tacking)算法。在PELT算法里,引入了"the accumulation of an infinite geometric series",英文本义是无穷几何级数的累加,本文把这个概念简单称为历史累计计算。
把1ms(准确来说是1024us,为了方便移位操作)的时间跨度算成一个周期(period),简称PI。一个调度实体(可以是一个进程,也可以是一个调度组)在PI内对工作负载的贡献和以下两个因素有关:
- 进程的权重
- PI内可运行的时间(runnable_time)的衰减累计值,这里包括运行时间和等待CPU的时间
历史累计衰减负载的计算公式如下:
- decay_sum_load:历史累计衰减工作负载,简称累计工作总负载,它的值为累计衰减时间乘以权重。注意,这个值在Linux内核中只是—个临时使用的值
- decay_sum_time:历史累计衰减总时间,简称累计衰减总时间。这里计算进程在过去几个周期可运行时间的衰减累计值
- weight:进程的权重
历史累计衰减负载的意义在于,把进程的历史工作负载考虑进来,这样不仅可以避免系统无法分辨进程的运行行为,如进程长时间睡眠然后突然占用CPU,还可以避免进程长时间占用CPU。
计算一个进程的历史累计工作负载的难点是计算它在运行时的历史累计衰减总时间,下面我们来看一下如何计算历史累计衰减总时间。如下图所示,假设当前时间为A,P0表示当前时间A所在的周期,如P0在第0个周期里,P1表示过去的第1个周期,P2表示过去的第2个周期,Pn表示过去的第n个周期:
假设Li是一个调度实体在第i个周期内的贡献,那么这个累计衰减总时间的计算公式如下所示:
其中,y是一个预先选定好的衰减系数,y32约等于0.5,因此,计算过去的第32个周期的贡献度可以简单地把负载值减半。内核为了处理器计算方便,对衰减因子乘以2的32次方,计算完成后再右移32位,因为在处理器中,乘法运算比浮点运算快得多,乘以2的32次方的衰减系数存放在内核源码的runnable_avg_yN_inv[]
表中:
// 用于方便使用衰减系数,32 个下标对应过去 32ms 的负载贡献的衰减因子
static const u32 runnable_avg_yN_inv[] = {
0xffffffff, 0xfa83b2da, 0xf5257d14, 0xefe4b99a, 0xeac0c6e6, 0xe5b906e6,
0xe0ccdeeb, 0xdbfbb796, 0xd744fcc9, 0xd2a81d91, 0xce248c14, 0xc9b9bd85,
0xc5672a10, 0xc12c4cc9, 0xbd08a39e, 0xb8fbaf46, 0xb504f333, 0xb123f581,
0xad583ee9, 0xa9a15ab4, 0xa5fed6a9, 0xa2704302, 0x9ef5325f, 0x9b8d39b9,
0x9837f050, 0x94f4efa8, 0x91c3d373, 0x8ea4398a, 0x8b95c1e3, 0x88980e80,
0x85aac367, 0x82cd8698,
};
内核中的相关代码如下:
-
decay_load()
:用于计算第n个周期的衰减值 -
为了方便计算n个周期的衰减总和,内核维护了一个表runnable_avg_yN_sum[](Linux 4.12 内核中就已经删除了),其中已预先计算好如下值:
其中,n取1-32的整数,1024即一个周期是1024us。
-
___update_load_sum
:工作负载之和的计算
2. 量化负载的计算
如果我们要比较两个进程的负载大小,使用历史累计衰减负载似乎不太合理,原因在于,历史累计衰减负载中的权重是一个容易量化的值,而累计衰减时间是很难进行量化比较的。假设进程A和进程B的权重值相同,进程A运行了几天几夜,而进程B只运行了几分钟,如何比较进程A和进程B的负载呢?按照历史累计衰减负载来计算的,进程A的累计工作负载更大,因为它可以把更多的历史衰减的值统计在内,这是不科学的,那么如何比较和量化两个进程的负载呢?我们应该使用量化负载(也称历史累计衰减量化负载):
- decay_avg_load:量化负载
- decay_sum_runnable_time:就绪队列或者调度实体在可运行状态下的所有历史累计衰减时间
- decay_sum_period_time:就绪队列或者调度实体在所有的采样周期里全部时间的累加衰减时间。通常从进程开始执行时就计算和累计该值了
- weight:调度实体或者就绪队列的权重
内核里有—个宏LOAD_AVG_MAX
,用于表示在无限个时间周期里,历史累计衰减总时间的最大值是多少。LOAD_AVG_MAX
可以用于表示在无限个周期里decay_sum_period_time的最大值,因此量化负载的计算可以简化为:
在内核中利用函数 ___update_load_avg()
进行计算量化负载:
static __always_inline void
___update_load_avg(struct sched_avg *sa, unsigned long load, unsigned long runnable)
{
u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib;
/*
* Step 2: update *_avg.
*/
sa->load_avg = div_u64(load * sa->load_sum, divider);
sa->runnable_load_avg = div_u64(runnable * sa->runnable_load_sum, divider);
WRITE_ONCE(sa->util_avg, sa->util_sum / divider);
}
3. 实际算力的计算
处理器有一个计算能力的概念,也就是这个处理器最大的处理能力。在SMP架构下,系统中所有处理器的计算能力是一样的,但是在ARM的大/小核架构下,处理器的计算能力就不一 样了。Linux内核使用量化计算能力来描述处理器的计算能力,若系统中功能最强大的CPU的量化计算能力设定为1024,那么系统中计算能力比较弱的CPU的量化计算能力就要小于1024。通常,CPU的量化计算能力通过设备树或者BIOS等方式提供给Linux内核。rq数据结构中用cpu_capacity_orig成员来描述这个计算能力。
实际算力的计算公式:
- util_avg:实际算力
- decay_sum_running_time:统计就绪队列或者调度实体处于运行状态的历史累计衰减总时间
- decay_sum_period_time:就绪队列或者调度实体在所有采样周期里的累加衰减时间。通常从进程开始执行时就计算和累计该值了
- cpu_capacity:处理器的额定算力,它的默认值为1024(SCHED_CAPACITY_SCALE),也就是系统中最强的CPU的量化计算能力。另外,额定算力和处理器的运行频率有关
4. sched_avg 数据结构
sched_avg
数据结构用于描述调度实体的负载信息,另外,它还能描述一个就绪队列的负载信息:
struct sched_avg {
// 上一次更新的时间点,用于计算时间间隔
u64 last_update_time;
// 对于调度实体来说,它的统计对象是进程的调度实体在可运行状态下的累计衰减总时间
// 对于调度队列来说,它是调度队列中所有进程的累计工作总负载(decay_sum_load),即时间乘权重
u64 load_sum;
// 对于调度实体来说,它是在就绪队列里可运行状态下的累计衰减总时间(decay_sum_time)
// 对于调度队列来说,它统计就绪队列里所有可运行状态下进程的累计工作总负载(decay_sum_load)
u64 runnable_load_sum;
// 对于调度实体来说,它是正在运行状态下的累计衰减总时间(decay_sum_time)。使用cfs_rq->curr==se来判断当前进程是否正在运行
// 对于调度队列来说,它整个就绪队列中所有处于运行状态进程的累计衰减总时间(decay_sum_time)。只要就绪队列里有正在运行的进程,它就会去计算和累加
u32 util_sum;
// 存放着上一次时间采样时,不能凑成一个周期(1024us)的剩余的时间
u32 period_contrib;
// 对于调度实体来说,它是可运行状态下的量化负载(decay_avg_load)。在负载均衡算法中,使用该成员来衡量一个进程的负载贡献值,如衡量迁移进程的负载量
// 对于调度队列来说,它是调度队列中总的量化负载
unsigned long load_avg;
// 对于调度实体来说,它是可运行状态下的量化负载,等于load_avg
// 对于调度队列来说,它统计就绪队列里所有可运行状态下进程的总量化负载,在SMP负载均衡算法中使用该成员来比较CPU的负载大小
unsigned long runnable_load_avg;
// 实际算力。通常用于体现一个调度实体或者CPU的实际算力需求,类似于CPU使用率的概念
unsigned long util_avg;
struct util_est util_est;
} ____cacheline_aligned;