DumpStack

静下来,享受技术!
  1. 首页
  2. 调度原理
  3. 正文

Linux调度原理之(五):组调度

2022年4月5日 1422点热度 0人点赞 0条评论

关注公众号不迷路:DumpStack

扫码加关注

目录

  • 一、前言
    • 1.1 为什么要引入组调度
    • 1.2 为什么Linux中调度的单位是sched_entity而不是task_struct?
  • 二、数据结构中关键成员解释
    • 2.1 task_group中的X_se[]和X_rq[]
    • 2.2 se中的my_q和X_rq成员
  • 三、支持组调度时,各个数据结构之间的拓扑关系
    • 3.1 拓扑结构图
      • a) 各数据结构之间的拓扑关系
      • b) 各数据结构之间的拓扑关系
      • c) 组调度时,se的拓扑关系
    • 3.1 root_task_group - 根组
    • 3.2 task_groups - 全局链表,系统中所有group都挂在该链表上
    • 3.5 sched_create_group - 创建一个新的group
    • 3.6 alloc_fair_sched_group - cfs组调度相关初始化
      • 3.6.1 init_cfs_rq - 初始化cfs_rq结构
      • 3.3.1 init_tg_cfs_entry
    • 3.7 alloc_rt_sched_group - rt组调度相关初始化
      • 3.3.2 init_rt_rq - 初始化rt_rq结构
      • 3.3.2 init_tg_rt_entry
    • 3.3 sched_init - 对根组的初始化
    • 3.4 sched_online_group - 新增一个组
    • 3.5 sched_offline_group
  • 四、组调度策略
    • 4.1 rt进程的组调度
    • 4.2 cfs进程的组调度
  • 五、group的权重:shares - 被所有gse瓜分
    • 5.1 怎样指定group的shares值 - cgroup中导出的接口
      • 5.1.1 怎样找到挂在的节点位置
      • 5.1.2 方法实现
    • 5.2 gse怎样瓜分shares
      • 5.2.1 计算原理
      • 5.2.2 sched_group_set_shares - 重新设置group的shares值
      • 5.2.3 update_cfs_shares - 更新指定grq的shares值
      • 5.2.4 calc_cfs_shares - 计算这个grq瓜分到的shares
        • 5.2.4.1 公式推导原理
        • 5.2.4.2 函数实现
      • 5.2.5 reweight_entity - 更新gse的权重
  • 六、附:组调度时的组织架构
    • 6.1 entity_is_task - 判断这个se结构是不是一个task
    • 6.2 group_cfs_rq - 返回gse对应的grq结构
    • 6.3 is_same_group - 判断两个se是否在同一个group中
    • 6.4 cfs_rq_of - 得到这个se被挂在哪个cfs_rq运行队列上
    • 6.5 task_cfs_rq - 得到这个任务p被挂在哪个cfs_rq队列上
    • 6.6 task_of - 由se返回对应的task_struct结构
    • 6.7 rq_of - 已知运行队列cfs_rq,返回其所属的rq结构
    • 6.8 parent_entity - 返回se的父节点
    • 6.9 for_each_sched_entity - 支持组调度的时候,逐层遍历祖先的gse结构
    • 6.10 find_matching_se - 层层向上,直到这两个se在同一个cfs_rq树中,也就是说在同一个组中
  • 关注公众号不迷路:DumpStack

 

 

 

 

术语约定:

gse: group se

grq: group cfs_rq

tg: task_group

 

一、前言

1.1 为什么要引入组调度

Linux调度器的粒度是一个进程,这样的粒度在很多场景是不合适的,比如,希望达到用户粒度,也就是希望每个用户占有相同的cpu时间。但是,在各个用户拥有的进程数量不同的情况下,显然调度器会将cpu更多地分配给进程数量多的用户。组调度的引入,正是解决此问题的

 

linux的cgroup(control group)支持将进程分组,然后按组来划分各种资源,例如group1拥有30%的CPU和50%的磁盘IO,group2拥有10%的CPU和20%的磁盘IO等等

组调度就是通过cgroup子系统实现的,cgroup支持很多种资源的划分,CPU资源就是其中之一,可以将进程进行分组,按组来分配CPU资源等

例如A和B两个用户使用同一台机器,A用户16个进程,B用户2个进程,如果按照进程的个数来分配CPU资源,显然A用户会占据大量的CPU时间,这对于B用户是不公平的。组调度就可以解决这个问题,分别将A、B用户进程划分成两个组,调度器先从两个组中选择一个组,再从选中的组中选择一个进程来执行,如果两个组被选中的机率相当,即两个组的权重相同,那么用户A和B将各占有约50%的CPU。

 

下面举个例子,如下图,系统中共有5个线程,如果没有组调度的话,则5个线程均分cpu时间,每个占用20%。如果对这5个线程进程分组,其中task1~4归属groupA,task5归属于groupB,并且两个组的权重是不一样的,shares值分别是1024和4096,那么各个任务分到的时间如下:

 

总结:引入组调度,是为了实现将进程分组,按分组来进行CPU资源分配

 

1.2 为什么Linux中调度的单位是sched_entity而不是task_struct?

Linux使用task_struct结构体描述一个进程,但是调度的单位却是调度实体sched_entity,并不是task_struct,为什么呢?

这主要是为了方便支持组调度扩展,如果不支持组调度的话,确实可以使用task_struct作为调度的单位。当支持组调度的时候,是按组来分配运行时间的。在支持组调度的时候,一个组也会抽象成一个调度实体,如下图所示,黄色的圈表示group,只有se结构,没有task_struct结构,而绿色的每个圈即对应一个se结构,也对应一个task_struct结构

 

CFS调度器管理的是sched_entity调度实体,task_struct(代表线程)和task_group(代表线程组,即进程)中都包含sched_entity,进而来参与调度;

 

 

二、数据结构中关键成员解释

下面这个图是相当生动了

参考文档:https://blog.csdn.net/ctthuangcheng/article/details/8914825

 

 

2.1 task_group中的X_se[]和X_rq[]

一个task_group可以包含具有任意类别的进程(实际目前只支持对cfs和rt两种进程进行分组,这一点从"task_group数据结构中只包含cfs和rt的调度实体和就绪队列"可以看出),于是task_group需要为每一种进程提供一组"调度相关的结构",这里所说的一组"调度相关的结构"主要包括两个部分:调度实体和运行队列,也就是task_group数据结构中的X_se[]和X_rq[],两者都是per-cpu的。在cpuN上这个组中的所有任务对应的调度实体会被添加到运行队列X_rq[N]中,而在第N个cpu上,task_group自身也有一个调度实体X_se[N],它的调度实体会被添加到其父task_group的运行队列,(实际上这个group对应的调度实体se是一个数组,即X_se[],当要把这个group对应的se挂到指定的cpu对应的队列中时,是把对应的X_se[N]结构挂入cpuN对应的链表中;而当在cpuN上,在这个组内pick任务时,实际上是在X_rq[N]中pick任务)

 

2.2 se中的my_q和X_rq成员

这里说的调度实体se包括sched_entity和sched_rt_entity

 

my_q成员,表示这个se对应的cfs_rq和rt_rq结构

  • 如果调度实体代表group,则my_q字段指向这个组对应的运行队列,也就是task_group结构中的X_rq[N]结构,用于挂载这个组在这个cpu上所有等待调度的任务
  • 如果调度实体代表task时,则my_q字段为NULL

entity_is_task就是根据my_q来判断entity是表示task还是group的

 

X_rq成员,表示这个se所属的cfs_rq或rq_rq

  • 如果调度实体代表group,则X_rq表示这个组对应的se会被挂在哪个运行队列被调度
  • 如果调度实体代表task,则X_rq表示这个tase对应的se应该被挂在哪个运行队列被调度

 

三、支持组调度时,各个数据结构之间的拓扑关系

3.1 拓扑结构图

a) 各数据结构之间的拓扑关系

支持组调度时,各数据结构之间的拓扑结构如下:

 

从上面图中,我们主要关注以下几点:

  • 内核维护了一个全局链表task_groups,系统中的所有task_group都会被添加到这个链表中;
  • 内核还定义了一个root_task_group全局变量,作为系统的根组,也是系统中所有task_group树形结构的根;
  • task_group数据结构的子节点,会加入到父节点的siblings链表中;
  • 每个task_group会分配就绪队列数组和调度实体数组,其中数组的个数为系统CPU的个数,也就是为每个CPU都分配了运行队列和调度实体;

 

b) 各数据结构之间的拓扑关系

假设系统包含4个CPU,组调度的打开的情况下,各种结构体之间的关系如下图。

 

 

通过上面的示意图,可以获取以下信息

  • 系统中的所有task_group结构会形成一个树形结构,该树形结构的根为root_task_group,对应上图黄色的root tg,注意到root tg的se和parent都是NULL,root tg的cfs_rq[cpu_id]指向这个cpu对应的rq.cfs_rq
  • 一个组只对应一个task_group结构,但是这个组在每个cpu上都有一个group se结构,记录在task_group.se[cpu_id]中
  • 在每个CPU上都有一个全局的就绪队列struct rq结构,对应上图的紫色rq,有多少个CPU就有多少个全局就绪队列rq,rq->cfs_rq指向这个cpu上的根cfs_rq就绪队列
  • 每个cpu都有自己的根cfs_rq就绪队列,有多少个cpu就有多少个这样的根cfs_rq就绪队列,根cfs_rq就绪队列中维护一棵红黑树,上图中红黑树上一共10个就绪态调度实体,其中9个是task se(上图橘色se),1个group se(上图蓝色se)
  • 蓝色group se的my_q成员指向这个group自己对应的csf_rq就绪队列,该csf_rq就绪队列上也有一个红黑树,该红黑树上共9个task se,这9个task se的parent成员的指向和task_group.se[cpu_id](即下面这个黄色的tg)的指向一致,即都是指向group se(对应上图蓝色的group se),用于标记自己所属的组;
  • 每个group se(蓝色se)都有自己的group cfs_rq,有多少个CPU就会有多少个group se和group cfs_rq,分别存储在task_group.se[]和task_group.cfs_rq[]中
  • task se的depth成员记录se嵌套深度,最顶层cfs_rq就绪队列下的se的深度为0,group se往下一层层递增
  • cfs_rq->nr_runing成员记录当前cfs_rq就绪队列所有调度实体个数,不包含子就绪队列
  • cfs_rq->h_nr_running成员记录就绪队列层级上所有调度实体的个数,包含group se对应group cfs_rq上的调度实体。例如,图中上半部,nr_runing和h_nr_running的值分别等于10和19,多出的9是group cfs_rq的h_nr_running。group cfs_rq中由于没有group se,因此nr_runing和h_nr_running的值都等于9

 

c) 组调度时,se的拓扑关系

对应到实际的运行中,如下:

 

  • struct cfs_rq包含了红黑树结构,sched_entity调度实体参与调度时,都会挂入到红黑树中,task_struct和task_group都属于被调度对象;
  • task_group会为每个CPU再维护一个cfs_rq,这个cfs_rq用于组织挂在这个任务组上的任务以及子任务组,参考图中的Group A;
  • 调度器在调度的时候,比如调用pick_next_task_fair时,会从遍历队列,选择sched_entity,如果发现sched_entity对应的是task_group,则会继续往下选择;
  • 由于sched_entity结构中存在parent指针,指向它的父结构,因此,系统的运行也能从下而上的进行遍历操作,通常使用函数walk_tg_tree_from进行遍历;

 

3.1 root_task_group - 根组

根组就是一个静态定义的task_group数据结构

文件位置:W:\opensource\linux-5.10.61\kernel\sched\core.c

/*

* Default task group.

* Every task in system belongs to this group at bootup.

*/

struct task_group root_task_group;

 

3.2 task_groups - 全局链表,系统中所有group都挂在该链表上

文件位置:W:\opensource\linux-5.10.61\kernel\sched\core.c

LIST_HEAD(task_groups);

 

3.5 sched_create_group - 创建一个新的group

/* allocate runqueue etc for a new task group */

struct task_group *sched_create_group(

            struct task_group *parent)        //指定新创建的group的parent

{

    struct task_group *tg;

 

    //1.动态申请group结构,注意根组是静态定义的

    tg = kmem_cache_alloc(task_group_cache, GFP_KERNEL | __GFP_ZERO);

    if (!tg)

        return ERR_PTR(-ENOMEM);

 

    //2.cfs相关处理

    if (!alloc_fair_sched_group(tg, parent))

        goto err;

 

    //3.rt相关处理

    if (!alloc_rt_sched_group(tg, parent))

        goto err;

 

    //4.uclamp相关初始化

    alloc_uclamp_sched_group(tg, parent);

 

    return tg;

 

err:

    sched_free_group(tg);

    return ERR_PTR(-ENOMEM);

}

 

3.6 alloc_fair_sched_group - cfs组调度相关初始化

文件位置:W:\opensource\linux-5.10.61\kernel\sched\fair.c

int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)

{

    struct sched_entity *se;

    struct cfs_rq *cfs_rq;

    int i;

 

    //1.为group中的X_se[]和X_rq[]申请空间

    // 注意:

    // a) X_se[]和X_rq[]中存放的是地址,并不是真正的cfs_rq和sched_entity空间

    // b) 这个group在每个cpu上都有自己的X_se和X_rq结构

    tg->cfs_rq = kcalloc(nr_cpu_ids, sizeof(cfs_rq), GFP_KERNEL);

    if (!tg->cfs_rq)

        goto err;

    tg->se = kcalloc(nr_cpu_ids, sizeof(se), GFP_KERNEL);

    if (!tg->se)

        goto err;

 

    //2.新创建的group的shares值默认为1024

    tg->shares = NICE_0_LOAD;

 

    //3.带宽初始化,暂不分析

    init_cfs_bandwidth(tg_cfs_bandwidth(tg));

 

    //4.初始化这个组在每个cpu上的se结构

    for_each_possible_cpu(i) {

        //4.1 为这个组在每个cpu上申请一个cfs_rq和sched_entity结构

        cfs_rq = kzalloc_node(sizeof(struct cfs_rq), GFP_KERNEL, cpu_to_node(i));

        if (!cfs_rq)

            goto err;

 

        se = kzalloc_node(sizeof(struct sched_entity), GFP_KERNEL, cpu_to_node(i));

        if (!se)

            goto err_free_rq;

 

        //4.2 对cfs_rq结构进行简单的初始化

        init_cfs_rq(cfs_rq);

 

        //4.3 指定这个group在这个cpu上的cfs_rq和se结构,并指定parent

        init_tg_cfs_entry(tg, cfs_rq, se, i, parent->se[i]);

        init_entity_runnable_average(se);

    }

 

    return 1;

 

err_free_rq:

    kfree(cfs_rq);

err:

    return 0;

}

 

3.6.1 init_cfs_rq - 初始化cfs_rq结构

void init_cfs_rq(struct cfs_rq *cfs_rq)

{

    cfs_rq->tasks_timeline = RB_ROOT_CACHED;

    cfs_rq->min_vruntime = (u64)(-(1LL << 20));

#ifndef CONFIG_64BIT

    cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime;

#endif

#ifdef CONFIG_SMP

    raw_spin_lock_init(&cfs_rq->removed.lock);

#endif

}

 

3.3.1 init_tg_cfs_entry

下面的代码实现结合上面的拓扑结构图一起看

注意:根组传入的se和parent为NULL

void init_tg_cfs_entry(

            struct task_group *tg,            //要初始化的group

            struct cfs_rq *cfs_rq,            //这个组在这个cpu上对应的grq结构

            struct sched_entity *se,            //这个组在这个cpu上对应的gse结构,根组没有se结构

            int cpu,                            //哪个cpu

            struct sched_entity *parent)        //parent在这个cpu上对应的gse结构,根组为空

{

    struct rq *rq = cpu_rq(cpu);

 

    cfs_rq->tg = tg;

    cfs_rq->rq = rq;

    init_cfs_rq_runtime(cfs_rq);

 

    //1.每个group在每个cpu上都有自己的cfs_rq和se结构

    tg->cfs_rq[cpu] = cfs_rq;

    tg->se[cpu] = se;

 

    /* se could be NULL for root_task_group */

    //2.为NULL说明初始化的是根组

    if (!se)

        return;

 

    //3.代码走到这里,说明初始化的不是根组,根组没有se结构,无需进行下面的初始化

 

    //4.下面初始化se所属的cfs_rq

    // 若entity表示group,cfs_rq表示"这个group在第N个cpu

    // 上的se结构"被挂在哪个cfs_rq队列中,

    // 若entity表示task,cfs_rq表示这个se所属的cfs_rq

    if (!parent) {

        //这个分支难道表示和根组并列的组吗???

        se->cfs_rq = &rq->cfs;

        se->depth = 0;

    } else {

        se->cfs_rq = parent->my_q;

        se->depth = parent->depth + 1;

    }

 

    //5.下面初始化这个gse对应的cfs_rq结构

    //若entity表示一个task,则my_q为NULL

    //若entity表示一个group,则my_q指向这个组对应的task_group

    // 结构中的cfs_rq[N]成员,N表示cpu编号

    //entity_is_task就是根据这个变量来判断entity是表示task还是group的

    se->my_q = cfs_rq;

 

    /* guarantee group entities always have weight */

    //6.权重初始化为1024

    update_load_set(&se->load, NICE_0_LOAD);

 

    //7.指定parent

    se->parent = parent;

}

 

3.7 alloc_rt_sched_group - rt组调度相关初始化

int alloc_rt_sched_group(struct task_group *tg, struct task_group *parent)

{

    struct rt_rq *rt_rq;

    struct sched_rt_entity *rt_se;

    int i;

 

    //1.为group中的X_se[]和X_rq[]申请空间

    // 注意:

    // a) X_se[]和X_rq[]中存放的是地址,并不是真正的rt_rq和sched_rt_entity空间

    // b) 这个group在每个cpu上都有自己的X_se和X_rq结构

    tg->rt_rq = kcalloc(nr_cpu_ids, sizeof(rt_rq), GFP_KERNEL);

    if (!tg->rt_rq)

        goto err;

    tg->rt_se = kcalloc(nr_cpu_ids, sizeof(rt_se), GFP_KERNEL);

    if (!tg->rt_se)

        goto err;

 

    //2.带宽初始化

    init_rt_bandwidth(&tg->rt_bandwidth,

            ktime_to_ns(def_rt_bandwidth.rt_period), 0);

 

    //3.初始化这个组在每个cpu上的se结构

    for_each_possible_cpu(i) {

        //3.1 为这个组在每个cpu上申请一个rt_rq和sched_rt_entity结构

        rt_rq = kzalloc_node(sizeof(struct rt_rq), GFP_KERNEL, cpu_to_node(i));

        if (!rt_rq)

            goto err;

 

        rt_se = kzalloc_node(sizeof(struct sched_rt_entity), GFP_KERNEL, cpu_to_node(i));

        if (!rt_se)

            goto err_free_rq;

 

        //3.2 对rt_rq结构完成简单的初始化

        init_rt_rq(rt_rq);

        rt_rq->rt_runtime = tg->rt_bandwidth.rt_runtime;

 

        //3.3 指定这个group在这个cpu上的rt_rq和se结构,并指定parent

        init_tg_rt_entry(tg, rt_rq, rt_se, i, parent->rt_se[i]);

    }

 

    return 1;

 

err_free_rq:

    kfree(rt_rq);

err:

    return 0;

}

 

3.3.2 init_rt_rq - 初始化rt_rq结构

void init_rt_rq(struct rt_rq *rt_rq)

{

    struct rt_prio_array *array;

    int i;

 

    array = &rt_rq->active;

    for (i = 0; i < MAX_RT_PRIO; i++) {

        INIT_LIST_HEAD(array->queue + i);

        __clear_bit(i, array->bitmap);

    }

    /* delimiter for bitsearch: */

    __set_bit(MAX_RT_PRIO, array->bitmap);

 

#if defined CONFIG_SMP

    rt_rq->highest_prio.curr = MAX_RT_PRIO;

    rt_rq->highest_prio.next = MAX_RT_PRIO;

    rt_rq->rt_nr_migratory = 0;

    rt_rq->overloaded = 0;

    plist_head_init(&rt_rq->pushable_tasks);

#endif /* CONFIG_SMP */

    /* We start is dequeued state, because no RT tasks are queued */

    rt_rq->rt_queued = 0;

 

    rt_rq->rt_time = 0;

    rt_rq->rt_throttled = 0;

    rt_rq->rt_runtime = 0;

    raw_spin_lock_init(&rt_rq->rt_runtime_lock);

}

 

3.3.2 init_tg_rt_entry

void init_tg_rt_entry(

            struct task_group *tg,            //要初始化的group

            struct rt_rq *rt_rq,                //这个组在这个cpu上对应的rt_rq结构

            struct sched_rt_entity *rt_se,    //这个组在这个cpu上对应的se结构,根组没有se结构

            int cpu,                            //哪个cpu

            struct sched_rt_entity *parent)    //parent在这个cpu上对应的se结构,根组为空

{

    struct rq *rq = cpu_rq(cpu);

 

    rt_rq->highest_prio.curr = MAX_RT_PRIO;

    rt_rq->rt_nr_boosted = 0;

    rt_rq->rq = rq;

    rt_rq->tg = tg;

 

    //1.每个group在每个cpu上都有自己的rt_rq和se结构

    tg->rt_rq[cpu] = rt_rq;

    tg->rt_se[cpu] = rt_se;

 

    //2.为NULL说明初始化的是根组

    if (!rt_se)

        return;

 

    //3.代码走到这里,说明初始化的不是根组,根组没有se结构,无需进行下面的初始化

 

    //4.下面初始化se所属的运行队列

    //若entity表示group,rt_rq表示"这个group在第N个cpu

    // 上的se结构"被挂在哪个运行队列中

    //若entity表示task,rt_rq表示这个se应该被放入的运行队列

    if (!parent)

        rt_se->rt_rq = &rq->rt;

    else

        rt_se->rt_rq = parent->my_q;

 

    //5.这个se对应的rt_rq结构

    //若entity表示一个task,则my_q为NULL

    //若entity表示一个group,则my_q指向这个组对应的task_group

    // 结构中的rt_rq[N]成员,N表示cpu编号

    //entity_is_task就是根据这个变量来判断entity是表示task还是group的

    rt_se->my_q = rt_rq;

 

    //6.指定parent

    rt_se->parent = parent;

    INIT_LIST_HEAD(&rt_se->run_list);

}

 

3.3 sched_init - 对根组的初始化

这里我们只关注组调度相关的操作,其他部分暂时忽略

void __init sched_init(void)

{

    unsigned long ptr = 0;

    ...

 

    //1.为根组task_group结构中的X_se[]和X_rq[]结构申请空间

    // 注意:一个组,在每个cpu上都有自己的X_se和X_rq结构,所以要使用数组来描述

#ifdef CONFIG_FAIR_GROUP_SCHED

    ptr += 2 * nr_cpu_ids * sizeof(void **);

#endif

#ifdef CONFIG_RT_GROUP_SCHED

    ptr += 2 * nr_cpu_ids * sizeof(void **);

#endif

    if (ptr) {

        ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT);

 

#ifdef CONFIG_FAIR_GROUP_SCHED

        root_task_group.se = (struct sched_entity **)ptr;

        ptr += nr_cpu_ids * sizeof(void **);

 

        root_task_group.cfs_rq = (struct cfs_rq **)ptr;

        ptr += nr_cpu_ids * sizeof(void **);

 

        root_task_group.shares = ROOT_TASK_GROUP_LOAD;        //默认值为1024

        init_cfs_bandwidth(&root_task_group.cfs_bandwidth);

#endif

#ifdef CONFIG_RT_GROUP_SCHED

        root_task_group.rt_se = (struct sched_rt_entity **)ptr;

        ptr += nr_cpu_ids * sizeof(void **);

 

        root_task_group.rt_rq = (struct rt_rq **)ptr;

        ptr += nr_cpu_ids * sizeof(void **);

#endif

    }

    ...

 

    //2.带宽控制相关,暂不分析

#ifdef CONFIG_RT_GROUP_SCHED

    init_rt_bandwidth(&root_task_group.rt_bandwidth,

            global_rt_period(), global_rt_runtime());

#endif

 

    //3.将根组对应的task_group结构加入全局链表task_groups中

#ifdef CONFIG_CGROUP_SCHED

    list_add(&root_task_group.list, &task_groups);

    INIT_LIST_HEAD(&root_task_group.children);

    INIT_LIST_HEAD(&root_task_group.siblings);

#endif /* CONFIG_CGROUP_SCHED */

 

    //4.对每个cpu的rq

    for_each_possible_cpu(i) {

        struct rq *rq;

        rq = cpu_rq(i);

 

        ...

#ifdef CONFIG_FAIR_GROUP_SCHED

        /*

         * How much CPU bandwidth does root_task_group get?

         *

         * In case of task-groups formed thr' the cgroup filesystem, it

         * gets 100% of the CPU resources in the system. This overall

         * system CPU resource is divided among the tasks of

         * root_task_group and its child task-groups in a fair manner,

         * based on each entity's (task or task-group's) weight

         * (se->load.weight).

         *

         * In other words, if root_task_group has 10 tasks of weight

         * 1024) and two child groups A0 and A1 (of weight 1024 each),

         * then A0's share of the CPU resource is:

         *

         *    A0's bandwidth = 1024 / (10*1024 + 1024 + 1024) = 8.33%

         *

         * We achieve this by letting root_task_group's tasks sit

         * directly in rq->cfs (i.e root_task_group->se[] = NULL).

         */

        init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);

#endif /* CONFIG_FAIR_GROUP_SCHED */

 

#ifdef CONFIG_RT_GROUP_SCHED

        init_tg_rt_entry(&root_task_group, &rq->rt, NULL, i, NULL);

#endif

        ...

    }

 

    ...

}

 

3.4 sched_online_group - 新增一个组

void sched_online_group(

            struct task_group *tg,                    //新增的task_group结构

            struct task_group *parent)                //指定parent

{

    unsigned long flags;

 

    spin_lock_irqsave(&task_group_lock, flags);

 

    //1.系统中所有的group都挂在全局链表task_group上

    list_add_rcu(&tg->list, &task_groups);

 

    /* Root should already exist: */

    WARN_ON(!parent);

 

    //2.形成拓扑结构,这里参见上面的图

    tg->parent = parent;

    INIT_LIST_HEAD(&tg->children);

    list_add_rcu(&tg->siblings, &parent->children);

    spin_unlock_irqrestore(&task_group_lock, flags);

 

    online_fair_sched_group(tg);

}

 

3.5 sched_offline_group

void sched_offline_group(struct task_group *tg)

{

    unsigned long flags;

 

    /* End participation in shares distribution: */

    unregister_fair_sched_group(tg);

 

    spin_lock_irqsave(&task_group_lock, flags);

    list_del_rcu(&tg->list);

    list_del_rcu(&tg->siblings);

    spin_unlock_irqrestore(&task_group_lock, flags);

}

 

 

四、组调度策略

组调度的主要数据结构已经理清了,这里还有一个很重要的问题。我们知道task拥有其对应的优先级(静态优先级 or 动态优先级),调度程序根据优先级来选择运行队列中的进程。那么,既然task_group和task一样,都被抽象成调度实体,接受同样的调度,task_group的优先级又该如何定义呢?这个问题需要具体到调度类别来解答(因为不同的调度类别,其优先级定义方式不一样),具体来说就是rt和cfs两种类别

 

4.1 rt进程的组调度

rt线程的优先级是跟具体任务相关的,完全由用户来定义的,调度器总是会选择优先级最高的实时进程来运行。

发展到组调度,组的优先级就被定义为"组内最高优先级的进程所拥有的优先级"。比如组内有三个优先级分别为10、20、30的进程,则组的优先级就是10(数值越小优先级越大)。

组的优先级如此定义,引出了一个有趣的现象。当task入队或者出队时,要把它的所有祖先节点都先出队,然后再重新由底向上依次入队。因为组节点的优先级是依赖于它的子节点的,task的入队和出队将影响它的每一个祖先节点。

 

于是,当调度程序从根节点的task_group出发选择调度实体时,总是能沿着正确的路径,找到所有TASK_RUNNING状态的实时进程中优先级最高的那一个。这个实现似乎理所当然,但是仔细想想,这样一来,将实时进程分组还有什么意义呢?无论分组与否,调度程序要做的事情都是"在所有TASK_RUNNING状态的实时进程中选择优先级最高的那一个"。这里似乎还缺了些什么……

现在需要先介绍一下linux系统中的两个proc文件:/proc/sys/kernel/sched_rt_period_us和/proc/sys/kernel/sched_rt_runtime_us。这两个文件规定了,在以sched_rt_period_us为一个周期的时间内,所有实时进程的运行时间之和不超过sched_rt_runtime_us。这两个文件的默认值是1s和0.95s,表示每秒种为一个周期,在这个周期中,所有实时进程运行的总时间不超过0.95秒,剩下的至少0.05秒会留给普通进程。也就是说,实时进程占有不超过95%的CPU。而在这两个文件出现之前,实时进程的运行时间是没有限制的(就像《linux进程调度浅析》里面描述的那样),如果一直有处于TASK_RUNNING状态的实时进程,则普通进程会一直不能得到运行。相当于sched_rt_runtime_us等于sched_rt_period_us。

为什么要有sched_rt_runtime_us和sched_rt_period_us两个变量呢?直接使用一个表示CPU占有百分比的变量不可以么?我想这应该是由于很多实时进程实际上都是周期性地在干某件事情,比如某语音程序每20ms发送一个语音包、某视频程序每40ms刷新一帧、等等。周期是很重要的,仅仅使用一个宏观的CPU占有比无法准确描述实时进程需求。

 

而实时进程的分组就把sched_rt_runtime_us和sched_rt_period_us的概念扩展了,每个task_group都有自己的sched_rt_runtime_us和sched_rt_period_us,(使用的变量依然是那两个全局变量),保证自己组内的进程在以sched_rt_period_us为周期的时间内,最多只能运行sched_rt_runtime_us这么多时间。CPU占有比为sched_rt_runtime_us/sched_rt_period_us。

对于根节点的task_group,它的sched_rt_runtime_us和sched_rt_period_us就等于上面两个proc文件中的值。而对于一个task_group节点来说,假设它下面有n个调度子组和m个TASK_RUNNING状态的进程,这个组的CPU占有比为A、这n个子组的CPU占有比为B,则B必须小于等于A,而A-B剩下的CPU时间将分给那m个TASK_RUNNING状态的进程。(这里讨论的是CPU占有比,因为每个调度组可能有着不同的周期值。)

为了实现sched_rt_runtime_us和sched_rt_period_us的逻辑,内核在更新进程的运行时间的时候(比如由周期性的时钟中断触发的时间更新)会给当前进程的调度实体及其所有祖先节点都增加相应的runtime。如果一个调度实体达到了sched_rt_runtime_us所限定的时间,则将其从对应的运行队列中剔除,并将对应的rt_rq置throttled状态。在这个状态下,这个rt_rq对应的调度实体不会再次进入运行队列。而每个rt_rq都会维护一个周期性的定时器,定时周期为sched_rt_period_us。每次定时器触发,其对应的回调函数就会将rt_rq的runtime减去一个sched_rt_period_us单位的值(但要保持runtime不小于0),然后将rt_rq从throttled状态中恢复回来。

还有一个问题,前面说到,默认情况下,系统中每秒钟内实时进程的运行时间不超过0.95秒。如果实时进程实际对CPU的需求不足0.95秒(大于等于0秒、小于0.95秒),则剩下的时间都会分配给普通进程。而如果实时进程的对CPU的需求大于0.95秒,它也只能够运行0.95秒,剩下的0.05秒会分给其他普通进程。但是,如果这0.05秒内没有任何普通进程需要使用CPU(一直没有TASK_RUNNING状态的普通进程)呢?这种情况下既然普通进程对CPU没有需求,实时进程是否可以运行超过0.95秒呢?不能。在剩下的0.05秒中内核宁可让CPU一直闲着,也不让实时进程使用。可见sched_rt_runtime_us和sched_rt_period_us是很有强制性的。

最后还有多CPU的问题,前面也提到,对于每一个task_group,它的调度实体和运行队列是percpu的。而sched_rt_runtime_us和sched_rt_period_us是作用在调度实体上的,所以如果系统中有N个CPU,实时进程实际占有CPU的上限N*sched_rt_runtime_us/sched_rt_period_us。也就是说,尽管默认情况下限制了每秒钟之内,实时进程只能运行0.95秒。但是对于某个实时进程来说,如果CPU有两个核,也还是能满足它100%占有CPU的需求的(比如执行死循环)。然后,按道理说,这个实时进程占有的100%的CPU应该是由两部分组成的(每个CPU占有一部分,但都不超过95%)。但是实际上,为了避免进程在CPU间的迁移导致上下文切换、缓存失效等一系列问题,一个CPU上的调度实体可以向另一个CPU上对应的调度实体借用时间。其结果就是,宏观上既满足了sched_rt_runtime_us的限制,又避免了进程的迁移

 

4.2 cfs进程的组调度

文章一开头提到了希望A、B两个用户在进程数不相同的情况下也能平分CPU的需求,但是上面关于实时进程的组调度策略好像与此不太相干,其实这就是普通进程的组调度所要干的事。

相比实时进程,普通进程的组调度就没有这么多讲究。组被看作是跟进程几乎完全相同的实体,它拥有自己的静态优先级、调度程序也动态地调整它的优先级。对于一个组来说,组内进程的优先级并不影响组的优先级,只有这个组被调度程序选中时,这些进程的优先级才被考虑。

为了设置组的优先级,每个task_group都有一个shares参数(跟前面提到的sched_rt_runtime_us和sched_rt_period_us两个参数并列)。shares并不是优先级,而是调度实体的权重(这是CFS调度器的玩法),这个权重和优先级是有一一对应的关系的。普通进程的优先级也会被转换成其对应调度实体的权重,所以可以说shares就代表了优先级。

shares的默认值跟普通进程默认优先级对应的权重是一样的。所以在默认情况下,组和进程是平分CPU的。

 

五、group的权重:shares - 被所有gse瓜分

task和group都有权重的概念,调度器会根据权重来分配cpu的时间,task_group的shares值就是该group的权重。想想cfs中的进程,其权重需要从优先级转化而来,而task_group就简单了:直接指定即可。用户可以通过sysfs中的接口指定该group的shares值,kernel会按照一定的规则将这个shares瓜分到各个cpu的gse上

所以:与其说"shares是所有group se的权重之和",倒不如说"shares被该group下的所有gse所瓜分"

 

关键字:

  • 直接指定group的shares值;
  • 将shares值瓜分到各个cpu的gse上;

 

PS:

group的默认shares是1024,也就是相当于一个1024优先级的进程,(nice值为0)

task的优先级(权重)也是由用户自己指定的

 

组调度的原理,就是每个group都拥有自己的cfs_rq以及融入上级的gse,从而搭建出一颗se/cfs_rq树,通过tg的shares或者task的权重将所有节点(tg或task)管理起来。关键是,引入了平行于task的调度实体:tg。

 

5.1 怎样指定group的shares值 - cgroup中导出的接口

5.1.1 怎样找到挂在的节点位置

因为组调度的实现依赖于cgroup,所以可以通过下面命令找出导出的节点

注意:执行mount挂载时传入的参数为cpu

OP5209L1:/ # mount | grep cgroup

none on /dev/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)

none on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

none on /dev/cpuctl type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)

none on /dev/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset,noprefix,release_agent=/sbin/cpuset_release_agent)

none on /dev/memcg type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

none on /dev/freezer type cgroup (rw,relatime,freezer)

 

命令执行截图如下:

 

5.1.2 方法实现

对节点的操作如下:

文件位置:U:\linux-5.10.61\kernel\sched\core.c

static struct cftype cpu_legacy_files[] = {

#ifdef CONFIG_FAIR_GROUP_SCHED

    {

        .name = "shares",

        .read_u64 = cpu_shares_read_u64,

        .write_u64 = cpu_shares_write_u64,

    },

    ...

}

 

实现如下:

static int cpu_shares_write_u64(

            struct cgroup_subsys_state *css,

            struct cftype *cftype,

            u64 shareval)

{

    if (shareval > scale_load_down(ULONG_MAX))

        shareval = MAX_SHARES;

 

    //在下面函数中完成设置和瓜分的操作

    return sched_group_set_shares(css_tg(css), scale_load(shareval));

}

 

static u64 cpu_shares_read_u64(

            struct cgroup_subsys_state *css,

            struct cftype *cft)

{

    struct task_group *tg = css_tg(css);

 

    return (u64) scale_load_down(tg->shares);

}

 

5.2 gse怎样瓜分shares

5.2.1 计算原理

task_group结构中的shares是这个group的权重,被这个组的在所有cpu上的gse所瓜分

下面我们通过一个示例来说明gse是怎么瓜分shares的,如下图所示,已知group的shares值被指定为1024, cpu0上gse下有2个task se,权重之和是3072,cpu1上group se下有3个task se,权重之和是4096,此时shares将会按照下面规则被瓜分

cpu0上group se权重 = 1024*3072/(3072+4096) = 439

cpu1上group se权重 = 1024*4096/(3072+4096) = 585

 

 

 

5.2.2 sched_group_set_shares - 重新设置group的shares值

group的权重(shares)可以通过/sys/fs/cgoup/cpu/<group>/cpu.shares节点设置,对该节点的操作如下,需要注意的是:每个cpu上都有一个gse结构,当前gse设置更新完后,会逐层向上对parent组的gse完成更新,直到根节点(根组没有gse结构);

 

下面场景,在根组下面有一个GroupA,在GroupA下面又有一个GroupB,下面我们通过下面命令修改GroupB的shares值,看看执行流程是什么样的

echo XXX > /sys/fs/cgroup/cpu/A/B/cpu.shares

 

下图中:

橙色的线代表传入参数指向的对象;

紫色的线代表每次更新涉及到的对象,包括三个部分;

处理完当前gse后,继续按同样的流程处理parent的gse;

 

下面我们来看看函数实现

int sched_group_set_shares(

            struct task_group *tg,            //要对哪个组设置shares

            unsigned long shares)                //要设置的shares的值

{

    int i;

    unsigned long flags;

 

    /*

     * We can't change the weight of the root cgroup.

     */

    //1.根组的group->se结构为NULL,根组的shares是不能修改的

    if (!tg->se[0])

        return -EINVAL;

 

    //2.uclamp相关,限制shares值不能超过一定的范围

    shares = clamp(shares, scale_load(MIN_SHARES), scale_load(MAX_SHARES));

 

    mutex_lock(&shares_mutex);

    //3.如果shares值没有变化,则不需要重新设置

    if (tg->shares == shares)

        goto done;

    tg->shares = shares;

 

    //4.因为一个group在所有的cpu上都对应一个group se,

    // 下面遍历所有的cpu上的group se,完成对shares的瓜分工作

    for_each_possible_cpu(i) {

        struct rq *rq = cpu_rq(i);

        struct sched_entity *se;

 

        //4.1 获取group在这个cpu上的group se

        se = tg->se[i];

        /* Propagate contribution to hierarchy */

        raw_spin_lock_irqsave(&rq->lock, flags);

 

        /* Possible calls to update_curr() need rq clock */

        update_rq_clock(rq);

 

        //4.2 从当前group se开始,逐层向上遍历,对所有父group se重新设置shares

        for_each_sched_entity(se)

            update_cfs_shares(group_cfs_rq(se));

        raw_spin_unlock_irqrestore(&rq->lock, flags);

    }

 

done:

    mutex_unlock(&shares_mutex);

    return 0;

}

 

5.2.3 update_cfs_shares - 更新指定grq的shares值

当我们dequeue task、enqueue task以及task tick的时候会通过update_cfs_shargs函数更新group se的权重信息

#ifdef CONFIG_FAIR_GROUP_SCHED

static void update_cfs_shares(

            struct cfs_rq *cfs_rq)    //这里传入的cfs_rq为group在某个cpu上的cfs_rq结构

{                                        //用于挂载这个group在这个cpu上的所有任务

    struct task_group *tg;

    struct sched_entity *se;

    long shares;

 

    //1.获取这个grq所属的group

    tg = cfs_rq->tg;

 

    //2.获取这个grq所属的group的gse

    se = tg->se[cpu_of(rq_of(cfs_rq))];

 

    //3.下面的和带宽控制相关,暂不分析

    if (!se || throttled_hierarchy(cfs_rq))

        return;

 

#ifndef CONFIG_SMP

    //4.如果这两个值相等的话,则说明这个group下的所有进程,都放在这个grq上了,

    // 也就是说这个group中的所有进程都在一个cpu上,这时候当然就不需要瓜分了,

    // 因为所有的线程都在一个cpu上跑,别的cpu上的gse完全不需要cpu运行时间

    if (likely(se->load.weight == tg->shares))

        return;

#endif

    //5.重新计算这个cfs_rq从tg中瓜分到的shares

    shares = calc_cfs_shares(cfs_rq, tg);

 

    //6.更新gse的权重

    reweight_entity(cfs_rq_of(se), se, shares);

}

#else

static inline void update_cfs_shares(struct cfs_rq *cfs_rq)

{

}

#endif

 

5.2.4 calc_cfs_shares - 计算这个grq瓜分到的shares

5.2.4.1 公式推导原理

由之前我们对group的理解,一个group对应的shares,被这个group在所有cpu上的cfs_rq所瓜分,某一个具体的grq所瓜分到的shares计算方法如下:

grq->load.weight

shares = tg->shares * ------------------------- (1)

\Sum grq->load.weight

 

这里插一句,对于上面公式(1),我们考虑到下面两种特殊情况:

  • 单核时,即系统中只有一个cpu时,(此时group只有一个grq)
  • 多核时,当空闲group刚开始运行一个进程的时候,此时这个group内只有一个线程,并且只有一个cpu上的grq上有线程

此时

\Sum grq->load.weight = grq->load.weight

 

此时上面的公式(1)转变为如下:

grq->load.weight

shares = tg->shares * ------------------- = tg->shares

grq->load.weight

 

下面继续,对于上面公式(1),由于计算\Sum grq->load.weight这个总和开销太大,(因为要遍历每个cpu上的grq下的所有task当cpu和task的数量比较大时,一方面访问其他cpu上的grq造成数据访问竞争激烈,另一方面遍历所有task带来的消耗也是庞大的)。因此我们使用平均负载来做近似处理,如下

grq->load.weight ~= grq->avg.load_avg (2)

 

将上面公式(2)带入公式(1)得到:

grq->load.weight

shares = tg->shares * -------------------------

\Sum grq->avg.load_avg

 

=>

grq->load.weight

shares = tg->shares * ------------------------- (3)

tg->load_avg

 

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

 

注意:在继续下面分析时,我们首先要知道如下事实

  • tg->load_avg: 指这个group在所有的cpu上的grq的负载之和
  • grq->tg_load_avg_contrib: 指当前这个grq已经向tg->load_avg贡献的负载。因为tg是一个全局共享变量,多个CPU可能同时访问,为了避免严重的资源抢占,grq负载贡献更新的值并不会立刻加到tg->load_avg上,而是等到负载贡献大于tg_load_avg_contrib一定差值后,再加到tg->load_avg上。例如,2个cpu的系统中,cpu0上的grq初始值tg_load_avg_contrib为0,当grq每次定时器更新负载的时候并不会访问tg变量,而是等到grq的负载grp->avg.load_avg大于tg_load_avg_contrib很多的时候,这个差值达到一个数值(假设是2000),才会更新tg->load_avg为2000。然后,tg_load_avg_contrib的值赋值2000。又经过很多个周期后,grp->avg.load_avg和tg_load_avg_contrib的差值又等于2000,那么再一次更新tg->load_avg的值为4000,这样就避免了频繁访问tg变量。因此,tg->load_avg为这个group在所有cpu上的grq的grq->tg_load_avg_contrib之和

 

所以上面公式(3)我们可以继续转化为

grq->load.weight

shares = tg->shares * ------------------------------------------------------------- (4)

tg->load_avg + (grq->load.weight - grq->tg_load_avg_contrib)

 

PS: 从上面加上的(grq->load.weight - grq->tg_load_avg_contrib)可以看出,这里值考虑了当前cpu上的grq,没有考虑到其他cpu上的grq,实际上也是一个近似值,但是这一句足够了

 

5.2.4.2 函数实现

# ifdef CONFIG_SMP

static long calc_cfs_shares(

            struct cfs_rq *cfs_rq,            //这个group在某个cpu上的grq结构

            struct task_group *tg)            //指向该group

{

    long tg_weight, load, shares;

 

    /*

     * This really should be: cfs_rq->avg.load_avg, but instead we use

     * cfs_rq->load.weight, which is its upper bound. This helps ramp up

     * the shares for small weight interactive tasks.

     */

    load = scale_load_down(cfs_rq->load.weight);

 

    tg_weight = atomic_long_read(&tg->load_avg);

 

    /* Ensure tg_weight >= load */

    tg_weight -= cfs_rq->tg_load_avg_contrib;

    tg_weight += load;

 

    shares = (tg->shares * load);

    if (tg_weight)

        shares /= tg_weight;

 

    if (shares < MIN_SHARES)

        shares = MIN_SHARES;

    if (shares > tg->shares)

        shares = tg->shares;

 

    return shares;

}

# else /* CONFIG_SMP */

static inline long calc_cfs_shares(

            struct cfs_rq *cfs_rq,

            struct task_group *tg)

{

    //当只有一个cpu的时候,这个group中的所有task肯定是在一个cfs_rq中

    return tg->shares;

}

# endif /* CONFIG_SMP */

 

5.2.5 reweight_entity - 更新gse的权重

static void reweight_entity(

            struct cfs_rq *cfs_rq,            //下面这个gse被挂在那个cfs_rq上

            struct sched_entity *se,            //要对哪个gse重新设置权重

            unsigned long weight)                //要设置的新的权重的值

{

    //1.如果这个gse现在已经被挂在运行队列上,

    // 则首先需要将这个gse的权重和负载从cfs_rq中移除

    if (se->on_rq) {

        /* commit outstanding execution time */

        //1.1 如果这个se是cfs_rq上正在运行的进程,那么他已经不在红黑树中

        // 在移除之前需要先更新这个cfs_rq的运行时间信息

        if (cfs_rq->curr == se)

            update_curr(cfs_rq);

 

        //1.2 将gse的权重weight从cfs_rq中删除

        account_entity_dequeue(cfs_rq, se);

    }

 

    //2.重新设置gse的权重信息

    update_load_set(&se->load, weight);

 

    //6.因为权重改了,所以这里需要重新将新的权重加到

    // cfs_rq->load.weight和cfs_rq->runnable_weight中去

    if (se->on_rq)

        account_entity_enqueue(cfs_rq, se);

}

 

六、附:组调度时的组织架构

6.1 entity_is_task - 判断这个se结构是不是一个task

如果一个se是gse,则gse->my_q指向对应的grq结构,一定不为空

#ifdef CONFIG_FAIR_GROUP_SCHED

//当支持组调度的时候,se结构可以表示一个task,也可以表示一个group的节点

//当这个se表示一个task时,se->mq结构为NULL

//当这个se表示一个用户组时,se->mq指向gse对应的grq,一定不为空

/* An entity is a task if it doesn't "own" a runqueue */

#define entity_is_task(se)            (!se->my_q)

#else

//当不支持组调度的时候,se结构始终对应着一个task结构

#define entity_is_task(se)            1

#endif

 

6.2 group_cfs_rq - 返回gse对应的grq结构

#ifdef CONFIG_FAIR_GROUP_SCHED

/* runqueue "owned" by this group */

static inline struct cfs_rq *group_cfs_rq(struct sched_entity *grp)

{

    //当支持组调度的时候,

    //若这个se对应一个group,则这个组对应的grq结构直接记录在my_q成员中

    //若这个se对应一个task,则此时的my_q成员直接为NULL

    return grp->my_q;

}

#else

/* runqueue "owned" by this group */

static inline struct cfs_rq *group_cfs_rq(struct sched_entity *grp)

{

    return NULL;

}

#endif

 

6.3 is_same_group - 判断两个se是否在同一个group中

/* Do the two (enqueued) entities belong to the same group ? */

static inline struct cfs_rq * is_same_group(

            struct sched_entity *se,

            struct sched_entity *pse)

{

    //如果两个se在同一个group中,则他们对应的grq一定一样

    //返回grq结构

    if (se->cfs_rq == pse->cfs_rq)

        return se->cfs_rq;

 

    //不在一个greop时返回NULL

    return NULL;

}

 

6.4 cfs_rq_of - 得到这个se被挂在哪个cfs_rq运行队列上

#ifdef CONFIG_FAIR_GROUP_SCHED

/* runqueue on which this entity is (to be) queued */

//当支持组调度的时候,se结构中会有一个cfs_rq结构,该结构用于指向se所属的cfs运行队列

static inline struct cfs_rq *cfs_rq_of(struct sched_entity *se)

{

    return se->cfs_rq;

}

#else

//若不支持组调度,则se是挂载在"对应的task所在的cpu上的rq结构"

static inline struct cfs_rq *cfs_rq_of(struct sched_entity *se)

{

    struct task_struct *p = task_of(se);        //由se得到对应的task_struct结构

    struct rq *rq = task_rq(p);                //得到这个task所在cpu的运行队列

 

    return &rq->cfs;

}

#endif

 

6.5 task_cfs_rq - 得到这个任务p被挂在哪个cfs_rq队列上

#ifdef CONFIG_FAIR_GROUP_SCHED

static inline struct cfs_rq *task_cfs_rq(struct task_struct *p)

{

    //当支持组调度的时候,虽然每个cpu只包含一个rq,但每个group都会对应

    //N个gse和grq结构,(N为cpu的个数,可通过gse->my_q找到对应的grq

    //结构),这个时候task_rq(p)->cfs指向最顶端的cfs_rq结构,并不是p所

    //在的cfs_rq结构,因为p有可能被挂在grq上,为了解决这个问题,在支持组

    //调度的时候,内核在se中新增一个cfs_rq成员,用于记录当前这个se所属的cfs_rq

    return p->se.cfs_rq;

}

#else

static inline struct cfs_rq *task_cfs_rq(struct task_struct *p)

{

    //当不支持组调度的时候,一个cpu上只有一个rq,在这个rq中只有一个cfs_rq,

    //此时,直接获取当前cpu上的运行队列runqueues,并从中提取出cfs_rq队列,

    //这就是任务p所在的cfs队列

    return &task_rq(p)->cfs;

}

#endif

 

6.6 task_of - 由se返回对应的task_struct结构

#ifdef CONFIG_FAIR_GROUP_SCHED

//当支持组调度的时候,需要判断这个se到底表示啥玩意

static inline struct task_struct *task_of(struct sched_entity *se)

{

    //首先判断这个se结构到底是表示一个task,还是表示一个group的节点

    //如果是一个group节点,则会产生一个警告,如果表示一个task,则返回这个task对应的task_struct

    SCHED_WARN_ON(!entity_is_task(se));

    return container_of(se, struct task_struct, se);

}

#else

//当不支持组调度的时候,直接返回se所属的task_struct结构

static inline struct task_struct *task_of(struct sched_entity *se)

{

    return container_of(se, struct task_struct, se);

}

#endif

 

6.7 rq_of - 已知运行队列cfs_rq,返回其所属的rq结构

#ifdef CONFIG_FAIR_GROUP_SCHED

/* cpu runqueue to which this cfs_rq is attached */

//当支持组调度的时候,因为cfs_rq可能包含在rq结构,也可能包含在se结构中

//所以不能通过container_of获取最外层的rq结构

//幸运的是,当开启组调度的时候,cfs_rq结构中会存在一个rq成员,

//这个成员就是用于标记这个cfs_rq属于哪一个运行队列rq

static inline struct rq *rq_of(struct cfs_rq *cfs_rq)

{

    return cfs_rq->rq;

}

#else

//当不支持组调度的时候,cfs_rq直接包含着rq结构中

static inline struct rq *rq_of(struct cfs_rq *cfs_rq)

{

    return container_of(cfs_rq, struct rq, cfs);

}

#endif

 

6.8 parent_entity - 返回se的父节点

#ifdef CONFIG_FAIR_GROUP_SCHED

//当支持组调度的时候,se是有层级关系的,此时返回父节点对应的se

static inline struct sched_entity *parent_entity(struct sched_entity *se)

{

    return se->parent;

}

#else

//不支持组调度的时候,se全部挂载运行队列上,没有层级关系,此时直接返回NULL

static inline struct sched_entity *parent_entity(struct sched_entity *se)

{

    return NULL;

}

#endif

 

6.9 for_each_sched_entity - 支持组调度的时候,逐层遍历祖先的gse结构

支持组调度时,调度实体是有层次结构的,例如当我们将进程加入的时候,同时要更新其父调度实体的调度信息

而非组调度情况下,就不需要调度实体的层次结构

#ifdef CONFIG_FAIR_GROUP_SCHED

/* Walk up scheduling entities hierarchy */

//当支持组调度的时候,se结构中会存在一个parent变量,

//这个变量指向"这个用户组在这个cpu上的节点对应的se结构"

//当只有两层的时候,这时的遍历实际就遍历了两个se结构,即当前se和group在当前cpu上的节点对应的se

//当有多层的时候,会层层向上遍历

#define for_each_sched_entity(se) \

        for (; se; se = se->parent)

#else

//当不支持组调度的时候,这个时候的遍历实际就遍历了一个se结构,即se本身

#define for_each_sched_entity(se) \

        for (; se; se = NULL)

#endif

 

6.10 find_matching_se - 层层向上,直到这两个se在同一个cfs_rq树中,也就是说在同一个组中

find_matching_se将两个se对齐到同一颗cfs_rq红黑树下,为什么要这么做呢?下面通过一个示例来分析,如下图所示,已知系统中共有4个task,其中task3和task4属于一个group,我们已知如下信息:

  • 各个task se/group se的load/shares值;
  • 一个调度周期为15ms;

 

则在经过一个调度周期之后,各个se的虚拟运行时间可经过运算得到具体的值,如下图所标记。

此时如果要对比task1和task3之间的虚拟运行时间,难道只直接对比吗?如果那样的话,因为task3_vtime < task1_vtime,所以task3将是最先运行的。

实际上并不是这样,在对比之前,首先task3要向上遍历到gse,然后对比task1_vtime和gse_vtime的时间,此时显然task1_vtime < gse_vtime,因此task1要先运行

可见上面两个方法会得到截然不同的结果,我们应该将group当成一个整体来进行调度

 

 

一般在对比两个se的虚拟运行时间的时候之前会执行该操作,因为当两个se不在同一个cfs_rq中的时候,也就是说不在同一颗红黑树的时候,对比着两个se的虚拟运行时间时没有任何意义的,因此在对比其虚拟运行时间之前,一定要将他俩搞到同一颗树上

 

当支持组调度的时候,se之间的拓扑关系如下图所示,由下面函数中的注释可知,该函数的作用是层层向上遍历,直到这两个se在同一个cfs_rq树中

  • 若该函数传入的se和pse分别对应下面图中的task1和task2结构,则经过find_matching_se后,se为gpa,pse对应gpb
  • 若该函数传入的se和pse分别对应下面图中的task1和task3结构,则经过find_matching_se后,se为gpa,pse对应gpb
  • 若该函数传入的se和pse分别对应下面图中的task2和task3结构,则经过find_matching_se后,se为task2,pse对应gpc

 

 

#ifdef CONFIG_FAIR_GROUP_SCHED

//当支持组调度的时候,se是有层级关系的,此时两个se可能会存在共同的祖先

static void find_matching_se(

            struct sched_entity **se,

            struct sched_entity **pse)

{

    int se_depth, pse_depth;

 

    /*

     * preemption test can be made between sibling entities who are in the

     * same cfs_rq i.e who have a common parent. Walk up the hierarchy of

     * both tasks until we find their ancestors who are siblings of common

     * parent.

     */

 

    /* First walk up until both entities are at same depth */

    //1.首先结果下面的两个while循环,使得两个se在"同一层"

    se_depth = (*se)->depth;

    pse_depth = (*pse)->depth;

 

    while (se_depth > pse_depth) {

        se_depth--;

        *se = parent_entity(*se);

    }

 

    while (pse_depth > se_depth) {

        pse_depth--;

        *pse = parent_entity(*pse);

    }

 

    //2.层层向上,直到这两个se在同一个group,变成了兄弟

    while (!is_same_group(*se, *pse)) {

        *se = parent_entity(*se);

        *pse = parent_entity(*pse);

    }

}

#else

//不支持组调度的时候,se全部挂载运行队列上,没有层级关系,此时也不会存在共同的祖先

static inline void find_matching_se(struct sched_entity **se, struct sched_entity **pse)

{

}

#endif

 

 

 

 

关注公众号不迷路:DumpStack

扫码加关注

本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2022年4月5日

tmmdh

这个人很懒,什么都没留下

打赏 点赞
< 上一篇

文章评论

取消回复

COPYRIGHT © 2022 dumpstack.cn. ALL RIGHTS RESERVED.

浙ICP备2022000966号