3. 调度实体与调度组:task_struct与sched_entity结构体、调度组概念、组调度与层级结构

在CFS(完全公平调度器)中,调度的基本单位并非直接是进程,而是调度实体(Scheduling Entity, SE)。这一抽象层使得CFS能够统一调度单个任务和任务组,从而构建出灵活、可控制的层级调度结构。本章将深入剖析task_struct与sched_entity的关系、调度组的概念以及组调度如何实现资源隔离与共享。

3.1 task_struct与sched_entity结构体

每个进程在内核中由一个task_struct表示,它包含了进程的所有信息(PID、状态、内存空间、文件描述符等)。而调度相关的信息则封装在sched_entity结构体中,作为task_struct的一个成员存在。

核心关系: 一个 task_struct 包含一个 sched_entity 实例,CFS调度器操作的是 sched_entity,而非直接操作 task_struct。

以下为sched_entity结构体中的关键字段(Linux 5.x 内核简化版):

struct sched_entity {
    struct load_weight      load;           // 调度实体的权重(用于计算虚拟时间)
    struct rb_node          run_node;       // 红黑树节点
    struct list_head        group_node;     // 组调度链表节点
    unsigned int            on_rq;          // 是否在就绪队列上

    u64                     vruntime;       // 虚拟运行时间(核心调度指标)
    u64                     prev_sum_exec_runtime; // 上次调度时的总执行时间

    u64                     nr_migrations;  // 迁移次数

    struct sched_avg        avg;            // PELT负载跟踪数据结构
};

关键字段解析:

  • load:权重值,决定该实体在CFS红黑树中的位置和CPU时间分配比例。
  • vruntime:虚拟运行时间,CFS通过比较各实体的vruntime来选择下一个运行的任务(vruntime最小的优先)。
  • avg:PELT(Per-Entity Load Tracking)负载跟踪结构,用于计算该实体的历史负载贡献。
  • on_rq:标记该实体是否已入队到CFS运行队列(rq)。
设计意图: 将调度信息从进程描述中解耦,使得一个 sched_entity 既可以代表一个普通线程,也可以代表一个任务组(通过 task_group 结构)。这是实现组调度的基础。

3.2 调度组概念与task_group结构

调度组(Scheduling Group)是CFS实现资源分组管理的核心机制。通过将多个任务放入一个组,可以对该组整体分配CPU时间,组内任务再按各自的权重竞争组内的时间配额。

调度组在内核中由task_group结构体表示:

struct task_group {
    struct cgroup_subsys_state  css;        // cgroup子系统状态
    struct sched_entity         **se;       // 每个CPU上的调度实体(组调度实体)
    struct cfs_rq               **cfs_rq;   // 每个CPU上的CFS运行队列(组内队列)
    struct sched_entity         my_se;      // 该组在父调度域中的调度实体
    struct cfs_rq               my_cfs_rq;  // 该组在父调度域中的CFS运行队列
    unsigned long               shares;     // 组权重(CPU份额)
    // ... 其他字段
};

组调度的工作机制:

  1. 每个task_group在每一个CPU上拥有一个独立的cfs_rq(组内运行队列)和一个sched_entity(组调度实体)。
  2. 组调度实体my_se被挂入父级调度域的CFS红黑树中,参与父级的调度竞争。
  3. 组内的任务(或子组)则挂在组内的cfs_rq上,竞争组分配到的CPU时间。
注意: 组调度引入了两层调度开销。当组调度实体被选中运行时,调度器需要进一步从组内红黑树中选择一个具体的任务执行。这增加了调度延迟的复杂性,是PELT与调度延迟联合优化需要重点关注的场景。

3.3 组调度与层级结构

CFS支持多层级的组调度结构,通常通过cgroup(控制组)来配置。用户可以创建嵌套的cgroup树,每一层对应一个task_group,形成从根组到叶子任务的树形调度层级。

以下是一个典型的层级调度结构示意图:

CFS层级调度结构(组调度树) 根组 (Root Group) 子组A (shares=1024) 子组B (shares=2048) 子组C (shares=512) 任务A1 任务A2 任务B1 任务B2 任务C1 任务C2 层级调度规则: 1. 根组总CPU时间为100%。子组A、B、C按shares比例分配:B获得最多时间(2048/3584 ≈ 57%)。 2. 子组A内部,任务A1和A2按各自权重竞争组内时间。 3. 调度器先选择根组红黑树中的组实体,再递归进入子组红黑树选择具体任务。 4. PELT负载跟踪在每个层级独立计算,父级负载为子级负载之和。

3.4 调度实体与运行队列的映射关系

理解调度实体(SE)与CFS运行队列(cfs_rq)之间的映射关系是掌握组调度的关键。每个CPU核心有一个根CFS运行队列,而每个task_group在每个CPU上也有一个独立的CFS运行队列。

组件 每个CPU实例数 作用 包含内容
根cfs_rq 1 管理该CPU上所有根级调度实体 根组SE、普通任务SE、子组SE
子组cfs_rq 每个task_group在每个CPU上1个 管理该组内任务的调度 组内任务SE、子子组SE
根组SE 1(全局) 代表根组参与调度竞争 权重为所有子组权重之和
子组SE 每个task_group在每个CPU上1个 代表子组在父级cfs_rq中竞争 权重由shares参数决定
任务SE 每个任务1个 代表单个线程参与调度 权重由nice值决定
关键结论: 在层级调度中,一个任务的调度延迟不仅取决于其所在组的权重,还取决于从根组到该任务路径上所有层级调度实体的竞争情况。PELT负载跟踪需要逐层聚合,才能准确反映任务的实际CPU时间分配。

3.5 组调度对PELT与调度延迟的影响

组调度引入了调度层级,这对PELT负载跟踪和调度延迟产生了直接影响:

  • PELT负载聚合:每个sched_entity的avg结构独立跟踪负载。子组SE的负载是其内部所有任务负载之和。根组SE的负载是所有子组SE负载之和。这种逐层聚合保证了负载信息的完整性。
  • 调度延迟放大:当调度器选择了一个组SE后,需要递归进入该组的cfs_rq选择下一个SE。层级越深,调度决策路径越长,调度延迟(从触发调度到任务实际运行)可能增加。
  • 时间片分配:组调度使得时间片分配变为两级甚至多级。父级按shares分配时间给组,组内再按权重分配时间给任务。这可能导致某些任务在短时间内获得的时间片不连续,影响PELT的负载衰减计算。
优化思路: 在联合优化中,可以通过限制组调度层级深度(例如建议不超过3层)、合理设置shares比例、以及调整PELT的半衰期周期来平衡负载跟踪精度与调度延迟。具体方法将在后续章节详细展开。

3.6 本章小结

调度实体(sched_entity)是CFS调度器的核心抽象,它将调度信息从进程描述中解耦,使得组调度成为可能。task_group结构通过在每个CPU上维护独立的cfs_rq和SE,实现了层级化的资源分组管理。这种层级结构在提供灵活的资源控制能力的同时,也增加了调度延迟的复杂性和PELT负载跟踪的层级聚合需求。理解这些基础结构是后续进行PELT与调度延迟联合优化的前提。