本文代码均基于主线4.19 LTS ,欢迎指正,持续更新。
目录
1. 度量
1.1 优先级
1.2 Weight
1.3 virtual runtime
1.4 physical runtime
1.5 nr_running
1.6 load三件套
1.6.1 衰减算法
1.6.2 load
1.6.3 blocked load
1.6.4 runnable load
1.6.5 util
1.6.6 层级
1.7 capacity
1.8 task_h_load
2. 调度
2.1 红黑树
2.2 Timeline
2.3 Period
2.4 层级
2.5 Quota
2.5.1 cfs_period_us和cfs_quota_us
2.5.2 CPU时间的统计
2.5.2 如何节流
2.5.2 如何恢复
3 负载均衡
3.1 调度域
3.2 load imbalance
3.2.1 busiest group
3.2.2 imbalance
3.2.3 busiest rq
3.3 Wakeup
3.3.1 Cache Hot
3.3.2 IDLE CPU
4 问题讨论
4.1 cfs_quota和cpuset
ps ax -o pid,ni,pri,cmd
pid ni pri cmd
255 -20 39 [kworker/40:0H]
256 0 19 [cpuhp/41]
257 - 139 [watchdog/41]
258 - 139 [migration/41]
259 0 19 [ksoftirqd/41]
对于使用cfs的进程,我们会看到有两个优先级
在内核中,nice值会被转换成prio,具体可以参考宏NICE_TO_PRIO()。
每个调度实体都拥有一个权重,即sched_entity.load.weight。
对于一个普通进程,task_struct.se.load.weight来自其优先级,具体可以参考,
const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
当nice值增长1,weight增大25%,这是为了保证,
“ cfs scheduling algorithm guarantee, if you go up 1 level, it's -10% CPU usage,
if you go down 1 level it's +10% CPU usage.”
比如,我们有nice 0和nice 1两个任务跑在一个cpu上,
task with nice 0 get 1.25/2.25 = 0.55 cpu time
task with nice 1 get 1/2.25 = 0.44 cpu time
it is about 0.11 gap here
对于一个任务组,其包含了两组per-cpu的结构,
它们每个都包含了load.weight,
tg->shares * grq->load.weight
ge->load.weight = -----------------------------
Sum grq->load.weight
ge : task group per-cpu sched_entity
grp : task group per-cpu cfs_rq
具体计算过程与上述公式略有差别,详情参考calc_group_shares()函数及其注释
即sched_entity.vruntime,其决定的是该调度实体在红黑树cfs_rq.tasks_timeline中的位置,决定了该调度实体何时可以被调度;其计算方式如下:
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
计算公式为,vruntime = (delta_exec * NICE_0_LOAD) / se.load.weight
对照sched_prio_to_weight可知,优先级越高,weight越大,则vruntime增长的越慢,在红黑树上的位置向右移动的速度越慢,则可以越多的执行时间。
即当前正在运行的调度实体已经运行的物理时间,
update_curr()
---
u64 now = rq_clock_task(rq_of(cfs_rq));
...
delta_exec = now - curr->exec_start;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
---
该时间本身并无特殊之处,其主要用途包括,
跟cfs有关的nr_running有三个,
综上,rq.nr_running是在rq上所有的running的任务的数量,而rq.cfs.h_nr_running则代表所有的cfs任务的数量,基于此,pick_next_task()通过两者是否相等判断该rq上是否只有cfs任务,
/*
* Optimization: we know that if all tasks are in the fair class we can
* call that function directly, but only if the @prev task wasn't of a
* higher scheduling class, because otherwise those loose the
* opportunity to pull in more work from other CPUs.
*/
if (likely((prev->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = pick_next_task_fair(rq, prev, rf);
cfs在计算load的时候,引入了一个衰减算法,如下:
V = Y*P + delta
Y : decay factor, Y^32 = 0.5, Y ~= 0.9785
P : previous value of V
用下面的程序模拟一个Long running task的计算过程,
int main(int argc, const char *argv[])
{
float y, p;
int i, s, d, m;
y = 0.9785;
p = d = 1000;
s = atoi(argv[1]);
m = atoi(argv[2]);
for (i = 0; i < m; i++) {
if (i % s)
p = p * y;
else
p = p * y + d;
printf("%.0f\n", p);
}
return 0;
}
可以看到有两个点:
如果我们让load一直衰减,也就是delta一直为0时,如下:
#include
#include
int main(int argc, const char *argv[])
{
float y, p;
int i, s, d, m;
y = 0.9785;
p = d = 1024;
s = atoi(argv[1]);
m = atoi(argv[2]);
for (i = 0; i < m; i++) {
if (i % s)
p = p * y;
else
p = p * y + d;
printf("%.0f\n", p);
}
while ((int)p) {
p = p * y;
printf("%.0f\n", p);
}
return 0;
}
可以看到,load从最大值衰减到0会经历一定的时间。
接下来我们看下cfs如何使用这个算法计算load_avg,参考函数___update_load_sum()
static int ___update_load_sum(u64 now, struct sched_avg *sa,
unsigned long load, unsigned long runnable, int running)
load, runnable, running
: 累加delta时的系数,这三个系数分别用来统计三种类型的load
now : 来自cfs_rq_clock_pelt(),该函数将该cfs_rq被throttle的时间去掉了,参考
throttle/unthrottle_cfs_rq(),在被throttle期间,se.on_rq是0,也就是说,
在load_avg计算中,throttle状态不算离开cpu。
last_update_time:
该load_avg的上次更新时间,参考函数update_load_avg()的调用点,涵盖了所有的
sched_entity状态更新函数;now - last_update_time就是delta,1024ns为一个
衰减周期
结果保存在sum中(有load_sum, runnable_sum, util_sum三种),计算公式可以大致表
Sum = Sum * Decay + delta * Factor
___update_load_avg()则计算出avg,同样有(load, runnable, util三种),算法大致如下:
Avg = (Factor * Sum) / divider
divider : 近似等于LOAD_AVG_MAX,在解释load算法时,我们说过,delta一定的情况下,
load计算出的最大值是一定的
FIXME: 不太理解这里为什么称为平均值,Max是step = 1,也就是任务一直处于running状态的Sum,
Sum/Max的意义更像是评估任务的 - 忙碌程度
在下文中,我们统一称,计算Sum的Factor为Sum_Factor, 计算Avg的Factor为Avg_Factor,Sum/Max称为Busy_Factor。
uptime命令中显示的load,统计的是系统中处于running状态和waitio状态的任务的数量;接下来我们看下cfs中的load表征的是什么?
在cfs调度算法中,有两个实例拥有load,即sched_entity和cfs_rq,下面我们分别开下,他们是如何计算的,
更新sched_entity的load_avg,
int __update_load_avg_se(u64 now, struct cfs_rq *cfs_rq, struct sched_entity *se)
{
if (___update_load_sum(now, cpu, &se->avg, !!se->on_rq, !!se->on_rq,
cfs_rq->curr == se)) {
___update_load_avg(&se->avg, se_weight(se), se_runnable(se));
}
!!se->on_rq : Sum_Factor,0或者1代表该实例是否被调度
se_weight(se) : Avg_Factor,即该sched_entity的load.weight
load_avg的意义是 weight * Busy_Factor
如果该任务100% busy,则load_avg = weight,但是如果50% busy,load_avg = weight/2
(参考load 计算时step = 2的情况,load最终的值大概只有step = 1时候的一半)
sched_entity的load_avg反映了它的load.weight和繁忙程度。
注意,这里的繁忙,并不是只是该任务被cpu运行,还包括处于就绪态,或者从linux内核的角度讲,处于running状态;这是非常合理的,load balance的一个重要的作用就是,避免一个cpu上累积了太多running的进程排队导致调度延迟。
cfs_rq的sched_avg反映的是其下所有的sched_entity的load.weight与繁忙程度;
int __update_load_avg_cfs_rq(u64 now, struct cfs_rq *cfs_rq)
{
if (___update_load_sum(now, &cfs_rq->avg,
scale_load_down(cfs_rq->load.weight),
scale_load_down(cfs_rq->runnable_weight),
cfs_rq->curr != NULL)) {
___update_load_avg(&cfs_rq->avg, 1, 1);
}
}
cfs_rq->load.weight : Sum_Factor是该cfs_rq上所有sched_entity的权重之和
1 : __update_load_avg()的入参是 1,也就是
load_avg = load_sum / LOAD_AVG_MAX
那么为什么不让Sum_Factor = 1,而让Avg_Factor = cfs_rq.load.weight ?
__update_load_avg()中的计算覆盖过去所有的历史,这对于会频发发生enqueue/dequeue的cfs_rq来说
明显是不合理的;__update_load_sum()中对delta的累加系数则只针对刚刚过去的这段时间,且这段时间
并没有状态变化(每次调度状态的变化都会调用update_load_avg)。
综上,cfs_rq的sched_avg反映的是位于其上的所有sched_entity的load.weight和繁忙程度。
对于因等待资源而block的任务的load,cfs如何处理?load的算法会让load值在之后的时间里相对平滑的衰减;一个sched_entity的load,在对一个任务被blocked之后,load值不会立刻减到0,而是有一个平滑的衰减的过程;也就是说,即使一个任务不处于running状态,其仍然可能拥有一个不为0的load,我们称之为blocked load(FIXME: 个人理解)。
cfs_rq的load也遵从了类似的规则,在任务被从队列dequeue之后,其load_avg并不会立即从队列的load_avg中减掉,而是只将任务的load.weight从cfs_rq的load.weight中减掉;在之后的运行过程中,因为cfs_rq.load.weight减小,cfs_rq的load_avg会发生衰减。
当任务被dequeue/enqueue的时候,
(1) 将sched_entity.load.weight从cfs_rq.load.weight减掉或者加上,具体参考
account_enqueue/dequeue_entity()
(2) 对cfs_rq load_avg不会做任何操作;enqueue/dequeue_load_avg()会操作cfs_rq
load_avg,但是这两个函数是在任务cpu迁移的时候被调用的
任务在cpu之间迁移主要有两个场景,
(1) load balance时的cpu迁移,
(2) wakeup的时候选择了另外一个cpu,
相关代码可以参考
set_task_cpu() -> migrate_task_rq_fair()
enqueue_entity() -> update_load_avg()
//The code branch check se.avg.last_update_time and DO_ATTACH flag
为什么需要runnable load ?官方解释是,
'runnable load' was originally introduced to take into account the case
where blocked load biases the load balance decision which was selecting
underutilized groups with huge blocked load whereas other groups were
overloaded.
当任务睡眠被dequeue之后,所在的队列的load会逐渐衰减。但是在这个衰减的过程中,blocked load仍然存在;于是可能出现,一个cpu上大多数任务都处于blocked状态,并不需要多少cpu,而另外一个cpu上,多个处于running状态的任务排队;runnable load avg就是为了避免出现这个场景。
下面我们看下,runnable load如何统计,
runnable weight的计算要分成两种情况,
grq->avg.runnable_load_avg
ge->runnable_weight = ge->load.weight * --------------------------
grq->avg.load_avg
这个计算过程会在以下函数发生,所以,runnabel_weight实时性很强
enqueue_entity() \
dequeue_entity() > update_cfs_group -> calc_group_runnable()
entity_tick() /
cfs中有两个利用率,util和util_est,我们先说第一个,它使用了跟load一样的算法,不过Sum_Factor有两个,
Avg_Factor为1,所以util反映的是sched_entity或者cfs_rq所占用的cpu capacity。
跟load类似,sched_entity的util也不会在enqueue/dequeue之后被从cfs_rq的util中加上或者减掉,而只是在attach/detach的时候;函数可以参考,attach/detach_entity_load_avg()。
前几小结提到的有关enqueue/dequeue和attach/detach的load变更,在不存在cgroup的情况下,非常好理解;但是,如果加入了cgroup层级,最底层task_struct.sched_entity的sched_avg的变更,如何传导到最顶层的rq.cfs_rq ?
首先我们看enqueue/dequeue的场景:
以dequeue场景为例,
rq.cfs_rq
|
tg se
cfs_rq0
/ | \
se0 se1 se2
^^^^
dequeue
dequeue_runnable_load_avg()的影响如何能传导到上层?
答案在update_cfs_group(),
(1) dequeue虽然将se2.load.weight从cfs_rq0.load.weight中减掉了,但
cfs_rq0.avg.load_avg和tg.load_avg并不会发生变化,因为dequeue并
不会操作load,而tg.load_avg需要等待cfs_rq0.avg.load_avg衰减,参
考函数upate_tg_load_avg(),所以,参考函数calc_group_shares(),
se.load.weight并不会立刻变化;
(2) cfs_rq0.load.weight和avg.runnable_load_avg/sum都已经变小,参考
函数calc_group_runnable(),se.runnable_weight也会变小,
到这里,相关更新传导到了se2所在的cfs_rq0的se;
(3) 参考函数reweight_entity()
reweight_entity //函数做了裁剪
---
if (se->on_rq) {
dequeue_runnable_load_avg(cfs_rq, se);
}
se->runnable_weight = runnable;
update_load_set(&se->load, weight);
do {
u32 divider = LOAD_AVG_MAX - 1024 + se->avg.period_contrib;
se->avg.load_avg = div_u64(se_weight(se) * se->avg.load_sum, divider);
se->avg.runnable_load_avg =
div_u64(se_runnable(se) * se->avg.runnable_load_sum, divider);
} while (0);
if (se->on_rq) {
enqueue_runnable_load_avg(cfs_rq, se);
}
---
se的runnable_load_avg在这里根据新的runnable_weight做了新的计算,然后重新累加到
rq.cfs_rq,由此,便传导到了最顶层。
enqueue/dequeue主要发生在任务wait & wakeup的时候,而attach/detach 主要发生在,task在cpu之前迁移的时候,例如:
在attach/detach的时候,load、runnable load和util全都会发生迁移;具体可以参考函数
attch_entity_load_avg()和detach_entity_load_avg()
这两个函数还会通过propagate机制,将相关更新一层一层向上传递。
cfs_rq
|
tg se
cfs_rq0
/ | \
se0 se1 se2 [new]
(1) 当se2被enqueue到cfs_rq0时,其相关load信息也会被更新到cfs_rq0,参考
attach_entity_load_avg(),同时调用add_tg_cfs_propagate()将
se2.avg.load_sum向上传递
(2) update_tg_cfs_runnable()会重新计算se的load信息,并根据se load
信息的delta,向cfs_rq更新
如果仔细观察propagate_entity_load_avg(),
---
cfs_rq = cfs_rq_of(se);
add_tg_cfs_propagate(cfs_rq, gcfs_rq->prop_runnable_sum);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
update_tg_cfs_util(cfs_rq, se, gcfs_rq);
update_tg_cfs_runnable(cfs_rq, se, gcfs_rq);
---
此处直接向上跨级propagate,不需要考虑weight吗?
参考load和runnable load,se的load sum的Sum_Factor都是!!se->on_rq,
并没有引入weight
capacity指的是cpu的能力,参考函数arch_scale_cpu_capacity(),
big.LITTLE – Arm®
Arm big.LITTLE technology is a heterogeneous processing architecture that uses two types of processor. ”LITTLE” processors are designed for maximum power efficiency while ”big” processors are designed to provide maximum compute performance. With two dedicated processors, the big.LITTLE solution is able to adjust to the dynamic usage pattern for smartphones, tablets and other devices. Big.LITTLE adjusts to periods of high-processing intensity, such as those seen in mobile gaming and web browsing, alternate with typically longer periods of low-processing intensity tasks such as texting, e-mail and audio, and quiescent periods during complex apps.
那么是不是非arm架构的cpu,他们的capacity就是一样的呢?
参考函数,scale_rt_capacity(),cpu可以给cfs任务使用的capacity为,
max = arch_scale_cpu_capacity(sd, rq)
higher = rq->avg_rt.util_avg + rq->avg_dl.util_avg
irq = cpu_util_irq(rq)
free = (max - higher) * (max - irq) / max
在runqueue中,保存了两个capacity,
函数check_cpu_capacity(),可用来检查side activity(包括,hardirq, softirq,
rt任务,dl任务等)是否占用了过多的CPU,判断公式为,
((rq->cpu_capacity * sd->imbalance_pct) < (rq->cpu_capacity_orig * 100))
task_h_load()一个任务的avg_load,我们不能简单的使用task_struct.se.avg.load_avg,而需要考虑层级;
cfs的层级关系如下:
root_task_group rq
cfs_rq
|
tg0 se0
cfs_rq0
|
tg1 se1
cfs_rq1
|
task se2
h_load = cfs_rq.avg.load_avg;
task's h_load = h_load * (se0.load_avg/cfs_rq.load_avg)
* (se1.load_avg/cfs_rq0.load_avg)
* (se2.load_avg/cfs_rq1.load_avg)
作为内核中最常用的两种数据结构之一(另外一个是list),红黑树并不是cfs专用的;但是,我们需要明白cfs算法为什么要选用红黑树?
因为红黑树算法稳定,增删查的时间复杂度都是O(log n),这也反映了cfs是一种通用型调度算法。
作为对比,我们可以参考rt调度算法的数据结构,是一个以优先级为索引的链表数组,通过bitmap来标明哪一个优先级有可运行的任务,参考函数pick_next_rt_entity()、__enqueue_rt_entity(), __dequeue_rt_entity,其时间复杂度为O(1);不过rt调度算法在选取任务的时候,并不考虑任务的运行时间,而只考虑优先级,所以,可以采用这种相对高效的数据结构。
而cfs算法,在选取任务的时候,需要考虑任务已经运行的时间,以保证公平性,所以,在enqueue的时候,需要对元素进行排序,所以具有稳定性能的红黑树就成了cfs的选择。
任务什么时候"上树"?什么时候"下树" ?
参考__enqueue_entity/__dequeue_entity的调用链,我们总结出,cfs调度器的红黑树上保存的是处于"就绪态"的任务;之所以打上引号,是因为linux内核中并没有一个叫做就绪态的任务状态,而只有TASK_RUNNING;但实际上,真正运行态的任务,每个CPU只有一个(即rq.curr),它并不在cfs调度器的红黑树上,具体可以参考set_next_task()/put_prev_task()函数。
注:红黑树只维护就绪态任务,这符合调度器的设计;同时,我们假设正在运行的任务如果也放到树上的话,因为是以vruntime为索引,那么每次更新vruntime都需要更新该任务在树中的位置,这操作起来比较费CPU吧。
常规的wait/wakeup场景很好理解,参考enqueue/dequeue_entity()。
基于virtual runtime的sched entity的排序是cfs算法的核心;vruntime决定了sched entity在cfs_timeline红黑树中的位置,也就决定了这个任务什么时候被pick调度。那么vruntime都会在什么时间被更新?
首先是正常运行状态增量,如下
最常见的场景,在update_curr()
curr->vruntime += calc_delta_fair(delta_exec, curr)
根据已经运行的时间和sched_entity的weight计算vruntime的增量
但是当进程睡眠之后被唤醒,如果直接将任务放到红黑树里,其vruntime的值可能远小于其他任务,而"饿死"其他任务;cfs算法做了如下处理,参考函数place_entity(),
se->vruntime = max_vruntime(se->vruntime, cfs_rq->min_vruntime - thresh)
thresh = sysctl_sched_latency
对于一个睡眠较长时间的任务来说,被唤醒之后,会在下一轮中被首先调度,甚至thresh还可以让它抢占正在运行的任务,不过,GENTLE_FAIR_SLEEPS 可以弱化这个效果,参考它的注释:
Only give sleepers 50% of their service deficit. This allows them to run sooner, but does not allow tons of sleepers to rip the spread apart.
对于迁移进程,由于不同的CPU的min_vruntime有差异,所以为了保证公平性,会对任务做normalize,也就是保证任务在cfs_rq中的相对位置,方式如下:
MIGRATION: dequeue (vruntime -= min_vruntime) enqueue (vruntime += min_vruntime)
WAKEUP (remote):
migrate_task_rq_fair() (p->state == TASK_WAKING) (vruntime -= min_vruntime)
enqueue (vruntime += min_vruntime)
对于一个新创建的进程,该赋予多少vruntime呢?参考函数task_fair_fork(),基本上,子任务会继承父任务在红黑树中的相对位置,可以参考下面的代码
task_fork_fair()
---
cfs_rq = task_cfs_rq(current);
curr = cfs_rq->curr;
if (curr) {
update_curr(cfs_rq);
se->vruntime = curr->vruntime;
}
place_entity(cfs_rq, se, 1);
if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
/*
* Upon rescheduling, sched_class::put_prev_task() will place
* 'current' within the tree based on its new key value.
*/
swap(curr->vruntime, se->vruntime);
resched_curr(rq);
}
se->vruntime -= cfs_rq->min_vruntime;
---
cfs调度算法有一个基本保证,即,在一个调度周期内,每个cpu上的所有任务都必须得到一次调度。调度周期的其计算方式为:
static u64 __sched_period(unsigned long nr_running)
{
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
else
return sysctl_sched_latency;
}
sched_nr_latency : 8 by default
sysctl_sched_min_granularity : 0.75 ms by default
sysctl_sched_latency : 6 ms by default
sched_slice()用于计算每个任务在每个调度周期中可以分得的份额,其并非平均分配,而是按照load.weight比例划分,其计算公式是,
slice = slice * se.load.weight / cfs_rq.load.weight
当一个进程超过其sched_slice()时,就会被抢占,然后scheduler会pick下一个进程出来。
check_preempt_tick()
---
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime) {
resched_curr(rq_of(cfs_rq));
...
return;
}
----
需要说明的是,决定一个任务所能分得的CPU时间的并不是这个机制,而是vruntime的算法(参考1.3小节);以上机制确保的是任务的调度延迟,而这,也是公平性的一部分。
之所以需要层级,是因为调度任务组的引入;任务组即需要以一个整体参与调度,又需要调度组内的任务,所以,每个任务组都包含了每CPU的sched_entity,用来以整体的方式参与调度,同时,每CPU的cfs_rq用来维护组内的任务或者子任务组;这里有两点需要说明:
任务组的每CPU的sched_entity/cfs_rq;每个任务组在每个cpu上都有一个sched_entity/cfs_rq,参与每CPU的runquue的调度;这里的cfs_rq的每CPU是比较好理解的,因为runqueue都是每CPU的,任务调度的核心是分享CPU资源;但是任务组的sched_entity也是每CPU的,而普通任务只需一个sched_entity,原因是?任务组每个CPU上的sched_entity代表的并不是整个任务组,而是,该任务组在该CPU上“分组”,参考下图:
task_group A { task_A0, task_A1, task_A2, task_A3 }
CPU0 CPU1
rq0 rq1
cfs_rq0 cfs_rq1
/ \ |
se_0 se_A0 se_A1
task_0 cfs_rq_A0 cfs_rq_A1
/ \ / \
se_A0 se_A1 se_A2 se_A3
task_A0 task_A1 task_A2 task_A3
一个任务组中的任务,是可能分布在多个CPU上的
任务组如何参与调度?
task_group A { task_A0, task_A1 }
CPU0
rq0 (curr = task_1)
cfs_rq0 (curr = se_1)
/ \
se_0 SE_A0
task_0 cfs_rq_A0
/ \
se_A0 se_A1
task_A0 task_A1
task_1.se_1让出CPU,从rq0.cfs_rq0上选择SE_A0,
CPU0
rq0 SE_A0
cfs_rq0 (curr = SE_A0) cfs_rq_A0
/ \ / \
se_0 se_1 se_A0 se_A1
task_0 task_1 task_A0 task_A1
在cfs_rq_A0上选择出se_A1,
CPU0
rq0 (curr = task_A1) SE_A0
cfs_rq0 (curr = SE_A0) cfs_rq_A0 (curr= se_A1)
/ \ |
se_0 se_1 se_A0
task_0 task_1 task_A0
在上图中,se_0(代表任务0)和se_A0(代表任务组A)是平级的,它们会首先根据各自的优先级(任务组的优先级就是cpu.shares)瓜分CPU0的时间;然后,se_A0获得的CPU0的时间再被task_A0和task_A1瓜分。参考下面的试验:
a.out is a while(1).
1 a.out is running under root group,
4 a.out are running under a cgroup with shares 1024
top:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
36006 root 20 0 4176 652 576 R 49.8 0.0 7:34.66 a.out
35675 root 20 0 4176 648 576 R 12.6 0.0 1:56.88 a.out
35677 root 20 0 4176 712 636 R 12.6 0.0 1:56.31 a.out
35679 root 20 0 4176 656 584 R 12.6 0.0 1:56.20 a.out
35676 root 20 0 4176 612 540 R 12.3 0.0 1:56.49 a.out
任务组的sched_entity和cfs_rq在整个runqueue中层级关系如下:
[sched] [parent] [cfs_rq]
global rq NULL
cfs_rq ^ rq.cfs_rq
| | ^
v | |
tg se se se.cfs_rq
cfs_rq ^ se.my_q
| | ^
v | |
tg se se se.cfs_rq
cfs_rq ^ se.my_q
| | ^
v | |
task se se se.cfs_rq
CPU Quota用来设置任务组可以用使用的CPU时间上限;其主要有两个参数,cfs_quota_us和cfs_period_us,语义是,该任务组内的任务,在cfs_period_us这段时间内,使用的CPU时间的上限是cfs_quota_us。在cfs代码中,CPU Quota的实现基于cfs bandwidth control机制。
cfs_period_us指的这段时间,是系统内所有CPU上并行的时间,比如各个CPU都是从01:30 -> 01:31;cfs_quota_us是所有CPU上的任务组使用的CPU时间总和。
如果这个任务组中有两个任务,cpu利用率都是100%
task_group A (task0, task1)
period = 100 quota = 100
CPU0 ----+XX--+XX--+XX--+XX--+XX-->
CPU1 ----+XX--+XX--+XX--+XX--+XX-->
CPU2 ----+----+----+----+----+---->
CPU3 ----+----+----+----+----+---->
task_group A (task0, task1)
period = 100 quota = 200
CPU0 -----+XXXX+XXXX+XXXX+XXXX+XXXX>
CPU1 -----+XXXX+XXXX+XXXX+XXXX+XXXX>
CPU2 ----+----+----+----+----+---->
CPU3 ----+----+----+----+----+---->
如果这个任务组中有四个任务,cpu利用率都是100%
task_group A (task0, task1, task2, task3)
period = 100 quota = 100
CPU0 ----+X---+X---+X---+X---+X--->
CPU1 ----+X---+X---+X---+X---+X--->
CPU2 ----+X---+X---+X---+X---+X--->
CPU3 ----+X---+X---+X---+X---+X--->
task_group A (task0, task1, task2, task3)
period = 100 quota = 200
CPU0 ----+XX--+XX--+XX--+XX--+XX-->
CPU1 ----+XX--+XX--+XX--+XX--+XX-->
CPU2 ----+XX--+XX--+XX--+XX--+XX-->
CPU3 ----+XX--+XX--+XX--+XX--+XX-->
支持cfs bandwidth control或者quota机制的,有3个和时间有关测变量:
cfs_rq的当前sched_entity的执行时间计算方式:
update_curr()
---
u64 now = rq_clock_task(rq_of(cfs_rq));
delta_exec = now - curr->exec_start;
curr->exec_start = now;
---
同时,这个delta_exec会被计入当前cfs_rq,并从当前cfs_rq.runtime_remaining减掉,
参考函数__account_cfs_rq_runtime()。
考虑层级,
CPU0
rq0 (curr = task_1)
cfs_rq0 (curr = se_1)
/ \
se_0 SE_A0
task_0 cfs_rq_A0
/ \
se_A0 SE_B0
task_A0 cfs_rq_B0
/ \
se_B0 se_B1
task_B0 task_B1
task_B0和task_B1被调度时候,SE_B0一直在cfs_rq_A0上调度,SE_B0的执行时间都会
计入cfs_rq_A0
当cfs_rq.runtime_remaining无法从task_group.cfs_bandwidth.runtime获得CPU时间,即quota耗尽,当前cfs_rq就会被节流,上面的sched_entity无法被调度,如何办到?
CPU0
rq0 (curr = task_1)
cfs_rq0 (curr = se_1)
/ \
se_0 SE_A0
task_0 cfs_rq_A0
/ \
se_A0 SE_B0
task_A0 cfs_rq_B0
/ \
se_B0 se_B1
task_B0 task_B1
当task_group B的quota用尽之后, SE_B0会被从cfs_rq_A0上dequeue下来,并保存在
task_group.cfs_bandwith.throttled_cfs_rq上,如下:
CPU0
rq0 (curr = task_1) cfs_b_B.throttled_cfs_rq
cfs_rq0 (curr = se_1) |
/ \ cfs_rq_B0
se_0 SE_A0 / \
task_0 cfs_rq_A0 se_B0 se_B1
/ task_B0 task_B1
se_A0
task_A0
在cfs_rq从task_group.cfs_bandwidth中申请runtime之后,就会启动period定时器;当定时器超时之后,会给当前task_group重新赋予quota,参考函数__refill_cfs_bandwidht_runtime();然后,调用distribute_cfs_runtime(),将时间分配给被throttle的cfs_rq,并把它对应的sched_entity挂回它的父cfs_rq;这里我们重点关注下,在distribute_cfs_runtime()的时候,该cfs_rq分配多少CPU时间呢?
runtime = -cfs_rq->runtime_remaining + 1;
负载均衡的本质是给任务选择一个合适的CPU运行,以最大限度的利用现代CPU的多核并行架构,提高任务的性能和系统的整体吞吐;其主要分为两大部分:
负载均衡的过程,其实是从别的CPU往本CPU拉任务以达到均衡的过程;对此,我们需要解答两个问题,
选择busiest group的时候,需要收集各个sched group的统计信息,这个过程是由函数update_sg_lb_stats()完成的,该函数会遍历sched_group.cpumask中的CPU以获得runqueue指针,这些信息包括,
group_load | 通过weighted_cpuload(),获得的是rq.cfs.avg.runnable_load_avg |
group_util | 通过cpu_util(),获得的是rq.cfs.avg.util_avg和util_est.enqueued中的最大值 |
sum_nr_running | 通过rq.cfs.h_nr_running |
group_weight | 注意,此处的weight并不是load.weight,而是sched_group.group_weight,代表的是该sg中cpu的数量; sum_nr_running < group_weight代表的是,该sg中每个cpu上运行的平局任务数量少于1个,此时,任务该sg仍有余力 |
group_capacity | 通过sched_group.sgc.capacity |
group_no_capacity | 通过group_is_overloaded(),计算公式为: group_capacity < group_util * (sd->imbalance_pct / 100) |
group_type | 通过group_classify(),group_no_capacity为group_overloaded,sg_imbalance()则为group_imbalanced,其他为group_other, overloaded > imbalanced > other 注: 关于sg_imbalanced(),它专指的是因cpu affinity设置导致的imbalance,参考load_balance()和can_migrate_task()中,涉及LBF_SOME_PINNED的代码。 |
avg_load | group_load * SCHED_CAPACITY_SCALE / group_capacity side activiy越多,group_load被放大的越厉害;find_busiest_group()还会计算一个sd的平均avg_load,计算方法类似 |
选择最忙的sched_group参考函数update_sd_pick_busiest(),不考虑异构的话,主要依据为:
在选择出busiest sched_group之后,还需要判断是不是存在imbalance,并且计算出imblance的load的值。判断方式参考函数find_busiest_group(),主要分成两种情况,
在判断确实存在imbalance之后,我们需要计算一个imbalance的load的值,这个值将决定拉几个任务过来,具体计算方法请参考calculate_imbalance,我们这里只看下最简单的情况,
^
|_ busiest sg avg_load \
| > gap0
|_ sd avg_load /
| \
| > gap1
|_ local sg avg_load /
imbalance = min(gap0, gap1)
参考函数find_busiest_queue(),选取busiest queue时的判断标准是,
weighted_cpuload busiest_wl
----------------- > -------------
cpu_capacity busiest_cap
选择好之后,将从这个run queue上拉取task,并将拉取的任务的task_h_load(),从imbalance中交掉,直到imbalance小于0为止;对于task_h_load() / 2 > imbalance的任务将被略过,具体参考detach_tasks()。
Wakeup的时候,需要选择一个合适的CPU来唤醒任务,需要考虑的因素有:
对于存在热的Cache的CPU有两个选择:
核心代码在select_idle_sibling(),参考代码:
select_idle_sibling()
---
sd = rcu_dereference(per_cpu(sd_llc, target));
if (!sd)
return target;
i = select_idle_core(p, sd, target);
if ((unsigned)i < nr_cpumask_bits)
return i;
i = select_idle_cpu(p, sd, target);
if ((unsigned)i < nr_cpumask_bits)
return i;
i = select_idle_smt(p, sd, target);
if ((unsigned)i < nr_cpumask_bits)
return i;
---
其策略如下:
理论上两种方式都可以实现给cgroup限流的结果,但是哪种方式更好呢?
下面理论上的推算,调度对性能的影响,主要包括两方面:
这里参考下memcahced和kbuild两个吞吐型的测试,
配置 | cfs quota | cpuset | |
16线程 | cpuset 跨越16个物理核 |
17m40s | 12m5s |
cpuset 跨越8个物理核 |
17m40s | 17m45s |
从测试结果来看,影响性能的关键似乎是超线程问题。
再看下调度延迟测试:
测试命令为: ./schbench -m 8 -t 4 -r 900 -c 80000 -R 10
配置 | schbench执行结果 |
cpuset 跨越8个物理核 |
50.0th: 9 (9159 samples) 75.0th: 10 (2132 samples) 90.0th: 12 (2015 samples) 95.0th: 13 (316 samples) *99.0th: 4052 (302 samples) 99.5th: 12048 (70 samples) 99.9th: 12816 (57 samples) min=3, max=17924 |
cfs quota 8核 | 50.0th: 10 (9282 samples) 75.0th: 11 (1824 samples) 90.0th: 12 (1130 samples) 95.0th: 13 (701 samples) *99.0th: 16 (546 samples) 99.5th: 18 (41 samples) 99.9th: 27 (48 samples) min=5, max=140 |
这个测试结果是符合预期的
从有限的两组测试来看,cpuset对比cfs quota似乎没有体现出明显的优势。