一、 前言
带宽限制是操作系统内核中一种至关重要的机制,其核心作用在于有效控制实时任务(RT tasks)的CPU使用量。实时任务通常被赋予高优先级,以确保实时任务能够迅速且高效的执行,这对于任务的及时完成至关重要。
然而,如果这些CPU资源密集型的实时任务被允许无限制地占用CPU资源,它们可能会过度消耗系统资源,从而严重影响到低优先级的普通任务的执行。这种情况下,低优先级任务可能会因资源被抢占而无法获得必要的处理时间,进而导致整个系统的性能显著下降,甚至出现不稳定的情况。
为了解决这一问题,Linux kerenl 实施RT带宽限制机制。通过这一机制,可以有效的平衡实时任务与普通任务之间的CPU资源分配,从而确保系统整体的高效运行和稳定性。
内核代码版本:linux-6.11.3
二、 RT带宽限制介绍
所谓带宽限制,就是允许任务在一个调度周期内运行的最大时间。RT调度器的带宽控制的核心思想是:限制CPU上可运行的RT任务在检测周期内占用CPU的时长不能超过运行门限,检测周期(sched_rt_period_us)和运行门限(sched_rt_runtime_us)是RT带宽控制的两个可配置参数。
RT调度类的带宽控制默认在CPU运行队列上生效。支持组调度后,可以扩展到任务组的运行队列上生效。

1.在创建实时任务组(RT task group)时,系统会调用 init_rt_bandwidth 函数初始化带宽控制参数。该函数的核心工作是将rt_bandwidth结构体的相关字段进行初始化:将时间周期rt_period和运行时间限制rt_runtime,都设置成默认值;这些参数都是和rt_rq绑定的,如果不开启CONFIG_RT_GROUP_SCHED,则每个CPU上只有一个rt_rq,参数是全局控制的;如果开启了CONFIG_RT_GROUP_SCHED,则除了root_task_group,所有新增的task_group都有rt_rq参数,可独立控制。
2. 用户态可以通过操作/sys/fs/cgroup/cpu下对应的节点进行动态设置rt_period和rt_runtime,最终调用的函数是tg_set_rt_bandwidth,该函数会检查所有子任务组的 rt_runtime 总和是否超过当前任务组的限制。
3. 当实时调度实体(RT entity)被加入运行队列时,系统会触发带宽控制流程,具体步骤如下:比较当前 rt_rq 的累计运行时间 rt_time 与配置的 rt_runtime,若 rt_time 超过 rt_runtime,则触发限流操作。调用 enqueue_rt_entity 将任务加入 rt_rq,该函数会调用 start_rt_bandwidth,为当前任务组启动一个高精度定时器。定时器到期时,系统调用 do_sched_rt_period_timer 函数,开始新一轮的带宽统计和限流检查。
4. do_sched_rt_period_timer函数是实时任务组带宽控制的核心,主要功能包括:通过判断该RT运行队列的累计运行时间rt_time与设置的限制运行时间rt_runtime之间的大小关系,以确定是否执行限流的操作。如果已经进行了限流操作,会调用balance_time来在多个CPU之间进行时间配额调整,简单来说就是从其他 CPU 的 rt_rq 中匀出部分时间配额,增加到当前 rt_rq 的 rt_runtime。相应减少其他 CPU 的 rt_runtime,确保全局时间配额不超限。
三、 RT带宽限制相关结构体

-
struct rt_bandwidth
内核使用rt_bandwidth结构体来限制RT任务的运行时间。
struct rt_bandwidth { /* nests inside the rq lock: */ //自旋锁,用于保护对实时运行时(rt_runtime)的访问。 raw_spinlock_t rt_runtime_lock; //表示实时调度周期的长度 ktime_t rt_period; //表示在当前实时周期内,实时任务还可以使用的CPU时间的总量。当实时任务执行时, //这个值会相应的减少。一旦rt_runtime耗尽,实时任务将被被调度器挂起,直到下一 //个实时周期开始或额外的带宽被分配给它们。 u64 rt_runtime; //时一个高精度定时器,用于管理实时周期的结束,当定时器触发时,表示一个实时周期 //结束,此时会重置rt_runtime并重新启动定时器,以开始下一个周期。 struct hrtimer rt_period_timer; //表示实时周期是否处于激活状态。 //如果为1,表示当前有一个有效的实时周期正在进行中。 //如果为0,则表示当前没有实时周期在运行。 unsigned int rt_period_active; };在单CPU环境中,rt_bandwidth机制用于严格限制实时任务在rt_period指定周期内所能占用的CPU运行时间,确保这一时间不超过rt_runtime所设定的上限。这一机制有效防止了实时任务过度占用CPU资源,从而保障了系统整体的稳定性和响应性。
在拥有多个CPU核心(SMP,Symmetric Multiprocessing)的复杂环境中,rt_bandwidth的限制方式则有所不同。此时,它不再仅仅关注单个CPU核心上的任务运行时间,而是着眼于整个系统中所有实时任务在rt_period周期内的CPU占用时间比例。具体来说,如果rt_runtime设置950000微秒,而rt_peroid位1000000微秒(即1秒),那么在这个1秒的周期内,系统中所有实时任务在所有CPU核心上的总占用时间不得超过950000微秒,也就是说,它们的CPU占用率总和不应超过95%。
进一步地,在实时任务组调度功能被启用的情况下,带宽限制变得更加的细致和灵活。每个实时任务组都会拥有一个独立的rt_bandwidth设置,这一设置专门用于限制该组内所有实时任务的运行时间。这意味着,不同任务组之间的实时任务可以拥有不同的带宽限制,从而实现了更为精细化的资源管理和分配。
-
struct rt_rq
内核中判断实时任务运行时间是否超出带宽限制的核心思想就是判断这个运行队列rt_rq的运行时间是否超过了额定的运行时间。而运行时间与额定时间放在struct_rt_rq运行队列这个结构中,我们来看一下这个结构中与带宽控制相关的具体实现:/* Real-Time classes' related field in a runqueue: */ struct rt_rq { ...... //此标志用来指示是否对实时任务施加了调度限制,当设置为1时,表示已经启用了对实时任务 //的调度限制。此时实时任务的运行时间已经达到了其额定的最大运行时间。 int rt_throttled; //rt_rq队列中的所有调度实体已经消耗的cpu时间总和。在每个调度周期结束时被重置。 u64 rt_time; //在一个调度周期内,rt_rq队列允许的最大CPU时间。实时任务的运行时间一旦达到或超 //过这个限制,就会被节流,以确保不会过多占用CPU资源。 u64 rt_runtime; /* Nests inside the rq lock: */ ...... //rt_rq所属的就绪队列rq struct rq *rq; //指向rt_rq所属的任务组(task_group),在支持任务组调度的系统中,任务组允许将多个任 //务组织在一起,并对它们进行统一的资源管理和限制。 struct task_group *tg; }; -
struct task_group
当一个调度组(task_group)被创建时,比如在cgroup中创建一个cpu子系统时就会创建一个task_group。内核会为该新的调度组在每一个CPU上分配并初始化一个运行队列(struct rt_rq)。而所有的实时调度实体都是挂在各自的运行队列rt_rq上的。/* task group related information */ struct task_group { //任务组与cgroup子系统交互的基础 struct cgroup_subsys_state css; ...... #ifdef CONFIG_RT_GROUP_SCHED //一个组在每个cpu上都有一个代表这个层级组的rt_se struct sched_rt_entity **rt_se; //一个组在每个CPU上都有一个rt_rq(实时运行队列),里面挂着属于这个组的实时调度实体 struct rt_rq **rt_rq; //限制实时任务的带宽使用 struct rt_bandwidth rt_bandwidth; #endif ...... //用于将任务组链接到相关链表中的链表头 struct list_head list; //指向父任务组的指针,构建任务组的层级结构 struct task_group *parent; // 兄弟进程组链表 struct list_head siblings; // 子进程组链表 struct list_head children; ...... //CFS的带宽控制结构,用于限制CFS任务的带宽使用 struct cfs_bandwidth cfs_bandwidth; };
四、 组调度层级结构
我们可以想这么一个问题,kernel为什么引入组调度?在Linux操作系统中,传统的调度算法主要是基于任务的,这意味着调度器会根据任务的优先级、运行状态等因素来分配CPU资源。然而,这种调度方式在某些情况下可能会引发用户之间的资源分配不公。例如,当用户A拥有大量任务同时运行时(假设10个任务),而用户B仅有少量任务在运行(比如只有1个任务),且这些任务的优先级相同时,按照任务调度的方式,用户A的任务将集体占用更多的CPU资源,导致用户A实际上获得了比用户B更多的CPU时间。
从任务调度的角度来看,这种分配方式是公平的,因为每个任务都根据其优先级获得了相应的资源。但是,如果从用户的角度来看,这种分配方式就显得不那么合理了。毕竟,用户B的任务虽然少,但同样需要保证自己的任务能够获得足够的CPU时间来高效执行。
因此,为了在保证任务调度公平性的同时兼顾用户之间的资源分配公平,Linux操作系统引入了组调度(CGroup Scheduling)机制。通过将用户A的任务归为一个组(GroupA),用户B的任务归为另一个组(GroupB),调度器可以确保这两个组获得的CPU资源是相等的。这样,即使用户A的任务数量多于用户B,他们之间的资源分配也能保持相对的公平。
组调度是Linux操作系统中的一个可选特性,其开关为CONFIG_CGROUP_SCHED。当该特性被开启后,Linux会基于CGroup(控制组)机制实现一个名为“cpu”的资源控制器。这个控制器专门负责管理和分配CPU时间资源。目前,Linux的组调度功能主要支持RT(实时)调度类和CFS(完全公平调度器)调度类。要启用完整的组调度功能,至少需要打开CONFIG_RT_GROUP_SCHED(实时组调度开关)或CONFIG_FAIR_GROUP_SCHED(公平组调度开关)中的一个。

- 在操作系统中,task_struct作为调度实体被加入到运行队列(rq)中。在支持组调度的情况下,运行队列(rq)中不仅包含代表单个任务的调度实体(task_se),还可以包含代表任务组的调度实体(group_se)。
- 根据rt优先级调度算法选出需要调度的最优实体,如果筛选出来的最优调度实体为一个task,直接运行即可。
- 若筛选出来的最优调度实体为一个任务组,则在该任务组范围内继续使用rt优先级调度算法选出该进程组内的最优调度实体。直到筛选出来的调度实体为一个单独可运行的task为止。

- 在操作系统中,tg->re_se代表任务组在实时运行队列(rt_rq)中的调度实体。与单个任务在同一时刻只能位于一个运行队列(rq)中不同,任务组由多个任务组成,这些任务可能分布在不同的CPU上执行。因此,为了在每个CPU上都能对任务组进行有效的调度管理,系统会为每个CPU上的任务组创建一个对应的调度实体对象。
- 根分组没有代表自己的调度实体,即root_task_group.rt_se数组均为NULL。这是因为调度实体的作用是将任务或任务组挂到运行队列中,以便进行调度管理。然而,根分组作为所有任务组的顶层容器,它不直接参与调度过程,因此,它本身并不需要被挂接到任何特定的运行队列中。
- 不支持组调度时,每个CPU上只有一个rq,该rq管理了该cpu上所有等待运行的task_se。支持组调度后,每个任务组也用运行队列管理组内的调度实体(属于该任务组的task_se和group_se),而且也是每个cpu一个队列。tg->rt_rq管理组内的rt调度实体。特殊的,
- 根分组的运行队列等同于CPU的rq。
- 在操作系统中,task_group对象构成了一个倒立的树形结构。root_task_group为树根,其tg->parent为NULL,以tg->children为链表头将下一层的task_group对象组织成链表(tg->siblings)。非根分组的tg->parent指向上一层的task_group对象。
- 系统中的所有的task_group对象(tg->list)会被组织到全局链表task_groups中。
- rt_se->parent指向所属任务组的group_se。
- rt_se->rt_rq指向所属任务组的tg->rt_rq。
- tg->rt_se[cpu]->my_q == tg.rt_rq[cpu],tg->rt_rq[cpu]队列挂着的所有任务都看成一个整体:tg->rt_se[cpu]。
- 对于task_se,任务就是最小的调度实体,所以其my_q为NULL。对于group_se,其内部还可以包含下一级的task_se和group_se,所以其my_q就是自己task_group中的tg->rt_rq。我们可以通过my_q字段是否为NULL来判断一个调度实体到底是task_se还是group_se。
五 、 RT带宽限制相关代码解析
5.1 root组带宽初始化
最顶层 的root组是所有task_group的祖先,在Linux内核中,root组通过全家变量struct task_group root_task_group定义。在内核启动初期,root组由sched_init()函数进行初始化,具体过程如下:
-
调用链
start_kernel() //内核启动的入口函数,负责初始化内核的各个子系统。
->sched_init() //调度子系统的初始化函数,完成调度相关的全局变量和结构体的初始化。 -
sched_init()中带宽相关的工作
-
带宽初始化
对于实时任务(RT任务),需要初始化两个带宽结构:
全局带宽结构:struct rt_bandwidth def_rt_bandwidth
root组的带宽结构: root_task_group.rt_bandwidth
初始化相关代码如下:def_rt_bandwidth.rt_period = sysctl_sched_rt_period; def_rt_bandwidth.rt_runtime = sysctl_sched_rt_runtime; root_task_group.rt_bandwidth.rt_period = sysctl_sched_rt_period; root_task_group.rt_bandwidth.rt_runtime = sysctl_sched_rt_runtime;参数说明:
sysctl_sched_rt_period 和 sysctl_sched_rt_runtime 是内核定义的全局变量,分别表示带宽周期和运行时间额度。
默认值:
sysctl_sched_rt_period = 1000000 微秒(1 秒)
sysctl_sched_rt_runtime = 950000 微秒(0.95 秒)
可以通过以下文件查看或调整:/proc/sys/kernel/sched_rt_period_us /proc/sys/kernel/sched_rt_runtime_us -
调度实体和运行队列初始化
root组的调度实体和运行队列通过以下成员管理:
struct sched_rt_entity **rt_se:调度实体数组,每个元素对应一个 CPU。
struct rt_rq **rt_rq:运行队列数组,每个元素对应一个 CPU。
root_task_group 的 rt_rq 和 rt_se 需要与每个 CPU 的就绪队列(rq)中的成员关联。// 将root_task_group对象加入全局的task_groups链表中 list_add(&root_task_group.list, &task_groups); INIT_LIST_HEAD(&root_task_group.children); INIT_LIST_HEAD(&root_task_group.siblings); rq[cpu]->rt.rt_runtime = def_rt_bandwidth.rt_runtime; // 设置运行队列的运行时间额度为 0.95 秒 //在init_tg_rt_entry()函数中,我们将传参带入得 rt_rq->rq = rq //将运行队列所在的rq指向cpu所在的rq rt_rq->tg = root_task_group; // 将运行队列的 task_group 指向 root_task_group root_task_group->rt_rq[cpu] = rq[cpu]->rt; // root_task_group 的 rt_rq 指向对应 CPU 的运行队列 root_task_group->rt_se[cpu] = NULL; // 初始化时,调度实体为空作用:
root_task_group对象被加入全局的task_groups链表中
运行队列所在的rq指向cpu所在的rq
每个 CPU 的就绪队列(rq)中的实时运行队列(rq->rt)被关联到 root_task_group。
每个 CPU 的运行队列的运行时间额度初始化为 def_rt_bandwidth.rt_runtime,即 0.95 秒。
-
5.2 task_group
cpu控制器的CGroup接口定义如下:
struct cgroup_subsys cpu_cgrp_subsys = {
.css_alloc = cpu_cgroup_css_alloc, // 分配新cgroup时回调(分配task_group对象)
.css_online = cpu_cgroup_css_online, // cgroup在线(对系统可见)前的回调
.css_released = cpu_cgroup_css_released, // cgroup即将被删除前的回调
.css_free = cpu_cgroup_css_free, // cgroup内存释放前的回调
.css_extra_stat_show = cpu_extra_stat_show, // 显示额外统计信息(如cpu.stat)
.css_local_stat_show = cpu_local_stat_show, //显示本地统计信息(如cpu.stat)
#ifdef CONFIG_RT_CROUP_SCHED
.can_attach = cpu_cgroup_can_attach, // 检查任务是否允许加入该cgroup
#endif
.attach = cpu_cgroup_attach, // 任务加入cgroup后的回调
.legacy_cftypes = cpu_legacy_files, // 传统接口属性文件(如cpu.shares)
.dfl_cftypes = cpu_files, // 默认属性文件列表
.early_init = true, // 早期初始化标志
.threaded = true, // 支持线程粒度控制
};
下面我们将对一下关键回调函数进行介绍
5.2.1 创建新分组
一个task_group分组的创建是通过sched_create_group()函数来实现的,在内核中,当用户态通过mkdir在cpu控制器下创建一个CPU子系统控制组时,都会调用sched_create_group()来创建一个task组。
接下来,我们跟随sched_create_group()函数的实现,了解一个task_group是如果创建的,rt_bandwidth是如何初始化的,以及一个新的rt_rq的额定时间是如何设置的。
/* allocate runqueue etc for a new task group */
struct task_group *sched_create_group(struct task_group *parent)
{
struct task_group *tg;
// 分配 task_group 结构
tg = kmem_cache_alloc(task_group_cache, GFP_KERNEL | __GFP_ZERO);
if (!tg)
return ERR_PTR(-ENOMEM);
// fair 调度的情况
if (!alloc_fair_sched_group(tg, parent))
goto err;
// 实时调度相关的task_group初始化
if (!alloc_rt_sched_group(tg, parent))
goto err;
return tg;
err:
// 释放资源
sched_free_group(tg);
return ERR_PTR(-ENOMEM);
}
步骤总结:
- task_group 的创建: 通过kmem_cache_alloc() 分配内存,并初始化相关调度组。
- 公平调度(CFS)初始化: 调用 alloc_fair_sched_group()。
- 实时调度(RT)初始化: 调用 alloc_rt_sched_group(),完成 rt_bandwidth 的初始化和 rt_rq 额定时间的设置。
- 错误处理: 在初始化失败时,释放已分配的资源,避免内存泄漏。
RT调度类的alloc_rt_sched_group实现:
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;
// 为 task_group 对象分配运行队列指针数组和调度实体指针数组,准备nr_cpu_ids个rt_rq空间
tg->rt_rq = kcalloc(nr_cpu_ids, sizeof(rt_rq), GFP_KERNEL);
tg->rt_se = kcalloc(nr_cpu_ids, sizeof(rt_se), GFP_KERNEL);
//将tg->rt_period初始化为def_rt_bandwidth.rt_period,tg->rt_runtime初始化为 0
init_rt_bandwidth(&tg->rt_bandwidth,
ktime_to_ns(def_rt_bandwidth.rt_period), 0);
// 遍历所有可能的 CPU,为每个 CPU 分配和初始化调度实体和运行队列
for_each_possible_cpu(i) {
// 为每个 CPU 分配运行队列和调度实体
rt_rq = kzalloc_node(sizeof(struct rt_rq), GFP_KERNEL, cpu_to_node(i));
rt_se = kzalloc_node(sizeof(struct sched_rt_entity), GFP_KERNEL, cpu_to_node(i));
// 初始化运行队列
init_rt_rq(rt_rq, cpu_rq(i));
//设置rt_rq->rt_runtime = 0
rt_rq->rt_runtime = tg->rt_bandwidth.rt_runtime;
//将新分配的rt_rq、rt_se与tg以及parent组进行关联
init_tg_rt_entry(tg, rt_rq, rt_se, i, parent ? parent->rt_se[i] : NULL);
}
...
return 0; // 返回 0 表示成功
}
函数alloc_rt_sched_group()的主要任务就是分配tg->rt_rq和tg->rt_re这两个指针数组在各个cpu上对应的调度实体rt_se和运行队列rt_rq结构体实例,并初始化这些结构体。
在组调度场景下,可观察到新创建任务组(tg)的tg->rt_bandwidth.rt_runtime以及组内各CPU运行队列(rt_rq)的rt_runtime均初始化为0。
# mount -t cgroup cpu -o cpu /cgroup/cpu /* 创建cpu子系统 */
# mkdir /cgroup/cpu/app /* 创建一个app子组 */
查看rt_runtime的值
新创建的子组中tg->rt_bandwidth.rt_runtime为0
cat /cgroup/cpu/app/cpu.rt_runtime_us
设置rt_runtime的值
tg->rt_bandwidth.rt_runtime和组内各个cpu上的运行队列的额定时间tg-rt_rq[cpu]->rt_runtime都设置为我们写入的值。
echo xxx > /cgroup/cpu/app/cpu.rt_runtime_us
void init_tg_rt_entry(struct task_group *tg, struct rt_rq *rt_rq,
struct sched_rt_entity *rt_se, int cpu,
struct sched_rt_entity *parent)
{
struct rq *rq = cpu_rq(cpu); // 获取当前 CPU 的运行队列
// 初始化运行队列的字段
rt_rq->highest_prio.curr = MAX_RT_PRIO; // 设置最高优先级为 MAX_RT_PRIO
rt_rq->rt_nr_boosted = 0; // 初始化被提升的实时任务计数为 0
rt_rq->rq = rq; // 绑定到当前 CPU 的运行队列
rt_rq->tg = tg; // 绑定到所属的 task_group
// 将运行队列和调度实体存储到 task_group 中
tg->rt_rq[cpu] = rt_rq;
tg->rt_se[cpu] = rt_se;
// 如果调度实体为空,直接返回
if (!rt_se)
return;
// 设置调度实体的运行队列指针
if (!parent)
rt_se->rt_rq = &rq->rt; // 如果没有父级,绑定到 CPU 的默认实时运行队列
else
rt_se->rt_rq = parent->my_q; // 如果有父级,绑定到父级的队列
// 设置调度实体的其他字段
rt_se->my_q = rt_rq; // 绑定到当前运行队列
rt_se->parent = parent; // 设置父级调度实体
INIT_LIST_HEAD(&rt_se->run_list); // 初始化运行列表
}
函数alloc_rt_sched_grop()的主要作用就是分配tg->rt_rq和tg_se这两个指针数组在各个cpu上对应的调度实体rt_se和运行队列rt_rq结构体实例,并初始化这些结构体。
注意:该阶段的初始化只是创建了task_group对象并对其进行了初始化,还没有将其加入到系统的task_groups链表和task_group树中。
5.2.2 新分组关联到系统
这是完成用户态mkdir的第二部,该流程结束后,用户态就可以看到创建的新分组了。
static int cpu_cgroup_css_online(struct cgroup_subsys_state *css)
{
struct task_group *tg = css_tg(css);
struct task_group *parent = css_tg(css->parent);
// 仅对非根任务组执行初始化操作
if (parent)
sched_online_group(tg, parent);
...
}
void sched_online_group(struct task_group *tg, struct task_group *parent)
{
unsigned long flags;
spin_lock_irqsave(&task_group_lock, flags);
// 1. 将任务组加入全局任务组链表
list_add_rcu(&tg->list, &task_groups);
// 2. 确保父任务组存在(根任务组已存在)
WARN_ON(!parent);
// 3. 将任务组加入父任务组的子任务组链表
tg->parent = parent;
INIT_LIST_HEAD(&tg->children);
list_add_rcu(&tg->siblings, &parent->children);
spin_unlock_irqrestore(&task_group_lock, flags);
// 4. 初始化 CFS 调度类相关内容(此处省略)
online_fair_sched_group(tg);
}
注意事项
-
任务组的初始状态
-
新创建的任务组是一个空分组,尚未包含任何调度实体(scheduling entity)。
-
空分组的group_se不会被加入到上一级任务组的运行队列中。
-
全局链表与树结构
-
全局任务组链表: 通过task_groups维护所有任务组的链接关系。
-
任务组树: 通过parent和children链表维护任务组的层次结构。
5.2.3 添加任务到分组
新创建的任务和父任务属于同一个分组,大多数字段可以直接继承,不需要再进行特殊设置。
static void sched_cgroup_fork(struct task_struct *task)
{
unsigned long flags;
//获取的pi_lock锁
raw_spin_lock_irqsave(&p->pi_lock, flags);
#ifdef CONFIG_CGROUP_SCHED
if (1) {
struct task_group *tg;
// 从kargs中获取cpu控制组(cgroup)的子系统,并转换为task_group结构
tg = container_of(kargs->cset->subsys[cpu_cgrp_id],
struct task_group, css);
//为任务设置自动分组(autogroup)的调度组,(继承父任务的分组)
tg = autogroup_task_group(p, tg);
// 设置任务的调度组
p->sched_task_group = tg;
}
#endif
// 处理RSEQ(Restartable Sequences)相关迁移
rseq_migrate(p);
//设置任务的CPU
__set_task_cpu(p, smp_processor_id());
// 如果调度类有task_fork回调函数,则调用它
if (p->sched_class->task_fork)
p->sched_class->task_fork(p);
// 释放之前获取的pi_lock锁
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
}
新创建的任务(如通过fork()或clone()系统调用生成)在调度分组(task group)的归属上,遵循以下规则:
- 分组一致性: 新任务默认继承其父任务的调度分组,即与父任务属于同一个任务组(task group)。
- 字段继承机制: 新任务的调度相关字段(如分组标识、权重、共享资源等)大多可直接从父任务继承,无需进行额外的特殊设置或初始化。
- 简化设计目的: 通过字段继承机制,避免了为每个新任务单独配置调度参数的复杂操作,同时确保了任务内任务的一致性。
5.2.4 RT组调度之任务入队列
调度器调度执行的核心对象是任务(task),因此任务入队列操作的核心是针对调度实体。组调度通过分层管理运行队列(rt_rq)中的调度实体,实现任务分组的统一管理。
static void enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
struct sched_rt_entity *rt_se = &p->rt; // 获取任务的实时调度实体
enqueue_rt_entity(rt_se, flags); // 将调度实体加入运行队列
...
}
static void enqueue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
struct rq *rq = rq_of_rt_se(rt_se); // 获取调度实体所属的运行队列
// 按照"先出队,再入队"的方式完成调度实体的入队
dequeue_rt_stack(rt_se, flags);
for_each_sched_rt_entity(rt_se)
__enqueue_rt_entity(rt_se, flags);
enqueue_top_rt_rq(&rq->rt);
}
核心逻辑:
- 先出队: 通过dequeue_rt_stack清除调度实体在各层运行队列中的残留状态。
- 逐层入队: 从底层到顶层,将调度实体依次加入对应层的运行队列。
- 更新统计信息: 刷新各层运行队列的任务个数统计信息。
我们用文章上面的图来说明一下,假设我们要将GroupC中的一个任务入队列,应该要完成如下操作:
- 将任务的task_se加入GroupC分组的rt_rq中,并更新GroupC分组中运行队列中的任务个数的统计信息。
- 如果GroupC分组的group_se尚未加入根分组的rt_rq中,则将其加入;然后刷新根分组运行队列中的任务个数统计信息。
总的来说,就是要自底向上的将每一层的调度实体都加入上一层task_group的运行队列中,并刷新各层运行队列中的任务个数统计信息。RT调度类在实现上述操作时,使用了"先出队,再入队"的方式:
-
先出队
按照从顶层到底层的顺序将各层task_group的调度实体出队,由于是自顶向下进行的,随着出队过程,各层运行队列上维护的任务个数统计信息都会被刷新为删除该task_group的状态。static void dequeue_rt_stack(struct sched_rt_entity *rt_se, unsigned int flags) { struct sched_rt_entity *back = NULL; // 构建从顶层到底层的调度实体链表 for_each_sched_rt_entity(rt_se) { rt_se->back = back; back = rt_se; } // 获取最后一个实体所在的运行队列中的实时任务数 rt_nr_running = rt_rq_of_se(back)->rt_nr_running; // 逐层出队调度实体 for (rt_se = back; rt_se; rt_se = rt_se->back) { if (on_rt_rq(rt_se)) __dequeue_rt_entity(rt_se, flags); } // 移除顶层队列 dequeue_top_rt_rq(rt_rq_of_se(back)); }步骤:
-
遍历调度实体链表:从目标任务所属的调度实体(task_se)开始,沿着parent指针向上遍历,生成一个从顶层到底层的调度实体链表。最后,链表的最后一个元素指向根分组的调度实体。
-
逐层出队调度实体:从顶层到底层,依次将各层调度实体从其上一层的运行队列中移除。随着出队过程,逐层刷新运行队列上的任务计数信息,使其反映移除该分组后的状态。
-
移除顶层队列:调用dequeue_top_rt_rq将顶层队列(rq->rt)从运行队列中移除。注意,这一步并非真正将队列清空,而是递减任务数量,并将队列状态标记为"已出队"。
逐层出队调度实体的实现由__dequeue_rt_entity函数完成。
static void __dequeue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
//调度实体所在rt_rq,即上一层调度实体的运行队列
struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
struct rt_prio_array *array = &rt_rq->active;
// 从运行队列中移除调度实体
if (move_entity(flags)) {
WARN_ON_ONCE(!rt_se->on_list);
__delist_rt_entity(rt_se, array);
}
rt_se->on_rq = 0;
// 更新任务计数信息
dec_rt_tasks(rt_se, rt_rq);
}
- 再入队
系统采用自底向上的层级顺序将调度实体加入队列。
static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
/* 1. 获取目标运行队列 */
struct rt_rq *rt_rq = rt_rq_of_se(rt_se); // 当前调度实体要加入的顶层rt_rq
struct rt_prio_array *array = &rt_rq->active; // 活跃任务数组(按优先级分组)
/* 2. 检查底层调度实体状态(自底向上验证) */
struct rt_rq *group_rq = group_rt_rq(rt_se); // 获取底层关联的组调度队列
struct list_head *queue = array->queue + rt_se_prio(rt_se);
if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running)) {
/* 异常情况处理:
* - 组队列被限流(throttled)
* - 组队列中没有运行任务(rt_nr_running=0)
* 则从当前队列移除并返回 */
if (rt_se->on_list)
__delist_rt_entity(rt_se, array);
return;
}
/* 3. 正式入队操作 */
if (move_entity(flags)) { // 检查是否需要移动实体状态
WARN_ON_ONCE(rt_se->on_list); // 确保不会重复入队
/* 根据flags决定插入位置 */
if (flags & ENQUEUE_HEAD)
list_add(&rt_se->run_list, queue); // 插入队首
else
list_add_tail(&rt_se->run_list, queue); // 插入队尾
__set_bit(rt_se_prio(rt_se), array->bitmap); // 在优先级位图中标记
rt_se->on_list = 1; // 标记已在队列中
}
rt_se->on_rq = 1; // 标记已在运行队列
/* 4. 统计信息更新(自底向上累加) */
inc_rt_tasks(rt_se, rt_rq); // 更新rt_rq的任务计数
}
- 获取目标运行队列:找出当前调度实体要加入的顶层rt_rq
- 层级验证顺序:先检查底层组队列状态(group_rq),确保底层有可用资源后才允许入队
- 正式入队操作: on_list:标记是否在具体优先级的队列中,on_rq:标记是否在运行队列中
- 统计信息:inc_rt_tasks()会递归更新所有父级队列的统计信息,实现从底层到顶层的计数累加
5.2.5 RT组调度之任务出队列
当调度框架需要出队一个实时(RT)任务时,会调用dequeue_task_rt()回调函数,其核心逻辑如下:
static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
struct sched_rt_entity *rt_se = &p->rt;
// 调用RT实体出队函数
dequeue_rt_entity(rt_se, flags);
}
static void dequeue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
struct rq *rq = rq_of_rt_se(rt_se);
// 1. 执行实际的出队操作(自底向上)
dequeue_rt_stack(rt_se, flags);
// 2. 通过"伪入队"刷新统计信息
for_each_sched_rt_entity(rt_se) {
struct rt_rq *rt_rq = group_rt_rq(rt_se);
// 终止条件:
// - 对于task_se,rt_rq为NULL
// - 对于空分组,rt_nr_running为0
if (rt_rq && rt_rq->rt_nr_running)
__enqueue_rt_entity(rt_se, flags); // 伪入队操作
}
// 3. 确保顶层RT队列状态更新
enqueue_top_rt_rq(&rq->rt);
}
出队和入队面临一个相同的问题就是由于系统要刷新各层调度实体的运行队列上的任务个数统计信息,所以也采取了"先出队,再入队"的方式。不同的是在再次入队时,任务和空分组不再入队,相当于完成了出队。
5.2.6 RT组调度之任务选择
在支持组调度的系统中,RT调度类选择任务时必须找到具体的task_se(任务调度实体),而非group_se(组调度实体),当遇到group_se时,必须继续向下搜索,直到找到最高优先级的task_se
主函数入口:pick_next_task_rt()
static struct task_struct *pick_next_task_rt(struct rq *rq)
{
struct task_struct *p;
// 检查是否有可运行的RT任务
if (!sched_rt_runnable(rq))
return NULL;
// 选择优先级最高的任务
p = _pick_next_task_rt(rq);
// 准备运行所选任务
set_next_task_rt(rq, p, true);
return p;
}
static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
struct sched_rt_entity *rt_se;
struct rt_rq *rt_rq = &rq->rt;
// 循环查找:从顶层RT队列开始,穿透组层级
do {
rt_se = pick_next_rt_entity(rt_rq);
BUG_ON(!rt_se); // 必须找到有效实体
rt_rq = group_rt_rq(rt_se); // 获取下一层级队列
} while (rt_rq); // 直到找到task_se(其rt_rq为NULL)
return rt_task_of(rt_se); // 返回对应的task_struct
}
static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,
struct rt_rq *rt_rq)
{
struct rt_prio_array *array = &rt_rq->active;
struct sched_rt_entity *next = NULL;
struct list_head *queue;
int idx;
// 找到最高优先级的队列
idx = sched_find_first_bit(array->bitmap);
BUG_ON(idx >= MAX_RT_PRIO); // 优先级有效性检查
queue = array->queue + idx; // 获取对应优先级的队列
// 获取队列中的第一个实体(最高优先级任务)
next = list_entry(queue->next, struct sched_rt_entity, run_list);
return next;
}
核心流程:
- 层级穿透:通过do…while循环不断向下穿透组层级,当rt_rq为NULL时,表示已到达task_se级别
- 优先级选择:使用sched_find_first_bit()快速定位最高优先级,从对应优先级的队列中获取第一个任务(FIFO顺序)。
总的来说,组调度的特点如下:
- 组调度将系统中任务划分到一个个任务组,所有任务逻辑上构成一颗倒长的树且每个任务属于唯一任务组;
- 支持组调度后,运行队列从原来每个CPU仅有一个扩展到每个任务组都有NR_CPUS个运行队列,分别用于管理组内在各CPU上运行的任务;
- 将调度实体扩展至任务组,运行队列上除任务调度实体还可存在任务组调度实体;
5.3 RT带宽限制流程
5.3.1 RT带宽限制的检查流程
-
运行时间更新
内核在多个关键点(比如时钟中断、调度点、任务唤醒/阻塞时)更新当前任务的运行时间。对于实时任务,运行时间更新通过update_curr_rt()实现:
update_curr_rt(); // 更新当前实时任务的运行时间计算自上次更新以来的时间增量(delta_exec),并更新到当前任务的调度实体(sched_entity)
-
运行队列时间更新
若使能了带宽限制(sysctl_sched_rt_runtime >= 0),需递归更新从叶子节点到根节点的所有祖先运行队列(rt_rq)的运行时间// 获取当前任务的运行队列(叶子节点) struct rt_rq *rt_rq = task_rt_rq(current); // 更新当前队列及其所有祖先队列的运行时间 while (rt_rq) { rt_rq->rt_time += delta_exec; // 累加本次运行时间增量 rt_rq = rt_rq_parent(rt_rq); // 移动到父运行队列 }确保所有相关层级的运行队列都能准确反映实时任务组的资源使用情况。
-
带宽限制检查
sched_rt_runtime_exceeded()函数用于检查实时任务运行队列(rt_rq)是否超过了分配的带宽限制。返回1表示运行时间超限,需触发调度机制。返回0表示未超限,允许继续运行。static int sched_rt_runtime_exceeded(struct rt_rq *rt_rq) { u64 runtime = sched_rt_runtime(rt_rq); // 1. 如果已经处于调度限制状态,直接返回限制状态 if (rt_rq->rt_throttled) return rt_rq_throttled(rt_rq); // 2. 检查额定运行时间是否大于等于周期时间 //如果rt_rq的额定时间大于周期说明不会发生超时, //返回0表示不超额 if (runtime >= sched_rt_period(rt_rq)) return 0; // 不会超时,返回0 // 3. 对运行队列的额定时间进行平衡(可能从其他CPU借时间) balance_runtime(rt_rq); //重新获取平衡后的额定时间,因为balance后rt_rq的额定时间可能改变, //所以需要重新获取rt_rq->rt_runtime runtime = sched_rt_runtime(rt_rq); // 4. 检查是否为无限运行时间 //是的话返回0表示没有超频 if (runtime == RUNTIME_INF) return 0; // 无限时间,不会超时 // 5. 检查实际运行时间是否超过额定时间 if (rt_rq->rt_time > runtime) { struct rt_bandwidth *rt_b = sched_rt_bandwidth(rt_rq); // 6. 只有在有分配运行时间的情况下才进行限制 if (likely(rt_b->rt_runtime)) { //在rt_rq的运行时间超过额定时间的情况下设置调度限制标志 rt_rq->rt_throttled = 1; printk_deferred_once("sched: RT throttling activated\n"); } else { // 处理特殊情况:没有分配时间但通过boosting获得了时间 rt_rq->rt_time = 0; } // 7. 如果确实处于限制状态,执行出队操作 //如果rt_rq运行时间超额且设置了调度限制标志 if (rt_rq_throttled(rt_rq)) { //先将rt_rq对应的实体从队列删除,再放到队尾。 //函数想将rt_rq这个组的rt_se及其所有的祖先rt_se出队, //然后再从祖先rt_se开始依次放到队尾;任何一个rt_rq的 //调度受限时,对应的rt_se在__enqueue_rt_entity(rt_se) //是不能入队的,所以这里的rt_rq对应的调度实体不会入队的; //入不了队也就意味着无法得到调度。 sched_rt_rq_dequeue(rt_rq); return 1; // 返回超时状态 } } return 0; // 未超时 }函数流程解析
- 检查调度限制: 若rt_rq已被标记为受限(rt_throttled = 1),直接返回1。若额定运行时间(rt_runtime) >= 带宽周期(rt_period),表示无限制,因为rt_runtime为-1或rt_period时,表示允许无限制运行,返回0.
- Balance_runtime: 若当前rt_rq的rt_runtime小于rt_period,且系统存在多核,尝试从其它CPU的rt_rq中借剩余时间。调用balance_runtime()实现跨CPU时间分配,确保实时任务组能充分利用系统资源。平衡后,rt_rq->rt_runtime可能增加,但最大不超过rt_period。
- 超限判断: 比较rt_rq->rt_time(已用时间)和rt_rq->rt_runtime(额定时间)。若rt_time > rt_runtime且rt_runtime不等于0,则设置rt_throttled = 1,标记该队列受限。将rt_rq中的所有上层实体(如cgroup层级中的父队列)移动到队列尾部。本层实体(当前队列的任务)出队,停止调度。
调度限制的影响: 当rt_rq->rt_throttled被设置为1,将触发以下行为:
- 强制任务调度: 调用resched_curr(rq)为当前任务(rq->curr)设置TIF_NEED_RESCHED标志。在下次中断或调度点,调度器发现标志后,强制当前任务让出CPU。当任务被移出队列,调度器选择其它非受限的实时任务或普通任务运行。
- 任务入队拒绝: 实时任务在尝试加入运行队列时(如通过enqueue_top_rt_rq(rt_rq))或__enqueue_rt_entity(rt_se,head),会检查目标rt_rq的rt_throttled标志。若受限,则直接跳过该队列,拒绝任务入队。受限队列中的任务无法再被调度,直到带宽周期重置。
-
调度限制的解除
在实时调度(RT)任务管理中,调度限制(throttling)是一种重要机制,用于防止某个实时任务或任务组独占CPU资源。然而,调度限制不能无限期持续,否则相关任务将永远无法执行。当某个实时任务组(task_group)的带宽(rt_bandwidth)达到限制时,内核会启动调度限制,阻止该组中的任务继续获得CPU时间。这种限制机制通过rt_bandwidth结构体中的rt_throttled标志实现。
在init_rt_bandwidth()函数中初始化带宽控制结构:
void init_rt_bandwidth(struct rt_bandwidth *rt_b, u64 period, u64 runtime) { //设置带宽周期 rt_b->rt_period = ns_to_ktime(period); //设置周期内允许的运行时间 rt_b->rt_runtime = runtime; // 初始化自旋锁 raw_spin_lock_init(&rt_b->rt_runtime_lock); // 初始化带宽的高精度定时器 hrtimer_init(&rt_b->rt_period_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HEAD); //设置时钟到期回调函数 rt_b->rt_period_timer.function = sched_rt_period_timer; }当有RT任务入队时,通过__enqueue_rt_entity()检查并激活定时器:
static void start_rt_bandwidth(struct rt_bandwidth *rt_b) { if (!rt_bandwidth_enabled() || rt_b->rt_runtime == RUNTIME_INF) return; raw_spin_lock(&rt_b->rt_runtime_lock); if (!rt_b->rt_period_active) { rt_b->rt_period_active = 1; // 设置定时器到期时间为一个带宽周期 hrtimer_forward_now(&rt_b->rt_period_timer, rt_b->rt_period); // 激活定时器 hrtimer_start_expires(&rt_b->rt_period_timer, HRTIMER_MODE_ABS_PINNED); } raw_spin_unlock(&rt_b->rt_runtime_lock); }定时器到期后,会执行回调函数sched_rt_period_timer():
static enum hrtimer_restart sched_rt_period_timer(struct hrtimer *timer) { struct rt_bandwidth *rt_b = container_of(timer, struct rt_bandwidth, rt_period_timer); int idle = 0; int overrun; raw_spin_lock(&rt_b->rt_runtime_lock); for (;;) { //更新时钟,overrun返回时钟超时周期数 overrun = hrtimer_forward_now(timer, rt_b->rt_period); if (!overrun) break; raw_spin_unlock(&rt_b->rt_runtime_lock); // 执行核心处理 idle = do_sched_rt_period_timer(rt_b, overrun); raw_spin_lock(&rt_b->rt_runtime_lock); } if (idle) rt_b->rt_period_active = 0; // 如果没有可调度任务,停止定时器 raw_spin_unlock(&rt_b->rt_runtime_lock); return idle ? HRTIMER_NORESTART : HRTIMER_RESTART; }核心处理函数do_sched_rt_period_timer(),这个函数是解除调度限制的关键函数。主要用于处理实时任务组的带宽限制和周期性调度。
static int do_sched_rt_period_timer(struct rt_bandwidth *rt_b, int overrun) { int i, idle = 1, throttled = 0; const struct cpumask *span; // 获取受此带宽限制影响的CPU集合 span = sched_rt_period_mask(); #ifdef CONFIG_RT_GROUP_SCHED /* * 针对根任务组的特殊处理: * 如果传入的是根任务组的带宽控制结构,使用在线CPU掩码而不是调度掩码 * 这是为了处理CPU隔离的情况 */ if (rt_b == &root_task_group.rt_bandwidth) span = cpu_online_mask; #endif // 遍历所有受影响的CPU,分析此带宽所在的task_group组上 //各个cpu的运行队列rt_rq for_each_cpu(i, span) { int enqueue = 0; struct rt_rq *rt_rq = sched_rt_period_rt_rq(rt_b, i); // 获取该CPU的实时运行队列 struct rq *rq = rq_of_rt_rq(rt_rq); // 获取该CPU的根运行队列 raw_spin_lock(&rq->lock); // 获取运行队列锁 // 检查该CPU的实时队列是否有运行时间,rt_rq的运行时间只有在 //rt_bandwidth高精度时钟到期后才得以重新统计 if (rt_rq->rt_time) { u64 runtime; raw_spin_lock(&rt_rq->rt_runtime_lock); // 获取实时运行时间锁 // 如果该队列当前被限流,尝试平衡运行时间(从其他CPU借用时间) if (rt_rq->rt_throttled) balance_runtime(rt_rq); runtime = rt_rq->rt_runtime; // 获取该队列的周期运行时间配额 // 抹去周期运行时间(不超过overrun*runtime) //@overrun:超过时钟周期数; //@runtime:一个周期内运行队列的额定运行时间; //没有到一个周期,则将运行时间清0;否则 //运行时间设置为过期超出的额定时间: rt_rq->rt_time -= min(rt_rq->rt_time, overrun*runtime); /* * 如果解除限流后仍有剩余配额,或者队列中有等待任务: * 1. 清除限流标志 * 2. 设置入队标志 */ if (rt_rq->rt_throttled && rt_rq->rt_time < runtime) { rt_rq->rt_throttled = 0; enqueue = 1; /* * 特殊情况处理: * 当CPU空闲且有被唤醒的实时任务被限流时, * 跳过时钟更新以避免将唤醒到解除限流的时间计入运行时间 */ if (rt_rq->rt_nr_running && rq->curr == rq->idle) rq_clock_skip_update(rq, false); } // 标记系统是否空闲(如果此周期rt_rq没有运行时间,但是rt_rq还有就绪的任务, //idle等于0,且rt_rq没有调度限制则入队标志置1) if (rt_rq->rt_time || rt_rq->rt_nr_running) idle = 0; raw_spin_unlock(&rt_rq->rt_runtime_lock); } // 处理没有运行时间但有等待任务的情况 else if (rt_rq->rt_nr_running) { idle = 0; // 系统不空闲 // 如果队列没有被限流,设置入队标志 if (!rt_rq_throttled(rt_rq)) enqueue = 1; } // 标记是否有队列被限流 if (rt_rq->rt_throttled) throttled = 1; //rt_rq带宽超时后sched_rt_rq_dequeue()出队后无法再入队, //直到这里解除了调度限制。 if (enqueue) sched_rt_rq_enqueue(rt_rq); raw_spin_unlock(&rq->lock); // 释放运行队列锁 } //返回系统是否空闲: //如果没有队列被限流且(带宽限制未启用 或 运行时间配额无限) //则返回1 if (!throttled && (!rt_bandwidth_enabled() || rt_b->rt_runtime == RUNTIME_INF)) return 1; return idle; // idle返回0表示有cpu上无可运行调度实体 }接着我们来看一下balance_runtime是如何进行工作的呢?
static void balance_runtime(struct rt_rq *rt_rq) { if (!sched_feat(RT_RUNTIME_SHARE)) return; // 检查当前运行时间是否超过了分配的运行时配额 if (rt_rq->rt_time > rt_rq->rt_runtime) { raw_spin_unlock(&rt_rq->rt_runtime_lock); // 执行实际的运行时间平衡操作 do_balance_runtime(rt_rq); raw_spin_lock(&rt_rq->rt_runtime_lock); } }我们看一下实际的运行时间平衡操作的函数do_balance_runtime()该函数当实时任务在前CPU上被限制执行(因为耗尽了运行时间配额)时,内核调用此函数尝试从同一调度域的其他CPU借用未使用的运行时配额。
static void do_balance_runtime(struct rt_rq *rt_rq) { //获取与当前rt_rq关联的实时带宽限制结构 struct rt_bandwidth *rt_b = sched_rt_bandwidth(rt_rq); //获取当前运行队列所属的root_domain(调度域) struct root_domain *rd = rq_of_rt_rq(rt_rq)->rd; int i, weight; u64 rt_period; // 计算调度域中的CPU数量(权重) weight = cpumask_weight(rd->span); raw_spin_lock(&rt_b->rt_runtime_lock); //将周期时间转换为纳秒单位 rt_period = ktime_to_ns(rt_b->rt_period); for_each_cpu(i, rd->span) { //获取指定CPU的rt_rq结构 struct rt_rq *iter = sched_rt_period_rt_rq(rt_b, i); s64 diff; // 跳过当前CPU自己的rt_rq if (iter == rt_rq) continue; raw_spin_lock(&iter->rt_runtime_lock); /* * Either all rqs have inf runtime and there's nothing to steal * or __disable_runtime() below sets a specific rq to inf to * indicate its been disabled and disalow stealing. */ if (iter->rt_runtime == RUNTIME_INF) goto next; /* * From runqueues with spare time, take 1/n part of their * spare time, but no more than our period. */ //从有剩余运行时的运行队列中借用: //借用其剩余时间的1/n(n=CPU数量),但不超过我们的周期限制 //计算目标CPU的剩余运行时(已分配但未使用的时间) diff = iter->rt_runtime - iter->rt_time; if (diff > 0) { //将剩余时间平均分配给所有CPU(包括自己) diff = div_u64((u64)diff, weight); //确保借用后不超过当前CPU的周期限制 if (rt_rq->rt_runtime + diff > rt_period) diff = rt_period - rt_rq->rt_runtime; //从目标CPU扣除借用量,给当前CPU增加借用量 iter->rt_runtime -= diff; rt_rq->rt_runtime += diff; //如果当前CPU已达到周期限制,提前退出循环 if (rt_rq->rt_runtime == rt_period) { raw_spin_unlock(&iter->rt_runtime_lock); break; } } next: raw_spin_unlock(&iter->rt_runtime_lock); } raw_spin_unlock(&rt_b->rt_runtime_lock); }
最后我们来总结一下内核中通过带宽限制机制防止实时任务无限制占用CPU资源的实现方式,具体如下:
- 带宽限制对象: 带宽限制针对的是任务组,组内所有任务均挂载到同一运行队列 rt_rq 上,因此带宽限制的最终实施对象为 rt_rq。
- 运行时间检查: 内核在多个关键节点调用 update_curr_rt() 函数更新当前任务信息。更新完成后,会检查该任务所属组的运行时间是否超出预设的带宽限制。
- 调度受限处理: 若某个 rt_rq 的运行时间超过带宽限制,内核会标记该 rt_rq 为调度受限状态。此后,该 rt_rq 上的所有实体将被移出运行队列,并且在带宽限制解除之前,这些实体无法重新加入队列。
- 限制解除机制: 每个任务组维护一个高精度时钟,该时钟会按照设定的周期 rt_period 定期更新 rt_rq 上的运行时间。当检测到满足带宽限制解除条件的 rt_rq 时,内核会解除其调度限制。
257

被折叠的 条评论
为什么被折叠?



