当前位置: 首页 > news >正文

group scheduling

为什么要有组调度

Linux是支持多用户、多session的。如果只是提供基于任务实体的调度,那么试想一下,用户A有1个任务,而用户B有3个任务。很显然,用户B将得到更多的CPU占用率。这在某种程度上来说是不公平。甚至极端情况下,用户B可能有100个任务,从而导致系统卡顿,而用户A只有一个任务,也不得不跟着卡顿。这就即为不合理了。这就诞生了group scheduling,即将每个用户(或者session)的所有任务归为一组,group作为调度单位,去竞争cpu份额。这样就会更加合理!

如何进行组调度

数据结构关系

描述这种group的数据结构名为"task_group",属于cgroup架构的cpu子系统。它既不是同一进程的线程构成的thread group,也不是load balance中以CPU为单位的sched_group,它是task group

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;

#endif

#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity        **rt_se;
struct rt_rq                **rt_rq;
struct rt_bandwidth        rt_bandwidth;

#endif

struct task_group     *parent;
struct cfs_bandwidth  cfs_bandwidth;
...
};

  1. 从调度的层面,task group作为一个调度单位,和其他的task/task group一起参与CPU份额的分配,因此task group也以"sched_entity"形式参与调度,由"task_group->se[cpu]"指向。
  2. 以CFS为例,当这个task group因为vruntime最小被选中时,还需要从这个group中再挑选其中vruntime最小的task,所以这个group也形成了一个runqueue,被"task_group->cfs_rq[cpu]"指向。

注意:

1、这里"se" 和"cfs_rq" 都是以二级指针的形式出现的,实际代表结构体数组,数组的长度为CPU的数目,这是因为一个task group往往包含多个runable的任务,而这些任务可能在多个CPU上运行

2、调度实体有权重的概念,以权重的比例分配CPU时间。用户组同样有权重的概念,share就是task_group的权重

下图展示了cgroup目录和task group数据结构之间的关系:

这里父task group的children成员和子task group的sibling成员是个什么关系?其实,子task_group在创建好后,online的时候,会以sibling节点加入父task_group的children链表中,形成一个兄弟链(sibling)。对子tg来说是sibling link;而对父tg来说,则都是children!链表建立之后,就方便task_group的遍历了。

下图展示了task group、schedule entity以及cfs_rq之间的关系:

上图左,黄色椭圆表示task group;绿色的方框表示具体task的entity。

上图右,灰色椭圆表示cfs_rq,蓝色椭圆表示task groupentity,其有my_q成员指向自己task group对应该cpu的rq。这可以区别于普通taskentity(其my_q成员为NULL

从上图可以看出,在task group存在的情况下,cfs_rq形成了多级级联的hierarchy。调度器pick_next_task的时候,如果选中了task group的entity,则还会继续递归的从其owned的rq中挑选具体的task来执行。参照代码:

  1. static struct task_struct *
  1. pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
  1. {
  1.   struct cfs_rq *cfs_rq = &rq->cfs; /* 1 */
  2.   struct sched_entity *se;
  3.   struct task_struct *p;
  1.   put_prev_task(rq, prev);
  1.   do {
  1.     se = pick_next_entity(cfs_rq, NULL); /* 2 */
  1.     set_next_entity(cfs_rq, se);
  1.     cfs_rq = group_cfs_rq(se); /* 3 */
  1.   } while (cfs_rq); /* 4 */
  1.   p = task_of(se);
  1.   return p;
  1. }

/* runqueue "owned" by this group */

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

{

    return grp->my_q;

}

The following figure shows the parent relationship of schedule entities:

用户组的权重

每一个进程都会有一个权重,CFS调度器依据权重的大小分配CPU时间。同样task_group也不例外,前面已经提到使用share成员记录。Task group的share值,默认设置为1024(NICE_0_LOAD):

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

{

      ……

    tg->shares = NICE_0_LOAD;

      ……

不过,也可以通过用户态手动修改(其中A/B为级联的两层task group):

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

注意:root task group的share值(“/sys/fs/cgroup/cpu/cpu.shares”)不能通过上述方式修改!

按照前面的举例,系统有2个CPU,task_group中势必包含两个group se和与之对应的group cfs_rq。这2个group se的权重按照比例分配task_group权重。如下图所示:

 CPU0上group se下有2个task se,权重和是3072。CPU1上group se下有3个task se,权重和是4096task_group权重是1024。因此,CPU0上group se权重是439(1024*3072/(3072+4096)),CPU1上group se权重是585(1024-439)

注意:这里的计算group se权重的方法是最简单的方式示例,代码中实际计算公式是考虑每个group cfs_rq的负载贡献比例,而不是简单的考虑权重比例。

calc_group_shares

前面章节举例说到group se的权重计算是根据权重比例计算。但是,实际的代码并不是。当我们dequeue task、enqueue task以及task tick的时候会通过update_cfs_group()函数更新group se的权重信息。而核心的工作则在calc_group_shares()函数中。该函数内使用的计算权重的算法颇为奇怪,内核也给出了长篇的注释,用来解释这个算法,如下:

/*

 * All this does is approximate the hierarchical proportion which includes that

 * global sum we all love to hate.

 *

 * That is, the weight of a group entity, is the proportional share of the

 * group weight based on the group runqueue weights. That is:

 *

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

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

 *                       \Sum grq->load.weight

 *

 * Now, because computing that sum is prohibitively expensive to compute (been

 * there, done that) we approximate it with this average stuff. The average

 * moves slower and therefore the approximation is cheaper and more stable.

 *

 * So instead of the above, we substitute:

 *

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

 *

 * which yields the following:

 *

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

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

 *                             tg->load_avg

 *

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

 *

 * That is shares_avg, and it is right (given the approximation (2)).

 *

 * The problem with it is that because the average is slow -- it was designed

 * to be exactly that of course -- this leads to transients in boundary

 * conditions. In specific, the case where the group was idle and we start the

 * one task. It takes time for our CPU's grq->avg.load_avg to build up,

 * yielding bad latency etc..

 *

 * Now, in that special case (1) reduces to:

 *

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

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

 *                         grp->load.weight

 *

 * That is, the sum collapses because all other CPUs are idle; the UP scenario.

 *

 * So what we do is modify our approximation (3) to approach (4) in the (near)

 * UP case, like:

 *

 *   ge->load.weight =

 *

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

 *     ---------------------------------------------------         (5)

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

 *

 * But because grq->load.weight can drop to 0, resulting in a divide by zero,

 * we need to use grq->avg.load_avg as its lower bound, which then gives:

 *

 *

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

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

 *                             tg_load_avg'

 *

 * Where:

 *

 *   tg_load_avg' = tg->load_avg - grq->avg.load_avg +

 *                  max(grq->load.weight, grq->avg.load_avg)

 *

 * And that is shares_weight and is icky. In the (near) UP case it approaches

 * (4) while in the normal case it approaches (3). It consistently

 * overestimates the ge->load.weight and therefore:

 *

 *   \Sum ge->load.weight >= tg->weight

 *

 * hence icky!

 */

上面注释中,有一些简写需要说明一下:

  • tg——》task_group
  • ge——》task_group->se[cpu]
  • grq——》task_group->cfs_rq[cpu]
  • \Sum 表示percpu的数据求和

其实,注释里第(1)个公式正是上一章节计算group se权重使用的公式。后面出于方便计算的目的,逐步演化为目前代码里使用的第(6)个公式。演化的过程及依据,也在英文注释里说明了,这里就不再画色添足的翻译了。其核心,就是用cfs_rq->avg.load_avg来近似替代cfs_rq->load.weight,进而也就不用计算task_group percpu cfs_rq的权重之和了,而是直接使用task_group->load_avg变量!

其他说明:

  1. task group中的任务还可能具有不同的属性,其中一些是普通任务,另一些是实时任务,所以"task_group"中的"se"可能位于cfs_rq上,也可能位于rt_rq上。在RT调度中,总是选择优先级最高的se来执行,那task group作为se,其优先级该如何界定呢?原则是选择group中优先级最高的任务的priority作为group的priority
  2. "/proc/sys/kernel/"目录下两个参数"sched_rt_period_us" 和"sched_rt_runtime_us" ,默认值分别是1000000和950000,单位是us,因此分别是1s和0.95s,意思是在1s的时间内,实时任务至多可运行0.95s,剩下的0.05s则留给普通任务。这样设计主要是为了防止万一有实时任务失心疯地占着CPU不放,至少还可以通过普通任务的运行,尝试从这种异常中恢复。
http://www.dtcms.com/a/424761.html

相关文章:

  • 工业园区门户网站建设方案网站建设极地网
  • 网站建设网站系统选择做宣传册参考的网站
  • 自治区建设厅网站哪些网站做的比较好的
  • jsp网站开发技术难点建设永久网站
  • 做教育的网站有哪些中国怎么进fiverr网站做任务
  • 门户网站制作企业wordpress怎么做表格
  • 从索引失效到毫秒级响应——SQL 优化实战案例:从慢查询到高性能的完整指南之电商大促篇
  • 内联函数(inline)详解
  • 记力扣2271.毯子覆盖的最多白色砖块数 练习理解
  • MATH-500:大模型数学推理能力评估基准
  • 微商城网站建设方案网站宽度
  • 网站显示乱码怎么办啊微信小程序如何推广
  • iis 做网站市场调研报告怎么做
  • 免费搭建商业网站广州市行政区划图
  • 如何做宣传推广的网站链接网站建设标准 方案书
  • 温州网站建设团队利用百度网盘自动播放做视频网站
  • 如何做新网站保留域名展馆设计论文
  • 【机器学习】朴素贝叶斯法
  • 「React实战面试题」:React.memo为什么失效了?
  • 尼罗发表小说做的的网站是哪个抖音小店代运营
  • 手机网站用什么语言开发小影wordpress
  • 天津项目网站建设京东网站建设有哪些优点
  • 韩国在中国做美食的视频网站有哪些cvm可以做网站服务器吗
  • 做糕点的网站WordPress 主页分页
  • 做男装去哪个网站好网页设计包含的内容
  • 重庆免费网站建设自己做的网站可以百度推广吗
  • Java:代码块
  • 五维论推普朗克公式和质能方程和多普勒效应
  • 动画网站模板诸暨 外贸网站建设
  • 可视化信息 网站室内设计方案