调度器41—CFS组调度

基于Linux-5.10.110。

缩写

说明

tse

task sched_entity

gse

group sched_entity

grq

group sched_entitymy_q成员,也即group sched_entitycfs_rq

ts

task

 
 

一、CFS组调度简介

1.1. 存在的原因

总结来说是希望不同分组的任务在高负载下能分配可控比例的CPU资源。为什么会有这个需求呢,比如多用户计算机系统每个用户的所有任务划分到一个分组中,A用户90个相同任务,而B用户只有10个相同任务,在CPU完全跑满的情况下,那么A用户将占90%CPU时间,而B用户只占到了10%CPU时间,这对B用户显然是不公平的。再或者同一个用户,既想-j64快速编译,又不想被编译任务影响使用体验,也可将编译任务设置到对应分组中,限制其CPU资源。

1.3. 手机设备上的分组状态

/dev/cpuctl # ls -l
drwxr-xr-x background
drwxr-xr-x foreground
drwxr-xr-x top-app
...
-rw-r--r--  cpu.shares
-rw-rw-rw- tasks
-rw-rw-rw- cgroup.procs

/dev/cpuctl 目录使用struct task_group root_task_group 表示。其下的每一层级的子目录都抽象为一个task_group结构。有几个需要注意的点:

(1) 根组下的cpu.shares 文件默认值是1024而且不支持设置。根组的负载也不会更新。

(2) 根组也是一个task group,根组下的任务也是被分过组的,不会再属于其它组。

(3) 内核线程默认在根组下,其直接从根cfs_rq上分配时间片,有非常大的优势,若是其一直跑,那在trace上看就几乎就是”一直跑”,比如常见的kswapd内核线程。

(4) 默认cpu.shares配置下,若全是nice=0的任务,且只考虑单核的情况下,根组下的每个任务在完全满载情况下能得到的时间片等于其它分组下所有任务能分配到的时间片的总和。

注意:tasktask group都是通过权重来分配时间片的,但是task的权重来自其优先级,而task group的权重则来自与其cgroup目录下cpu.shares文件设置的值。使能组调度后,看任务分得的时间片,就不能单看其prio对应的权重了,还要看其task group分得的权重和本group中其它任务的运行情况。

 

二、任务的task group分组

CFS组调度功能主要是通过对任务进行分组体现出来的,一个分组由一个struct task_group来表示。

2.1. 如何设置分组

task group分组配置接口由cpu cgroup子系统通过cgroup目录层次结构导出到用户空间。

/dev/cpuctl # mkdir tg1 //创建分组
/dev/cpuctl # echo <pid> > tg1/tasks //向分组中拉一个任务
/dev/cpuctl # echo <pid> > tg1/cgroup.procs //向分组中拉pid所在进程的所有任务
/dev/cpuctl # rmdir tg1 //删除分组

如何从task group中移除一个任务呢,没有办法直接移除的,在cgroup语义下,一个任务某一时刻必须属于一个task group,只有通过将其echo到其它分组中才能将其从当前分组中移除。

2.2. Android中如何设置分组

Process.java中向其它模块提供 setProcessGroup(int pid, int group) pid进程设置进group参数指定的分组中,供其它模块进行调用设置。比如OomAdjuster.java 中将任务切前/后台分别调用传参group=THREAD_GROUP_TOP_APP/THREAD_GROUP_BACKGROUND

libprocessgroup 中提供了一个名为 task_profiles.json 的配置文件,它里面 AggregateProfiles 聚合属性字段配置了上层设置下来后的对应的行为。比如 THREAD_GROUP_TOP_APP 对应的聚合属性为 SCHED_SP_TOP_APP,其中的MaxPerformance属性对应的行为就是加入到cpu top-app分组。

    "AggregateProfiles": [
    ...
    {
      "Name": "SCHED_SP_TOP_APP",
      "Profiles": [ "MaxPerformance", "MaxIoPriority", "TimerSlackNormal" ]
    },
    ...
    ]

    {
      "Name": "MaxPerformance",
      "Actions": [
        {
          "Name": "JoinCgroup",
          "Params":
          {
            "Controller": "cpu",
            "Path": "top-app"
          }
        }
      ]
    },

”MaxPerformance”属性的配置可读性非常强,可以看出是加入到cpu子系统的top-app分组中。

2.3. Android中设置为TOP-APP分组,对cgroup设置了什么

因为有多个cgroup子系统,除了我们正在讲的CFS组调度依附的cpu cgroup子系统外,还有cpuset cgroup子系统(限制任务可运行的CPU和可使用的内存节点)blkio cgroup子系统(限制进程的块设备io)freezer cgroup子系统(提供进程冻结功能)等。上层配置分组下来可能不只切一个cgroup,具体切了哪些子系统体现在集合属性 AggregateProfiles 的数组成员上,比如上例中另外两个属性对应的行为分别是加入blkio子系统的根组和将任务的timer_slack_ns(一个平衡hrtimer定时唤醒及时性与功耗的参数)设置为50000ns

    {
      "Name": "MaxIoPriority",
      "Actions": [
        {
          "Name": "JoinCgroup",
          "Params":
          {
            "Controller": "blkio",
            "Path": "" //根组
          }
        }
      ]
    },
    {
      "Name": "TimerSlackNormal",
      "Actions": [
        {
          "Name": "SetTimerSlack",
          "Params":
          {
            "Slack": "50000" //单位ns
          }
        }
      ]
    },

2.4. 服务启动后就放在指定分组

在启动服务的时候使用 task_profiles <task_profiles.json属性> 进行配置,举例。

service cameraserver /system/bin/cameraserver
    class main
    user cameraserver
...
    task_profiles CameraServiceCapacity MaxPerformance //MaxPerformance表示加入到cpu的top-app分组。

 

三、内核实现概述

上面我们讨论了对task group分组的配置,本节开始将进入到内核中,了解其实现。

3.1. 相关功能依赖关系

内核相关CONFIG_*依赖如下:

1:

CGROUP提供cgroup目录层次结构的功能;CGROUP_SCHED提供cpu cgroup目录层次结构(如 /dev/cpuctl/top-app),并为每个cpu cgroup目录提供task group的概念;FAIR_GROUP_SCHED基于cpu cgroup提供的task group提供CFS任务组调度功能。图1中灰色的虚线框图是Android手机内核中默认不使能的。

3.2. task group数据结构框图

如下图2所示,展示了一个task group在内核中维护的主要数据结构的框图,对照着图下面概念更容易理解:

(1) 由于不能确定一个分组内的任务跑在哪个CPU上,因此一个task group在每个CPU上都维护了一个group cfs_rq,由于task group也要参与调度(要先选中task group才能选中其group cfs_rq上的任务) ,因此在每个CPU上也都维护了一个group se

(2) task semy_q成员为NULL,而group semy_q成员指向其对应的group cfs_rq,其分组在本CPU上的就绪任务就挂在这个group cfs_rq上。

(3) task group可以嵌套,由 parent/siblings/children 成员构成一个倒立树状层次结构,根task groupparent指向NULL

(4) 所有CPU的根cfs_rq属于root task group

2:

注:画图比较麻烦,此图是借鉴蜗窝的。

 

四、相关结构体

4.1. struct task_group

一个 struct task_group 就表示一个cpu cgroup分组。在使能FAIR_GROUP_SCHED的情况下,一个 struct task_group 就表示CFS组调度中的一个任务组。

struct task_group {
    struct cgroup_subsys_state css;
#ifdef CONFIG_FAIR_GROUP_SCHED
    struct sched_entity    **se;
    struct cfs_rq        **cfs_rq;
    unsigned long        shares;
    atomic_long_t        load_avg;
#endif
    struct task_group    *parent;
    struct list_head    siblings;
    struct list_head    children;
    ...
};

css: task group对应的cgroup状态信息,通过它依附到cgroup目录层次结构中。

se:  group se,是个数组指针,数组大小为CPU的个数。因为一个task group中有多个任务,且可以跑在所有CPU上,因此需要每个CPU上都要有本task group的一个 se

cfs_rq:  group secfs_rq, 是本task group的在各个CPU上的cfs_rq,也是个数组指针,数组大小为CPU的个数。当本task group的任务就绪后就挂在这个cfs_rq上。它在各个CPU上对应的指针和task group在各个CPU上的se->my_q具有相同指向,见 init_tg_cfs_entry().

shares:  task group的权重,默认为scale_up(1024)。和task se的权重类似,值越大task group能获取的CPU时间片就越多。但是和task 不同的是,task group 在每个CPU上都有一个group se,因此需要按照一定规则分配给各CPU上的group se, 下面会讲解分配规则。

load_avg: task group的负载,而且只是一个load_avg 变量(不像secfs_rq是一个结构),下面会对其进行讲解。注意它不是per-cpu的,此task group的任务在各个CPU上进行更新时都会更新它,因此需要注意对性能的影响。

parent/siblings/children: 构成task group的层次结构。

内核中有个全局变量 struct task_group root_task_group, 表示根组。其cfs_rq[]就是各个cpucfs_rq, se[]都为NULL。其权重不允许被设置,见 sched_group_set_shares()。负载也不会被更新,见 update_tg_load_avg()

系统中的所有task group 结构都会被添加到task_groups链表上,在CFS带宽控制时使用。

初学者可能会混淆组调度和调度组这两个概念,以及 struct task_group struct sched_group 的区别。组调度对应struct task_group,用来描述一组任务,主要用于util uclamp(对一组任务的算力需求进行钳制)CPU资源使用限制,也是本文中要讲解的内容。调度组对应 struct sched_group,是CPU拓扑结构sched_domain中的概念,用来描述一个CPU(MC层级)/Cluster(DIE层级)的属性,主要用于选核和负载均衡。

4.2. struct sched_entity

一个sched_entity 既可以表示一个task se, 又可以表示一个group se。下面主要介绍使能组调度后新增的一些成员。

struct sched_entity {
struct load_weight        load;
    ...
#ifdef CONFIG_FAIR_GROUP_SCHED
    int                depth;
    struct sched_entity        *parent;
    struct cfs_rq            *cfs_rq;
    struct cfs_rq            *my_q;
    unsigned long            runnable_weight;
#endif
    struct sched_avg        avg;
    ...
};

load: 表示se的权重,对于gse, 新建时初始化为NICE_0_LOAD, init_tg_cfs_entry()

depth:  表示task group的嵌套深度,根组下的se的深度为0,每嵌套深一层就加1。比如/dev/cpuctl目录下有个tg1目录,tg1目录下又有一个tg2目录,tg1对应的group se的深度为0tg1下的task se的深度是1tg2下的task se的深度是2。更新位置见 init_tg_cfs_entry()/attach_entity_cfs_rq()

parent: 指向父se节点, 父子se节点都是对应同一cpu的。根组下任务的指向为NULL

cfs_rq: se挂载到的cfs_rq。对于根组下的任务指向rqcfs_rq,非根组的任务指向其parent->my_rq,见init_tg_cfs_entry()

my_q: secfs_rq, 只有group se才有cfs_rqtask se的为NULLentity_is_task()宏通过这个成员来判断是task se还是group se

runnable_weight: 缓存 gse->my_q->h_nr_running 的值,在计算gserunnable负载时使用。

avg: se的负载,对于tse会初始化为其权重(创建时假设其负载很高),而gse则会初始化为0,见init_entity_runnable_average()task se的和group se的有一定区别,下面第五章会进行讲解。

4.3. struct cfs_rq

struct cfs_rq 既可以表示per-cpuCFS就绪队列,又可以用来表示gsemy_q队列。下面列出对组调度比较关键的一些成员进行讲解。

struct cfs_rq {
    struct load_weight    load;
    unsigned int        nr_running;
    unsigned int        h_nr_running;
    struct sched_avg    avg;
    struct {
        int        nr;
        unsigned long    load_avg;
        unsigned long    util_avg;
        unsigned long    runnable_avg;
    } removed;

#ifdef CONFIG_FAIR_GROUP_SCHED
    unsigned long        tg_load_avg_contrib;
    long            propagate;
    long            prop_runnable_sum;
    unsigned long    h_load;
u64            last_h_load_update;
    struct sched_entity    *h_load_next;
    struct rq        *rq;
    int            on_list;
    struct list_head    leaf_cfs_rq_list;
    struct task_group    *tg;
#endif /* CONFIG_FAIR_GROUP_SCHED */
};

load: 表示cfs_rq的权重,无论是根cfs_rq还是grq,这里的权重都等于其队列上挂的所有任务的权重之和。

nr_running: 当前层级下 task se group se的个数和。

h_nr_running: 当前层级以及所有子层级下task se的个数和,不包括group se

avg: cfs_rq的负载。下面将对比task segroup secfs_rq讲解负载。

removed: 当一个任务退出或者唤醒后迁移到到其他cpu上的时候,原来CPUcfs rq上需要移除该任务带来的负载。这个移除动作会先把移除的负载记录在这个removed成员中,在下次调用update_cfs_rq_load_avg()更新cfs_rq负载时再移除。nr表示要移除的se的个数,*_avg则表示要移除的各类负载之和。

tg_load_avg_contrib: 是对 grq->avg.load_avg 的一个缓存,表示当前grqload负载对tg的贡献值。用于在更新 tg->load_avg 的同时降低对 tg->load_avg 的访问次数。在计算gsetg分得的权重配额时的近似算法中也有用到,见 calc_group_shares()/update_tg_load_avg()

propagate: 标记是否有负载需要向上层传播。下面7.3节会进行讲解

prop_runnable_sum: 在负载沿着task group层级结构向上层传播的时候,表示要上传的tse/gseload_sum值。

h_load: 层次负载hierarchy load,表示本层cfs_rqload_avgCPUload_avg的贡献值,主要在负载均衡路径中使用。下面会对其进行讲解。

last_h_load_update: 表示上一次更新h_load的时间点(单位jiffies)

h_load_next: 指向子gse为了获取任务的hierarchy loadtask_h_load函数),需要从顶层cfs向下,依次更新各个levelcfs rqh_load。因此,这里的h_load_next就是为了形成一个从顶层cfs rq到底层cfs rqcfs rq--se--cfs rq--se的关系链。

rq: 使能组调度后才加的这个成员,若没有使能组调度的话,cfs_rq就是rq的一个成员,使用 container_of进行路由,使能组调度后,增加了一个rq成员进行cfs_rqrq的路由。

on_list/leaf_cfs_rq_list: 尝试将叶cfs_rq串联起来,在CFS负载均衡和带宽控制相关逻辑中使用。

tg: cfs_rq隶属的task group

 

五、task group权重

task group的权重使用struct task_group shares 成员来表示,默认值是scale_load(1024)。可以通过cgroup目录下的cpu.shares文件进行读写,echo weight > cpu.shares 就是将task group权重配置为weight,保存到shares 成员变量中的值是scale_load(weight)root_task_group不支持设置权重。

不同task group的权重大小表示系统CPU跑满后,哪个task group组可以跑多一些,哪个task group组要跑的少一些。

5.1. gse的权重

task group在每个CPU上都有一个group se,那么就需要将task group的权重 tg->shares 按照一定的规则分配到各个gse上。规则就是公式(1)

 *                     tg->weight * grq->load.weight
 *   ge->load.weight =   -----------------------------------------              (1)
 *                       \Sum grq->load.weight

其中 tg->weight 就是tg->shares, grq->load.weight 表示tg在各个CPU上的grq的权重。也就是每个gse根据其cfs_rq的权重比例来分配tg的权重。cfs_rq的权重等于其上挂载的任务的权重之和。假设tg的权重是1024,系统中只有2CPU,因此有两个gse, 若其grq上任务状态如下如下图3,则gse[0]分得的权重为 1024 * (1024+2048+3072)/(1024+2048+3072+1024+1024) = 768gse[1]分得的权重为 1024 * (1024+1024)/(1024+2048+3072+1024+1024) = 256

3:

gse的权重更新函数为 update_cfs_group(),下面看其具体实现:

 

static void update_cfs_group(struct sched_entity *se) //fair.c
{
    struct cfs_rq *gcfs_rq = group_cfs_rq(se); //return grp->my_q;
    long shares;

    /* 只对group se 进行更新 */
    if (!gcfs_rq)
        return;

    /*
     * 计算该gse的weight, 就是从 gcfs_rq->tg->shares
     * 中分配了多少权重到这个gse上
     */
    shares = calc_group_shares(gcfs_rq);

    /* 将分配到的weight更新到gse和grq的load.weight上 */
    reweight_entity(cfs_rq_of(se), se, shares);
}

tg的权重向gse[X]的分配动作是在 calc_group_shares() 中完成的。

公式(1)中使用到 \Sum grq->load.weight,也就是说一个gse权重的更新需要访问各个CPU上的grq,锁竞争代价比较高,因此进行了一系列的近似计算。首先进行替换:

*   grq->load.weight --> grq->avg.load_avg                         (2)

然后得到:

 *                     tg->weight * grq->avg.load_avg

 *   ge->load.weight =    ----------------------------------------             (3)

 *                             tg->load_avg

 *

 * Where: tg->load_avg ~= \Sum grq->avg.load_avg

由于cfs_rq->avg.load_avg = cfs_rq->avg.load_sum/divider。而 cfs_rq->avg.load_sum 等于 cfs_rq->load.weight 乘以非idle状态下的几何级数。 这个近似是在tg的每个CPU上的grq的非idle状态的时间级数是相同的前提下才严格相等的。也就是说tg的任务在各个CPU上的运行状态越一致,越接近这个近似值。

task group 空闲的情况下,启动一个任务。grq->avg.load_avg 需要时间来建立,在建立时间这种特殊情况下公式1简化为:

 *                     tg->weight * grq->load.weight

 *   ge->load.weight =    ---------------------------------------   =   tg->weight   (4)

 *                         grp->load.weight

相当于一个单核系统下的状态了。为了让公式(3)在这种特殊情况下更贴近与公式(4),又做了一次近似,得到:

 *                             tg->weight * grq->load.weight

 *   ge->load.weight =     --------------------------------------------------------------------         (5)

 *                      tg->load_avg - grq->avg.load_avg + grq->load.weight

但是因为grq上没有任务时,grq->load.weight 可以下降到 0,导致除以零,需要使用 grq->avg.load_avg作为它的下限,然后给出:

 *                     tg->weight * grq->load.weight

 *   ge->load.weight =    ------------------------------------------    (6)

 *                             tg_load_avg'

 *

 * 其中:

 *   tg_load_avg' = tg->load_avg - grq->avg.load_avg + max(grq->load.weight, grq->avg.load_avg)

max(grq->load.weight, grq->avg.load_avg) 一般都是取grq->load.weight,因为只有grq上一直有任务running+runnable才会趋近于grq->load.weight

calc_group_shares() 函数是通过公式(6)近似计算各个gse分得的权重:

static long calc_group_shares(struct cfs_rq *cfs_rq)
{
    long tg_weight, tg_shares, load, shares;
    struct task_group *tg = cfs_rq->tg;

    tg_shares = READ_ONCE(tg->shares);

    /* 计算公式(6)的分母 tg_weight */
    load = max(scale_load_down(cfs_rq->load.weight), cfs_rq->avg.load_avg);

    tg_weight = atomic_long_read(&tg->load_avg);

    /* Ensure tg_weight >= load */
    tg_weight -= cfs_rq->tg_load_avg_contrib; //它是 grq->avg.load_avg 的缓存
    tg_weight += load;

    /* 计算公式(6)的分子 */
    shares = (tg_shares * load);
    /* 计算公式(6)中的除法 */
    if (tg_weight)
        shares /= tg_weight;

    return clamp_t(long, shares, MIN_SHARES, tg_shares);
}

由于tg中的每个任务都对gse的权重有贡献,因此grq上任务个数变更时都要更新gse的权重,近似过程中使用到了se的负载,在entity_tick()中也进行了一次更新。调用路径:

 

enqueue_entity
dequeue_entity
enqueue_task_fair
dequeue_task_fair
entity_tick
sched_group_set_shares
    update_cfs_group
        calc_group_shares

5.2. gse上每个tse分到的权重

任务组中的任务也是按其权重比例分配gse的权重。如上图2gse[0]grq上挂的3个任务,tse1分得的权重就是768*1024/(1024+2048+3072)=128, tse2分得的权重就是768*2048/(1024+2048+3072)=256, tse3分得的权重就是768*3072/(1024+2048+3072)=384

tg中的任务分配tg分得的时间片的时候,会使用到这个按比例分得的权重。分组嵌套的越深,能按比例分得的权重就越小,由此可见,在高负载时task group中的任务是不利于分配时间片的。

 

六、task group时间片

6.1. 时间片分配

若使能CFS组调度会从上到下逐层通过权重比例来分配上层分得的时间片,分配函数是sched_slice()。但是从上到下不便于遍历,因此改为从下到上进行遍历,毕竟 A*B*C C*B*A 是相等的。

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    /* nr_running:当前层级下tse和gse个数之和 */
    unsigned int nr_running = cfs_rq->nr_running;
    u64 slice;

    if (sched_feat(ALT_PERIOD)) //默认为真
        /* 选择的是根 cfs_rq 的 h_nr_running 的值,也就是所有层级tse的个数和 */
        nr_running = rq_of(cfs_rq)->cfs.h_nr_running; //cfs_rq->rq->cfs.h_nr_running

    /*
     * se->on_rq: 正在running的任务为真,新fork的任务为0。
     * 超过8个就按任务数量乘以 sysctl_sched_min_granularity(3ms,默认0.75ms) 计算出的物理时间
     */
    slice = __sched_period(nr_running + !se->on_rq);

    /* down-top 遍历嵌套的tg */
    for_each_sched_entity(se) {
        struct load_weight *load;
        struct load_weight lw;

        cfs_rq = cfs_rq_of(se);
        load = &cfs_rq->load;

        /* 对于还没挂在cfs_rq上的se再加上其权重 */
        if (unlikely(!se->on_rq)) {
            lw = cfs_rq->load;
            update_load_add(&lw, se->load.weight);
            load = &lw; //cfs_rq->load + se->load
        }
        /*
         * 按权重比例逐层级计算分配到的物理时间,C*B*A 等于 A*B*C
         * slice = slice * (se->load.weight / se->cfs_rq->load.weight)
         */
        slice = __calc_delta(slice, se->load.weight, load);
    }

    if (sched_feat(BASE_SLICE)) //默认为真
        /* 保证分配的最小物理时间是0.75ms */
        slice = max(slice, (u64)sysctl_sched_min_granularity);

    return slice;
}

sched_slice的主要路径如下:

 

scheduler_tick //tick中断,传参 (rq, curr, 0)
    task_tick
        task_tick_fair //for_each_sched_entity 由下到上遍历每一个se
            entity_tick //本层级的就绪任务不只1个就调用
                check_preempt_tick //获取该的运行时间以便判断是否运行超时了
                    sched_slice //fair.c

tick中断中,若发现se此次运行时间已经超过了其分得的时间片,就触发抢占,以便让其让出CPU

 

如下图4,假设tg嵌套2层,且在当前CPU上各层gsetg那里分得的权重都是1024,且假设直接通过任务个数来计算周期,5tseperiod 就是 3 * 5 = 15ms那么:

tse1 获得 1024/(1024+1024) * 15 = 7.5ms;

tse2 获得 [1024/(1024+1024+1024)] * {[1024/(1024+1024)] * 15 }= 2.5ms

tse4 获得 [1024/(1024+1024)] * {[1024/(1024+1024+1024)] * [1024/(1024+1024)] * 15} = 1.25ms

4:

注:tg1tg2的权重通过 cpu.shares 文件进行配置,然后各个cpu上的gsecpu.shares 配置的权重中按其上的grq的权重比例分配权重。gse的权重不再和nice值挂钩。

6.2. 运行时间传导

pick_next_task_fair() 会优先pick虚拟时间最小的segse的虚拟时间是怎么更新的呢。虚拟时间是在 update_curr()中进行更新,然后通过 for_each_sched_entity 向上逐层遍历更新gse的虚拟时间。若tse运行5ms,则其父级各gse都运行5ms,然后各层级根据自己的权重更新虚拟时间。

static void update_curr(struct cfs_rq *cfs_rq)
{
    /* 只要tse是curr,各层级的父gse都会被其grq->curr指向 */
    struct sched_entity *curr = cfs_rq->curr;
/* 使用的是没有经过scale的clock_task这个timeline */
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    /* 这次运行的物理时间 */
    delta_exec = now - curr->exec_start;
    if (unlikely((s64)delta_exec <= 0))
        return;

    /* 根据curr的权重更新虚拟时间 */
    curr->vruntime += calc_delta_fair(delta_exec, curr);
}

主要调用路径:

__schedule
    deactivate_task
        dequeue_task
            dequeue_task_fair //for_each_sched_entity
                dequeue_entity //阻塞而停止运行
                    update_curr(cfs_rq)
    pick_next_task
        put_prev_task
            put_prev_task_fair //for_each_sched_entity
                put_prev_entity //被抢占而停止运行
                    update_curr(cfs_rq); 

scheduler_tick
    task_tick
        task_tick_fair //for_each_sched_entity
            entity_tick //tick命中
                update_curr(cfs_rq);

在选择下一个任务出来运行时逐层级选择虚拟时间最小的se,若选到gse就从其grq上继续选,直到选到tse

 

七、task groupPELT负载

7.1. 计算负载使用的timeline

计算负载使用的timeline和计算虚拟时间使用的timeline不同。计算虚拟时间时使用的timelinerq->clock_task, 这个是运行多长时间就是多长时间。而计算负载使用的timelinerq->clock_pelt,它是根据CPU的算力和当前频点scale后的,在CPUidle是会同步到rq->clock_task上。因此PELT计算出来的负载可以直接使用,而不用像WALT计算出来的负载那样还需要scale。更新rq->clock_pelt这个timeline的函数是 update_rq_clock_pelt()

static inline void update_rq_clock_pelt(struct rq *rq, s64 delta) //pelt.h
{
    if (unlikely(is_idle_task(rq->curr))) {
        /* The rq is idle, we can sync to clock_task */
        rq->clock_pelt  = rq_clock_task(rq);
        return;
    }

    /* delta * capacity_cpu / capacity_max(1024) */
    delta = cap_scale(delta, arch_scale_cpu_capacity(cpu_of(rq)));
    /* delta * cur_freq / max_freq */
    delta = cap_scale(delta, arch_scale_freq_capacity(cpu_of(rq)));

    rq->clock_pelt += delta;
}

最终计算的 delta= delta * (capacity_cpu / capacity_max(1024)) * (cur_cpu_freq / max_cpu_freq) 也就是将当前cpu在当前频点上运行得到的delta时间值,缩放到最大性能CPU的最大频点上对应的delta时间值。然后累加到 clock_pelt 上。比如在小核上1GHz下跑了5ms,可能只等效于在超大核上运行1ms,因此在不同ClusterCPU核上跑相同的时间,负载增加量是不一样的。

7.2. 负载定义与计算

load_avg 定义为:load_avg = runnable% * scale_load_down(load)

runnable_avg 定义为:runnable_avg = runnable% * SCHED_CAPACITY_SCALE

util_avg 定义为:util_avg = running% * SCHED_CAPACITY_SCALE

这些负载值保存在struct sched_avg结构中,此结构内嵌到secfs_rq结构中。此外,struct sched_avg中还引入了load_sumrunnable_sumutil_sum成员来辅助计算。 不同实体(tse/gse/grq/cfs_rq)的负载只是其runnable% 多么想运行,和 running% 运行了多少的表现形式不同。这两个因数只对tse取值是[0,1]的,对其它实体则超出了这个范围。

 

7.2. 1. tse负载

下面看一下tse负载计算公式,为了加深印象,举一个跑死循环的例子。计算函数见 update_load_avg --> __update_load_avg_se().

load_avg: 等于 weight * load_sum / divider, 其中 weight = sched_prio_to_weight[prio-100]。由于 load_sum 是任务 running+runnable 状态的几何级数,divider 近似为几何级数最大值,因此一个死循环任务的 load_avg 接近于其权重。

runnable_avg: 等于 runnable_sum / divider。由于 runnable_sum 是任务 running+runnable 状态的几何级数然后scale up后的值,divider 近似为几何级数最大值,因此一个死循环任务的 runnable_avg 接近于 SCHED_CAPACITY_SCALE

util_avg: 等于 util_sum / divider。由于 util_sum 是任务 running 状态的几何级数然后scale up后的值,divider 近似为几何级数最大值,因此一个死循环任务的 util_avg 接近于 SCHED_CAPACITY_SCALE

load_sum: 是对任务是单纯的 running+runnable 状态的几何级数累加值。对于一个死循环,此值趋近于 LOAD_AVG_MAX

runnable_sum: 是对任务 running+runnable 状态的几何级数累加值然后scale up后的值。对于一个死循环,此值趋近于 LOAD_AVG_MAX * SCHED_CAPACITY_SCALE

util_sum: 是对任务 running 状态的几何级数累加值然后scale up后的值。对于一个独占某个核的死循环,此值趋近于 LOAD_AVG_MAX * SCHED_CAPACITY_SCALE,若不能独占,会比此值小。

 

7.2.2. cfs_rq的负载

下面看一下cfs_rq负载计算公式,为了加深印象,举一个跑死循环的例子。计算函数见 update_load_avg --> update_cfs_rq_load_avg --> __update_load_avg_cfs_rq()

load_avg: 直接等于 load_sum / dividercfs_rq 跑满(跑一个死循环或多个死循环),趋近于cfs_rq的权重,cfs_rq的权重也就是其上挂的所有调度实体的权重之和,即Sum(sched_prio_to_weight[prio-100])

runnable_avg: 等于 runnable_sum / dividercfs_rq 跑满(跑一个死循环或多个死循环),趋近于cfs_rq上任务个数乘以 SCHED_CAPACITY_SCALE

util_avg: 等于 util_sum / dividercfs_rq 跑满(跑一个死循环或多个死循环),趋近于 SCHED_CAPACITY_SCALE

load_sum: cfs_rq weight,也就是本层级下所有se的权重之和乘以非idle状态下的几何级数。注意是本层级,下面讲解层次负载h_load时有用到。

runnable_sum: cfs_rq上所有层级的runnable+running 状态任务个数和乘以非idle状态下的几何级数,然后再乘以 SCHED_CAPACITY_SCALE 后的值。见 __update_load_avg_cfs_rq().

util_sum: cfs_rq 上所有任务 running 状态下的几何级数之和再乘以 SCHED_CAPACITY_SCALE 后的值。

load_avgrunnable_avgutil_avg分别从权重(优先级)、任务个数、CPU时间片占用三个维度来描述CPU的负载。

 

7.2.3. gse 负载

对比着tse来讲解gse:

(1) gse会和tse走一样的负载更新流程(逐层向上更新,就会更新到gse)

(2) gserunnable负载与tse是不同的。tserunnable_sum是任务 running+runnable 状态的几何级数累加值然后scale up后的值。而gse是其当前层级下所有层级的tse的个数之和乘以时间几何级数然后scale up后的值,见 __update_load_avg_se() 函数 runnable 参数的差异。

(3) gse tseload_avg 虽然都等于 se->weight * load_sum/divider, ___update_load_avg() 的参数差异。但是weight 来源不同,因此也算的上是一个差异点,tse->weight来源于其优先级,而gse来源于其从tg中分得的配额。

(4) gse会比tse多出了一个负载传导更新过程,放到下面讲解(若不使能CFS组调度,只有一层,没有tg的层次结构,因此不需要传导,只需要更新到cfs_rq上即可)

 

7.2. 4. grq 负载

grq的负载和cfs_rq的负载在更新上没有什么不同。grq会比cfs_rq多了一个负载传导更新过程,放到下面讲解。

 

7.2.5. tg的负载

tg只有一个load负载,就是tg->load_avg,取值为\Sum tg->cfs_rq[]->avg.load_avg,也即tg所有CPU上的grqload_avg 之和。tg负载更新是在update_tg_load_avg()中实现的,主要用于给gse[]分配权重。

static inline void update_tg_load_avg(struct cfs_rq *cfs_rq)
{
    /* 得到delta值 */
    long delta = cfs_rq->avg.load_avg - cfs_rq->tg_load_avg_contrib; //对 cfs_rq->avg.load_avg 的缓存

    /* 不需要更新root_task_group的负载 */
    if (cfs_rq->tg == &root_task_group)
        return;
    /*
     * 差值大到一定程度后才加到tg->load_avg上,以减少对公共的
     * tg->load_avg 成员的访问次数,降低对性能的影响
     */
    if (abs(delta) > cfs_rq->tg_load_avg_contrib / 64) {
        /* tg对应的所有cfs_rq 上的delta都加过来 */
        atomic_long_add(delta, &cfs_rq->tg->load_avg);
        /* 重新缓存 */
        cfs_rq->tg_load_avg_contrib = cfs_rq->avg.load_avg;
    }
}

调用路径:

  update_load_avg //负载更新
__update_blocked_fair //负载均衡路径
detach_entity_cfs_rq //任务从tg迁移走
attach_entity_cfs_rq //任务迁移到tg中来
    update_tg_load_avg

7.3. 负载传导

负载传导是使能CFS组调度后才有的概念。当tg层次结构上插入或删除一个tse的时候,整个层次结构的负载都变化了,因此需要逐层向上层进行传导。

 

7.3.1. 负载传导触发条件

是否需要进行负载传导是通过struct cfs_rq propagate 成员进行标记。 grq上增加/删除的tse时会触发负载传导过程。 tse的负载load_sum值会记录在 struct cfs_rq prop_runnable_sum 成员上,然后逐层向上传导。其它负载(runnable_*util_*)则会通过tse-->grq-->gse-->grq...逐层向上层传导。

add_tg_cfs_propagate() 中标记需要进行负载传导:

static inline void add_tg_cfs_propagate(struct cfs_rq *cfs_rq, long runnable_sum)
{
    cfs_rq->propagate = 1;
    cfs_rq->prop_runnable_sum += runnable_sum;
}

此函数调用路径:

switched_to_fair //从非CSF调度类变为CFS调度类
task_move_group_fair //移到tg中来
    attach_task_cfs_rq
    post_init_entity_util_avg //新建的任务开始挂到cfs_rq上
        attach_entity_cfs_rq
        update_load_avg //迁移到当前CPU
            attach_entity_load_avg
                add_tg_cfs_propagate(cfs_rq, se->avg.load_sum);

set_task_cpu //任务从当前CPU迁移走
    migrate_task_rq_fair //若是唤醒迁移走,先记录再传导
switched_from_fair //从CFS调度类变为非CFS调度类
task_move_group_fair //从tg中移走
    detach_task_cfs_rq
        detach_entity_cfs_rq
            detach_entity_load_avg
                add_tg_cfs_propagate(cfs_rq, -se->avg.load_sum);

由上可见,当从非CSF调度类变为CFS调度类、移到当前tg中来、新建的任务开始挂到cfs_rq上、迁移到当前CPU都会触发负载传导过程,此时会向整个层次结构中传导添加这个任务带来的负载。当任务从当前CPU迁移走、变为非CFS调度类、从tg迁移走,此时会向整个层次结构中传导移除这个任务降低的负载。

注意,任务休眠时并没有将其负载移除,只是休眠期间其负载不增加了,随时间衰减

 

7.3.2. 负载传导过程

负载传导过程体现在逐层更新负载的过程中。如下,负载更新函数update_load_avg() 在主要路径下,每层都会进行调用:

enqueue_task_fair //for_each_sched_entity(se)
    enqueue_entity(cfs_rq, se, flags)
        update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);

dequeue_task_fair //for_each_sched_entity(se)
    dequeue_entity(cfs_rq, se, flags)
        update_load_avg(cfs_rq, se, UPDATE_TG);

task_tick_fair //for_each_sched_entity(se)
    entity_tick(cfs_rq, curr, queued)
        update_load_avg(cfs_rq, curr, UPDATE_TG);

enqueue_task_fair //for_each_sched_entity(se)
    update_load_avg(cfs_rq, se, UPDATE_TG);

dequeue_task_fair //for_each_sched_entity(se)
    update_load_avg(cfs_rq, se, UPDATE_TG);

pick_next_task_fair //for_each_sched_entity(se)
    put_prev_entity(cfs_rq, prev) //if (prev->on_rq)为真才调用
        update_load_avg(cfs_rq, prev, 0);
    set_next_entity(cfs_rq, se) //if (se->on_rq)为真才调用
        update_load_avg(cfs_rq, se, UPDATE_TG);

load负载传导函数和标记需要进行传导的函数是同一个,为 add_tg_cfs_propagate(), 其调用路径如下:

 

update_load_avg
    propagate_entity_load_avg
        add_tg_cfs_propagate(cfs_rq, gcfs_rq->prop_runnable_sum) //辅助更新load负载
update_tg_cfs_util(cfs_rq, se, gcfs_rq);
update_tg_cfs_runnable(cfs_rq, se, gcfs_rq);
update_tg_cfs_load(cfs_rq, se, gcfs_rq);

 

7.3.2.1. update_tg_cfs_util() 更新gsegrqutil_* 负载,并负责将负载传递给上层。

/*
 * propagate_entity_load_avg 调用传参: (cfs_rq, se, gcfs_rq)
 * cfs_rq: gse所挂入的cfs_rq,  se: gse,  gcfs_rq: gse的cfs_rq
 */
static inline void update_tg_cfs_util(struct cfs_rq *cfs_rq, struct sched_entity *se, struct cfs_rq *gcfs_rq)
{
    long delta = gcfs_rq->avg.util_avg - se->avg.util_avg;;

    /* gse util 负载更新 */
    se->avg.util_avg = gcfs_rq->avg.util_avg;
    se->avg.util_sum = se->avg.util_avg * divider;

    /* util 负载向上层传导 */
    add_positive(&cfs_rq->avg.util_avg, delta);
    cfs_rq->avg.util_sum = cfs_rq->avg.util_avg * divider;
}

可见gseutil负载在传导时直接取的是其grq上的util负载。然后通过更新上层 grq util_avg 向上层传导。

 

7.3.2.2. update_tg_cfs_runnable() 更新gsegrqrunnable_*负载,并负责将负载传递给上层。

static inline void update_tg_cfs_runnable(struct cfs_rq *cfs_rq, struct sched_entity *se, struct cfs_rq *gcfs_rq)
{
    long delta = gcfs_rq->avg.runnable_avg - se->avg.runnable_avg;

    /* gse runnable 负载更新 */
    se->avg.runnable_avg = gcfs_rq->avg.runnable_avg;
    se->avg.runnable_sum = se->avg.runnable_avg * divider;

    /* runnable 负载向上层传导 */
    add_positive(&cfs_rq->avg.runnable_avg, delta);
    cfs_rq->avg.runnable_sum = cfs_rq->avg.runnable_avg * divider;
}

可见gserunnable负载在传导时也是直接取的是其grq上的runnable负载。然后通过更新上层 grq runnable_avg 向上层传导。

 

7.3.2.3. update_tg_cfs_load() 更新gsegrqload_*负载,并负责将负载传递给上层。

load负载比较特殊,负载传导时并不是直接取自grqload负载,而是在向grq添加/删除任务时就记录了tse load_sum值,然后在 add_tg_cfs_propagate() 中逐层向上传导,传导位置调用路径:

update_load_avg
    propagate_entity_load_avg
        add_tg_cfs_propagate(cfs_rq, gcfs_rq->prop_runnable_sum)

load负载的标记和传导都是这个函数:

 

static inline void add_tg_cfs_propagate(struct cfs_rq *cfs_rq, long runnable_sum)
{
    cfs_rq->propagate = 1;
    cfs_rq->prop_runnable_sum += runnable_sum;
}

load负载更新函数:

 

static inline void update_tg_cfs_load(struct cfs_rq *cfs_rq, struct sched_entity *se, struct cfs_rq *gcfs_rq)
{
/* 保存的是se的load_sum,最初是tse的 */
    long delta_avg, running_sum, runnable_sum = gcfs_rq->prop_runnable_sum;
    unsigned long load_avg;  u64 load_sum = 0;  s64 delta_sum;  u32 divider;
    gcfs_rq->prop_runnable_sum = 0;
    divider = get_pelt_divider(&cfs_rq->avg);

    if (runnable_sum >= 0) { //添加cfs任务
        runnable_sum += se->avg.load_sum; //单纯的时间级数
        runnable_sum = min_t(long, runnable_sum, divider); //不应超过时间级数最大值divider
    } else { //移除cfs任务
        /* 若grq上还有任务,得到grq上任务的平均load_sum */
        if (scale_load_down(gcfs_rq->load.weight)) {
            /* cfs_rq的load_sum是权重*几何级数,se的load_sum是单纯的几何级数 */
            load_sum = div_s64(gcfs_rq->avg.load_sum, scale_load_down(gcfs_rq->load.weight));
        }
        runnable_sum = min(se->avg.load_sum, load_sum); //删除任务负载不要越删越大
    }

    /* 这里的 runnable_sum 不是 struct sched_avg 中的 runnable_sum,保存
     * 的是 se 的 load_sum 值,是个单纯的几何级数。*/
    running_sum = se->avg.util_sum >> SCHED_CAPACITY_SHIFT;
    runnable_sum = max(runnable_sum, running_sum);

    /* 权重*几何级数转换为grq的load_sum */
    load_sum = (s64)se_weight(se) * runnable_sum;
    load_avg = div_s64(load_sum, divider);

    delta_sum = load_sum - (s64)se_weight(se) * se->avg.load_sum;
    delta_avg = load_avg - se->avg.load_avg;

    /* 更新到gse上 */
    se->avg.load_sum = runnable_sum;
    se->avg.load_avg = load_avg;

    /* 更新到上层cfs_rq上,delta可正可负 */
    add_positive(&cfs_rq->avg.load_avg, delta_avg);
    add_positive(&cfs_rq->avg.load_sum, delta_sum);
}

删除任务就是将grq上的se的平均load_sum赋值给gse。 添加任务是将gseload_sum直接加上delta值。

load_avg和普通tse计算方式一样,为load_sum*se_weight(gse)/divider

对比可见,runnable负载和util负载的传导方向是由grq-->gse,分别通过runnable_avg/util_avg进行传导,gse直接取grq的值。而load负载的传导方向是由gse-->grq进行传导,且是通过load_sum进行传导的。

load负载传导赋值方式上为什么和runnable负载和util负载有差异,可能和其统计算法有关。对于runnable_avggse计算的是当前层级下所有层级上tse的个数和乘以runnable状态时间级数的比值,底层增加一个tse对上层相当于tse个数增加了一个;对于util_avggse计算的是其下所有tserunning状态几何级数和与时间级数的比值,底层增加一个tse对上层就相当于增加了tserunning状态的几何级数;而 load_avg se的权重有关,gsetse的权重来源不同,前者来自从tg->shares中分得的配额,而后者来源于优先级,不能直接相加减。而load_sum对于se来说是一个单纯的runnable状态的时间级数,不涉及权重,因此tsegse都可以使用它。

对于load_avg的传导举个例子,如下图5,假如ts2一直休眠,ts1ts3是两个死循环,那么gse1grq1load_avg将趋近于4096,而根cfs_rq的负载将趋近于2048,若此时要将ts3迁移走,若像计算runnableutil负载那样直接想减,得到的delta值是-4096,那么根cfs_rqload_avg将会是个负值(2048-4096<0),这显然是不合理的。若通过load_sum进行传导,它只是个时间级数,相减后根cfs_rq上只相当于损失了50%的负载。

5:

: 这只是在tg的层次结构中添加/删除任务时的负载的传导更新路径,随着时间的流逝,即使没有添加/移除任务,gse/grq的负载也会更新,因为普通的负载更新函数 __update_load_avg_se()/update_cfs_rq_load_avg() 并没有区分是tse还是gse,是cfs_rq还是grq

7.4. 层次负载

在负载均衡的时候,需要迁移CPU上的负载以便达到均衡,为了达成这个目的,需要在CPU之间进行任务迁移。然而各个task seload avg并不能真实反映它对root cfs rq(即对该CPU)的负载贡献,因为task se/cfs rq总是在某个具体level上计算其load avg。比如grqload_avg并不会等于其上挂的所有tseload_avg的和,因为runnable的时间级数肯定是Sum(tse) > grq(runnable等待运行的状态存在)

为了计算taskCPU的负载(h_load),在各个cfs rq上引入了hierarchy load的概念,对于顶层cfs rq而言,其hierarchy load等于该cfs rqload avg,随着层级的递进,cfs rqhierarchy load定义如下:

下一层的cfs rq的h_load = 上一层cfs rq的h_load  x  gse负载在上一层cfs负载中的占比

在计算最底层tseh_load的时候,我们就使用如下公式:

tse的h_load = grq的h_load  x  tse的load avg / grq的load avg

获取和更新taskh_load的函数如下:

 

static unsigned long task_h_load(struct task_struct *p)
{
    struct cfs_rq *cfs_rq = task_cfs_rq(p); //p->se.cfs_rq

    /* 更新层级负载 */
    update_cfs_rq_h_load(cfs_rq);

    /* tse在最底层的gse->cfs_rq上再分解一次 */
    return div64_ul(p->se.avg.load_avg * cfs_rq->h_load, cfs_rq_load_avg(cfs_rq) + 1);
}

更新grqh_load的函数如下:

 

static void update_cfs_rq_h_load(struct cfs_rq *cfs_rq)
{
    struct rq *rq = rq_of(cfs_rq);
    struct sched_entity *se = cfs_rq->tg->se[cpu_of(rq)];
    unsigned long now = jiffies;
    unsigned long load;

    if (cfs_rq->last_h_load_update == now)
        return;

    /* 最低层级的 gse->cfs_rq 的赋值为NULL,在下面循环中作为退出条件 */
    WRITE_ONCE(cfs_rq->h_load_next, NULL);

    /* 从gse向上更新每层的 cfs_rq->h_load_next,它是个struct sched_entity* 类型指针 */
    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se); //se->cfs_rq
        WRITE_ONCE(cfs_rq->h_load_next, se);
        /* 有一层的jiffies值没有更新,也就是说有一层才刚刚更新完 */
        if (cfs_rq->last_h_load_update == now)
            break;
    }

    /* 已经到顶了,顶层的 se->parent 是NULL */
    if (!se) {
        cfs_rq->h_load = cfs_rq_load_avg(cfs_rq); //顶层的取值: cfs_rq->avg.load_avg;
        cfs_rq->last_h_load_update = now;
    }

    /* 然后从上而下计算 */
    while ((se = READ_ONCE(cfs_rq->h_load_next)) != NULL) {
        load = cfs_rq->h_load;
        /*
         * load = cfs_rq->h_load * se->avg.load_avg / (cfs_rq->avg.load_avg + 1)
         * se的load_avg与其所在的cfs_rq的load_avg的比值,然后乘以load。表示se的
         * load_avg表示多少cfs_rq的load_avg.
         */
        load = div64_ul(load * se->avg.load_avg, cfs_rq_load_avg(cfs_rq) + 1);
        cfs_rq = group_cfs_rq(se); //return se->my_q
        cfs_rq->h_load = load; //load赋值给子级的 h_load, 然后逐层分解下去
        cfs_rq->last_h_load_update = now;
    }
}

调用路径:

    pick_next_task_fair //在函数的最后调用
    newidle_balance //在函数最开始位置调用
    task_tick_fair //在函数最后调用
        update_misfit_status
find_idlest_group //负载均衡路径
    update_sg_wakeup_stats //MC DIE层级的唤醒状态更新    
        cpu_load_without
select_task_rq_fair //选核,wake亲和性路径
    wake_affine
        wake_affine_weight
    load_balance //负载均衡迁移负载路径
        detach_tasks
            task_h_load
                update_cfs_rq_h_load

可以看到,主要是唤醒wake_affine_weight机制和负载均衡逻辑中使用。比如迁移类型为load的负载均衡中,要迁移多少load_avg可以使负载达到均衡,使用的就是task_h_load(),见 detach_tasks()

八、总结

本文介绍了CFS组调度功能引入的原因,配置方法,和一些实现细节。此功能可以在高负载下"软限制"(相比与CFS带宽控制)各分组任务对CPU资源的使用占比,以达到各组之间公平使用CPU资源的目的。在老版原生Android代码中对后台分组限制的较狠(甚是将 background/cpu.shares 设置到52),将CPU资源重点向前台分组进行倾斜,但这个配置可能会在某些场景下出现前台任务被后台任务卡住的情况,对于普适性配置,最新的一些Android版本中将各个分组的 cpu.shares 都设置为1024以追求CPU资源在各组之间的公平。

 

九、补充

1. FAIR_GROUP_SCHED 和 RT_GROUP_SCHED 都定义在 init/Kconfig:config 中(msm-4.4)。 

 

参考

1.内核源码(https://www.kernel.org/)Android源码(https://source.android.com/docs/setup/download/downloading)

2.内核文档Documentation/admin-guide/cgroup-v1

3.CFS调度器(3-组调度: http://www.wowotech.net/process_management/449.html

4.PELT算法浅析: http://www.wowotech.net/process_management/pelt.html

 

posted on 2023-01-06 21:25  Hello-World3  阅读(809)  评论(0编辑  收藏  举报

导航