3. 组调度(Group Scheduling)概念

在早期的Linux内核中,调度器(O(1)调度器、CFS调度器初期版本)的调度单位是单个进程或线程。这意味着CPU时间片的分配是基于系统中所有独立进程的数量来计算的。这种设计在桌面或通用服务器场景下基本够用,但在多租户、容器化、资源隔离要求高的环境中暴露出了严重的公平性问题。

核心痛点: 当系统中有多个用户或任务组时,一个用户启动大量进程,就会挤占其他用户进程的CPU时间。例如,用户A启动100个计算密集型进程,用户B只启动1个交互式进程,在单进程调度模式下,用户B的进程只能获得约1%的CPU时间,而用户A的进程则占用了99%。这显然违背了多用户公平共享的原则。

3.1 单进程调度的局限性

在传统的单进程调度模型中,CFS(Completely Fair Scheduler)维护一个红黑树,每个可运行进程对应一个调度实体(struct sched_entity)。调度器遍历红黑树,选择虚拟运行时间(vruntime)最小的进程来执行。每个进程获得的时间片与其权重(weight)成正比,与系统中总进程数成反比。

单进程调度模型示意图:

单进程调度模型:CPU时间片按进程数量均分 CPU 核心 CFS 调度器 红黑树(调度实体队列) 进程 A1 (权重 1024) 进程 A2 (权重 1024) 进程 B1 (权重 1024) 进程 C1 (权重 1024) 每个进程独立调度,时间片均分

单进程调度的不公平场景:

用户 进程数量 每个进程权重 总权重 获得的CPU时间占比
用户A 100 1024 102400 99%
用户B 1 1024 1024 1%
结论: 单进程调度无法实现用户级或任务组级的公平性。用户B的单个进程虽然权重与用户A的每个进程相同,但由于进程数量悬殊,用户B几乎无法获得有意义的CPU时间。这正是组调度机制要解决的核心问题。

3.2 任务组调度(Group Scheduling)的引入

为了解决上述不公平问题,Linux内核从2.6.24版本开始引入了组调度(Group Scheduling)机制,并在后续版本中不断完善。组调度的核心思想是:将进程组织成任务组(Task Group),调度器以任务组为单位分配CPU时间片,组内的进程再按照组内规则进一步分配时间片。

在组调度模型中,调度实体(struct sched_entity)不仅可以代表一个进程,还可以代表一个任务组。每个任务组内部维护自己的红黑树,组内的进程或子组在该红黑树上进行调度。这种层级化的调度结构实现了CPU时间片的多级公平分配。

组调度模型示意图:

组调度模型:CPU时间片按任务组层级分配 CPU 核心 CFS 顶层调度器 顶层红黑树(任务组调度实体) 任务组 A(权重 1024) 任务组 B(权重 1024) 任务组 C(权重 1024) 组A 内部红黑树 进程 A1 进程 A2 组B 内部红黑树 进程 B1 组C 内部红黑树 进程 C1 进程 C2 进程 C3 每个任务组获得相等的CPU时间片(权重相同),组内进程再按组内规则分配

3.3 组调度的公平性分析

在组调度模型下,重新审视前面用户A和用户B的场景。假设用户A的100个进程属于任务组A,用户B的1个进程属于任务组B,两个任务组的权重相同(均为1024)。

组调度下的CPU时间分配:

任务组 组权重 组获得的CPU时间 组内进程数量 每个进程获得的CPU时间
任务组A(用户A) 1024 50% 100 0.5%
任务组B(用户B) 1024 50% 1 50%
关键改进: 在组调度模式下,用户B的单个进程获得了50%的CPU时间,而不是之前的1%。这实现了用户级别的公平性——无论一个用户启动了多少进程,该用户所属的任务组都能获得与其权重相匹配的CPU时间份额。

3.4 组调度的内核实现要点

组调度在内核中的实现涉及以下几个关键数据结构与机制:

  • struct task_group:内核中表示任务组的主要数据结构。每个任务组包含自己的CFS运行队列(cfs_rq)、调度实体(se)以及用于CPU带宽控制的统计信息。
  • 层级化调度实体:一个任务组在父调度器的红黑树中表现为一个调度实体(struct sched_entity),同时该任务组内部又维护自己的红黑树,调度组内的进程或子组。
  • 权重继承与计算:任务组的权重可以手动设置(通过cgroup的cpu.shares),也可以根据组内进程的总权重自动计算。组调度器在分配时间片时,会综合考虑组权重和组内进程的权重。
  • 虚拟运行时间(vruntime)的层级维护:每个层级都有自己的vruntime计算逻辑。任务组在顶层调度器中的vruntime增长速率取决于该组获得的CPU时间比例,而组内进程的vruntime则基于组内分配的时间片进行计算。

组调度关键数据结构关系图:

组调度内核数据结构关系 struct task_group 任务组描述符 struct cfs_rq 组内CFS运行队列 struct sched_entity 父调度器中的调度实体 cpu.shares / weight 组权重配置 组调度核心逻辑 1. 顶层调度器选择 task_group 对应的 sched_entity 执行 2. 选中的 task_group 再从其内部的 cfs_rq 中选择一个进程执行 3. 组权重 cpu.shares 决定该组在顶层调度器中的时间片比例

3.5 组调度与cgroup的关联

组调度机制通过cgroup(Control Groups)暴露给用户空间。在cgroup v1中,cpu子系统提供了cpu.shares文件用于设置任务组的CPU权重;在cgroup v2中,cpu.weight文件承担了类似的功能。用户可以通过修改这些文件来动态调整任务组的CPU时间片分配比例。

组调度与cgroup的对应关系:

概念 内核实现 cgroup接口 说明
任务组 struct task_group cgroup目录(如 /sys/fs/cgroup/cpu/mygroup) 每个cgroup目录对应一个内核任务组
组权重 shares 字段 cpu.shares(v1) / cpu.weight(v2) 设置任务组在CPU时间片分配中的相对权重
组内进程 tasks 列表 cgroup.procs / tasks 将进程PID写入该文件,进程即加入对应任务组
CPU带宽控制 cfs_bandwidth cpu.cfs_period_us / cpu.cfs_quota_us 限制任务组在给定周期内最多使用的CPU时间(下一章详述)
实践要点: 组调度是cgroup CPU资源管理的底层基础。理解组调度的层级模型,有助于正确配置cgroup的CPU权重和带宽限制。在实际运维中,通常将不同业务、不同用户或不同容器的进程划分到不同的cgroup任务组中,然后通过设置合理的权重来实现CPU资源的差异化分配。

3.6 组调度的优势与适用场景

组调度相比单进程调度的核心优势:

  • 多用户公平性:确保每个用户或租户获得与其权重匹配的CPU时间,不受其启动进程数量的影响。
  • 容器资源隔离:每个容器可以作为一个任务组,容器内的所有进程共享该组的CPU时间片,避免一个容器内的进程风暴影响其他容器。
  • 服务质量保障(QoS):可以为关键业务任务组分配较高的权重,确保其在CPU竞争中获得优先服务。
  • 层级化管理:支持多级任务组嵌套,实现精细化的资源分配策略(如:部门→项目→服务→容器)。

典型适用场景:

  1. 云计算多租户平台:为每个租户创建一个cgroup任务组,按租户付费等级分配CPU权重。
  2. 容器编排(Kubernetes):每个Pod对应一个cgroup任务组,Pod内的所有容器共享CPU时间片。
  3. 混合负载部署:将在线服务与离线任务分别放入不同的任务组,在线服务组设置高权重,离线任务组设置低权重。
  4. 系统资源保护:将系统关键进程(如sshd、监控代理)放入高权重任务组,避免被用户进程完全挤占。
注意事项: 组调度虽然解决了公平性问题,但也会引入额外的调度开销(层级遍历、权重计算)。在任务组数量极大(如数万个容器)的场景下,需要关注调度延迟。此外,组调度仅控制CPU时间的分配比例,不提供硬性上限保护——这正是下一章“CPU带宽控制”要解决的问题。