2. 调度实体与调度组:task_struct 中的调度字段、调度组(task_group)的概念、组调度与带宽控制

在 CFS 调度器的设计中,调度的基本单位并非直接是进程(task),而是调度实体(Scheduling Entity, sched_entity)。这一抽象层使得 CFS 能够统一调度单个进程和进程组,为组调度(Group Scheduling)和带宽控制(Bandwidth Control)奠定了数据结构基础。

2.1 task_struct 中的调度字段

每个进程在内核中对应一个 task_struct 结构体,其中包含了与 CFS 调度直接相关的关键字段。这些字段是调度器进行虚拟时间计算、入队出队、负载跟踪的依据。

字段名 类型 作用说明
se struct sched_entity CFS 调度实体,包含 vruntime、权重、负载等核心调度数据
rt struct sched_rt_entity 实时调度实体(RT 类使用)
dl struct sched_dl_entity Deadline 调度实体(DL 类使用)
prio int 动态优先级,影响 CFS 权重计算
static_prio int 静态优先级,由 nice 值决定(-20 ~ 19)
normal_prio int 常规优先级,综合静态优先级和调度策略计算
sched_class const struct sched_class * 指向该进程所属调度类(如 fair_sched_class)
cpus_ptr const struct cpumask * 进程可运行的 CPU 集合

其中 se(sched_entity)是 CFS 调度的核心,其内部结构如下:

struct sched_entity {
    struct load_weight      load;           // 调度权重,由 nice 值映射
    struct rb_node          run_node;       // 红黑树节点
    struct list_head        group_node;     // 组调度链表节点
    unsigned int            on_rq;          // 是否在运行队列中
    u64                     vruntime;       // 虚拟运行时间(核心指标)
    u64                     prev_sum_exec_runtime; // 上次调度时的总执行时间
    u64                     sum_exec_runtime;      // 总执行时间
    u64                     nr_migrations;  // 迁移次数
    struct sched_avg        avg;            // PELT 负载跟踪数据
};
关键理解: vruntime 是 CFS 选择下一个运行进程的唯一依据。调度器总是选择红黑树中 vruntime 最小的调度实体投入运行。权重(load.weight)决定了进程实际执行时间与虚拟时间的换算比例。

2.2 调度组(task_group)的概念

调度组(task_group)是 CFS 实现组调度的核心数据结构。它允许将多个进程组织成一个逻辑组,组内的所有进程共享一个 CPU 时间配额。组调度使得资源隔离和容器化成为可能。

每个 task_group 包含以下关键元素:

  • 自己的调度实体:在父调度组或根任务组的红黑树中,该组以一个 sched_entity 的身份参与调度。
  • 自己的运行队列:组内包含一个 CFS 运行队列(cfs_rq),用于管理组内进程的调度。
  • 权重与配额:组可以设置 cpu.shares(权重)和 cpu.cfs_quota_us/cpu.cfs_period_us(带宽限制)。
实际应用: 在 Kubernetes 或 Docker 容器环境中,每个容器对应一个 task_group。通过调整组的 cpu.shares,可以实现容器间的 CPU 时间比例分配,而 cpu.cfs_quota_us 则限制容器的 CPU 使用上限。

下图展示了根任务组与两个子调度组之间的层级关系:

调度组层级结构图 根任务组 (root_task_group) cfs_rq (根运行队列) 子调度组 A 子调度组 B cfs_rq (组 A) cfs_rq (组 B) 进程 P1 进程 P2 进程 P3 进程 P4 每个子调度组在根 CFS RQ 中表现为一个调度实体,组内进程在组 CFS RQ 中独立调度

2.3 组调度与带宽控制

组调度解决了资源隔离问题,而带宽控制则解决了资源上限问题。两者结合,构成了 Linux 容器 CPU 资源管理的完整方案。

2.3.1 组调度的核心机制

组调度的实现基于以下原理:

  1. 层级时间分配:调度器先按权重在根任务组和子组之间分配时间,子组再在其内部进程间分配时间。
  2. 嵌套红黑树:每个 cfs_rq 维护自己的红黑树,组调度实体作为父级红黑树的节点。
  3. 权重传播:子组的权重会向上传播到父组,影响父组在更高层级中的时间分配。
性能注意事项: 过多的调度组层级(深度超过 3-4 层)会增加调度器的遍历开销。在生产环境中,建议保持扁平化的组结构,避免深度嵌套。

2.3.2 带宽控制参数

带宽控制通过 CGroup 的 CPU 子系统实现,主要参数如下:

参数文件 类型 默认值 说明
cpu.cfs_period_us 微秒 100000 (100ms) 带宽控制周期
cpu.cfs_quota_us 微秒 -1 (无限制) 每个周期内允许的运行时间上限
cpu.shares 权重 1024 CPU 时间相对权重(非硬限制)
cpu.stat 状态 - 显示带宽控制统计信息(如 throttled 次数)

带宽控制的工作流程如下:

带宽控制流程图 周期开始 (period_us) 配额 > 0 ? (quota_us) 允许运行 消耗配额 throttled 等待下一周期 周期结束 / 重置配额 当组内所有进程的累计运行时间达到 quota_us 时,该组被 throttled,直到下一周期开始

2.3.3 带宽控制配置示例

以下示例展示如何通过 CGroup 限制一个容器组使用 2 个 CPU 核心(200ms/100ms 周期):

# 创建 CGroup
mkdir -p /sys/fs/cgroup/cpu/my_container

# 设置带宽限制:每 100ms 周期最多运行 200ms(即 2 个 CPU 核心)
echo 200000 > /sys/fs/cgroup/cpu/my_container/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/my_container/cpu.cfs_period_us

# 设置权重(与其他组相比的相对比例)
echo 2048 > /sys/fs/cgroup/cpu/my_container/cpu.shares

# 将进程加入组
echo 1234 > /sys/fs/cgroup/cpu/my_container/cgroup.procs

# 查看带宽控制统计
cat /sys/fs/cgroup/cpu/my_container/cpu.stat
# 输出示例:
# nr_periods 0
# nr_throttled 0
# throttled_time 0
生产环境建议:
  • 对于延迟敏感型服务(如数据库、Web 服务器),建议设置 cpu.cfs_quota_us 为物理 CPU 核心数的 80%-90%,预留突发处理能力。
  • 对于批处理任务,可以设置 cpu.shares 较低(如 512),确保不抢占关键业务资源。
  • 监控 cpu.stat 中的 nr_throttled 值,如果频繁出现 throttled,说明配额设置过紧,需要调整。

2.4 调度实体与调度组的性能影响

理解调度实体和调度组的结构,对于性能调优至关重要:

场景 问题表现 调优方向
大量短生命周期进程 频繁创建/销毁调度实体,增加红黑树操作开销 使用线程池复用进程,减少调度实体数量
深度嵌套的 CGroup 调度遍历路径变长,上下文切换延迟增加 保持组层级 ≤ 3 层,使用扁平结构
带宽配额设置过小 频繁 throttled,服务吞吐量下降 根据实际负载计算配额,预留 20% 缓冲
权重分配不合理 高优先级组得不到足够 CPU 时间 使用 cpu.shares 按业务重要性比例分配