任务分组
我们先看以下两种场景。
(1)执行<font style="color:rgb(0,0,0);">make -j10</font>
(选项<font style="color:rgb(0,0,0);">-j10</font>
表示同时执行 10 条命令),编译 Linux 内核,同时运行视频播放器,如果给每个进程平均分配 CPU 时间,会导致视频播放很卡。
(2)用户 1 启动 100 个进程,用户 2 启动 1 个进程,如果给每个进程平均分配 CPU 时 间,用户 2 的进程只能得到不到 1%的 CPU 时间,用户 2 的体验很差。
怎么解决呢?把进程分组。对于第一种场景,把编译 Linux 内核的所有进程放在一个任务组 中,把视频播放器放在另一个任务组中,给两个任务组分别分配 50%的 CPU 时间。对于第二种场景,给用户 1 和用户 2 分别创建一个任务组,给两个任务组分别分配 50%的 CPU 时间。
任务分组(Task Group) 是对进程进行逻辑划分和资源限制管理的一种机制,主要用于 控制资源使用、调度策略、统计 等目的。
任务分组的实现核心是 cgroup(Control Groups)子系统,以及调度器中的 task_group 结构体。
struct task_group
/* task group related information */
struct task_group {
struct cgroup_subsys_state css;
#ifdef CONFIG_FAIR_GROUP_SCHED
/* schedulable entities of this group on each cpu */
struct sched_entity **se;
/* runqueue "owned" by this group on each cpu */
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
...
};
- 每个 task_group 表示一个进程组。
- 每个 task_group 对应一个调度实体数组(
se
)和一个运行队列(cfs_rq
)等。 task_group
和cgroup
是一一对应的(由css
连接)。- 每个进程的
task_struct
中会关联其所属的task_group
。
调度器中的任务分组支持
Linux 支持多个调度器,比如:
- CFS(完全公平调度器)
- RT(实时调度器)
每种调度器都有对应的 task group 处理逻辑,例如:
cfs_rq
:表示该 task_group 的 CFS 调度运行队列。rt_rq
:表示该 task_group 的实时调度运行队列。- 对每个 CPU,调度器会在其上为每个 task_group 创建对应的运行队列。
调度时,内核会根据任务所属的 group,在其 group 的运行队列中进行选择,这样可以实现任务分组的公平调度和资源限制。
CPU运行队列和task_group中运行队列的关系
CPU 的运行队列(per-CPU runqueue)
每个 CPU 上维护一个独立的运行队列:
struct rq {
struct cfs_rq cfs; // 默认调度器 CFS 的运行队列
struct rt_rq rt; // 实时调度器的运行队列
...
};
这个 rq
是每个 CPU 的调度核心数据结构,调度器总是在该 CPU 的 rq
上操作任务。
task_group 的运行队列(per-task_group runqueue)
task_group 的核心在于它为 每个 CPU 创建自己的调度子队列,例如:
struct task_group {
struct cfs_rq **cfs_rq; // 所有 CPU 上本组的 CFS 队列数组
struct sched_entity **se; // 本组在上一级 group 中的调度实体
...
};
tg->cfs_rq[cpu]
表示该任务组在第cpu
核心上的 CFS 运行队列。tg->se[cpu]
表示该任务组在父组中代表自身的调度实体(可嵌套)。
这形成了一个分组树结构:组嵌套 -> 组调度实体 -> 组运行队列。
任务组在每个处理器上有公平调度实体、公平运行队列、实时调度实体和实时运行队列,根任务组比较特殊:没有公平调度实体和实时调度实体。
(1)任务组的下级公平调度实体加入任务组的公平运行队列,任务组的公平调度实体加入上级任务组的公平运行队列。
(2)任务组的下级实时调度实体加入任务组的实时运行队列,任务组的实时调度实体 加入上级任务组的实时运行队列。
运行队列之间的关系图
以 CFS 调度器为例,关系如下图:
每个 CPU:
rq (CPU0)
├── cfs_rq // CPU 的根调度队列
│ ├── se (A组调度实体) ─┐
│ ├── se (B组调度实体) ─┐│
│ ││
│ ▼▼
│ task_group_A.cfs_rq[CPU0]
│ task_group_B.cfs_rq[CPU0]
- 每个 CPU 的
rq.cfs
是顶层队列,里面的sched_entity
可以是单个任务,也可以是一个组。 - 如果是组的
sched_entity
,就指向该组的cfs_rq[cpu]
。 - 递归结构允许 group 嵌套 group,调度器按公平调度递归下去。
调度器如何使用它们
以cfs调度器为例,调度器在进行调度选择时:
- 遍历 CPU 的
rq.cfs
队列,挑选最合适的sched_entity
- 如果该
sched_entity
是一个任务,就直接调度 - 如果该
sched_entity
是一个任务组,就进入对应的task_group.cfs_rq[cpu]
- 在该队列中递归选择下一级的
sched_entity
,直到选出最终的任务
为什么这么设计?
这是一种典型的 分层调度(Hierarchical Scheduling) 机制,有几个优点:
- 支持资源公平:例如,每组任务在顶层调度中获得公平份额
- 支持资源限制:可以对每组任务设置 CPU 份额(如通过
cfs_quota_us
) - 支持嵌套组调度:适用于容器、Kubernetes pod、服务/用户分组等
- 实现清晰:调度逻辑复用 CFS 的实体结构和算法
task_group与 cgroup 的关系
task_group
并不是用户直接使用的接口,而是内核内部与 **cgroup**
** 子系统** 的绑定结构。
# 创建一个新的 CPU 资源组
mkdir /sys/fs/cgroup/cpu/mygroup
# 向其中加入任务
echo <pid> > /sys/fs/cgroup/cpu/mygroup/tasks
当进程加入某个 cgroup 后,其 task_struct
中的 task_group
指针会指向该 cgroup 对应的 task_group
实例,调度器就会按组来对其调度。
任务分组的用途
任务分组的主要作用包括:
作用 | 说明 |
---|---|
资源隔离 | 可以为每组任务限制 CPU、内存、I/O 等资源 |
公平调度 | 组内共享时间片,组间公平分配 CPU |
负载统计 | 按组统计 CPU、内存使用,支持审计与性能分析 |
服务级别管理 | 可按服务、容器或租户分组,管理更清晰 |
典型应用场景
- Docker 容器:每个容器会被映射为一个独立的 cgroup,自然形成任务分组。
- 系统级任务隔离:比如将系统服务和用户应用程序分组,防止干扰。
调试与查看
可以通过以下方式查看任务所属的 task group(cgroup):
cat /proc/<pid>/cgroup
查看调度相关的任务组结构:
cat /sys/fs/cgroup/cpu/mygroup/cpu.stat
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12