4. 调度实体与调度类
在 Linux 内核中,进程调度并非直接操作 task_struct,而是通过两个核心抽象层:调度实体(struct sched_entity) 和 调度类(struct sched_class)。调度实体是调度器眼中的“可调度单元”,调度类则定义了不同调度策略的具体实现。本章将深入剖析这两个结构的设计思想、字段含义以及它们如何构成内核的调度类层次结构。
核心概念:调度实体是“被调度”的对象,调度类是“执行调度”的算法。这种分离设计使得内核可以同时支持多种调度策略(如 CFS、实时、截止时间),并且可以动态扩展。
4.1 调度实体 struct sched_entity
struct sched_entity 是调度器跟踪和管理进程运行时间的基本单位。它被嵌入到 task_struct 中,对于 CFS 调度类,每个进程都有一个对应的调度实体。
4.1.1 核心字段与作用
| 字段名 | 类型 | 作用描述 |
|---|---|---|
load |
struct load_weight | 调度实体的权重,决定其占用 CPU 时间的比例 |
run_node |
struct rb_node | 红黑树节点,用于将实体插入 CFS 运行队列 |
on_rq |
unsigned int | 标记实体是否在运行队列上(1=在队列,0=已出队) |
exec_start |
u64 | 当前调度周期开始的时间戳(单位:ns) |
sum_exec_runtime |
u64 | 实体累计执行的总时间(ns) |
vruntime |
u64 | 虚拟运行时间,CFS 调度的核心指标 |
prev_sum_exec_runtime |
u64 | 上次切换出 CPU 时的累计执行时间 |
nr_migrations |
u64 | 实体在不同 CPU 之间迁移的次数 |
statistics |
struct sched_statistics | 调度统计信息(睡眠时间、等待时间等) |
depth |
int | 实体在调度组中的嵌套深度 |
parent |
struct cfs_rq * | 指向所属的 CFS 运行队列 |
my_q |
struct cfs_rq * | 如果实体代表一个调度组,指向其子运行队列 |
关键设计:
vruntime 是 CFS 调度的“心脏”。内核通过维护红黑树,始终选择 vruntime 最小的实体来运行,从而实现“公平”分配 CPU 时间。
4.1.2 调度实体与 task_struct 的关系
每个进程的 task_struct 中包含多个调度实体,分别对应不同的调度类:
struct task_struct {
// ... 其他字段 ...
struct sched_entity se; // CFS 调度实体
struct sched_rt_entity rt; // 实时调度实体
struct sched_dl_entity dl; // 截止时间调度实体
// ... 其他字段 ...
};
这种设计使得一个进程可以同时被多个调度类管理,但实际运行时只会使用其中一个调度类(由 task_struct->sched_class 指针决定)。
4.1.3 调度实体的生命周期
- 初始化:在
sched_fork()中初始化调度实体,设置初始权重和 vruntime。 - 入队:当进程变为可运行状态时,通过
enqueue_task()将实体插入对应调度类的运行队列。 - 调度:调度器从运行队列中选择 vruntime 最小的实体,通过
context_switch()切换执行。 - 出队:当进程阻塞或退出时,通过
dequeue_task()从运行队列中移除。 - 销毁:在进程结束时,释放调度实体相关的资源。
4.2 调度类 struct sched_class
struct sched_class 是 Linux 内核实现多调度策略的核心抽象。它定义了一组函数指针,每个调度类(如 CFS、实时、截止时间)都需要实现这些接口。调度器通过 task_struct->sched_class 指针调用对应调度类的方法,实现多态调度。
4.2.1 调度类接口定义
struct sched_class {
const struct sched_class *next; // 指向下一个优先级更低的调度类
// 运行队列操作
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);
// 调度选择
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
struct task_struct *(*pick_next_task)(struct rq *rq);
// 上下文切换
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);
// 优先级与时间片
void (*set_curr_task)(struct rq *rq);
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
void (*task_fork)(struct task_struct *p);
void (*task_dead)(struct task_struct *p);
// 负载均衡
void (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
int (*select_task_rq)(struct task_struct *p, int cpu, int flags);
// 调试与统计
void (*update_curr)(struct rq *rq);
unsigned int (*get_rr_interval)(struct rq *rq, struct task_struct *p);
};
4.2.2 核心接口说明
| 接口 | 触发时机 | 典型实现(CFS) |
|---|---|---|
enqueue_task |
进程变为可运行状态 | 将调度实体插入红黑树,更新 vruntime |
dequeue_task |
进程阻塞或退出 | 从红黑树移除实体,更新统计信息 |
pick_next_task |
调度器需要选择下一个运行进程 | 选择红黑树最左节点(vruntime 最小) |
put_prev_task |
当前进程被切换出 CPU | 更新 vruntime,记录执行时间 |
task_tick |
每个时钟 tick 触发 | 检查是否需要抢占,更新 vruntime |
check_preempt_curr |
新进程加入运行队列 | 比较 vruntime,决定是否抢占当前进程 |
select_task_rq |
进程唤醒或迁移时选择目标 CPU | 负载均衡算法选择最优 CPU |
注意:调度类的
next 指针构成了一个单向链表。当高优先级调度类没有可运行进程时,内核会遍历 next 指针,降级到下一个调度类寻找进程。这是实现调度类层次结构的关键机制。
4.3 调度类层次结构
Linux 内核按照优先级从高到低,将调度类组织成以下层次结构:
stop_sched_class → dl_sched_class → rt_sched_class → fair_sched_class → idle_sched_class
(最高优先级) (最低优先级)
4.3.1 各调度类职责
| 调度类 | 优先级 | 调度策略 | 适用场景 |
|---|---|---|---|
stop_sched_class |
最高 | 停止调度类 | CPU 停止、热插拔、migration 线程 |
dl_sched_class |
高 | 截止时间调度(SCHED_DEADLINE) | 硬实时任务,要求严格的时间约束 |
rt_sched_class |
中 | 实时调度(SCHED_FIFO / SCHED_RR) | 软实时任务,如音频/视频处理 |
fair_sched_class |
低 | 完全公平调度(SCHED_NORMAL / SCHED_BATCH) | 普通用户进程,交互式与批处理任务 |
idle_sched_class |
最低 | 空闲调度类 | CPU 空闲时运行 idle 进程 |
4.3.2 调度类选择流程
当内核需要选择下一个运行进程时,遵循以下流程:
- 从当前调度类开始:如果当前进程的调度类有可运行进程,优先选择。
- 遍历 next 指针:如果当前调度类没有可运行进程,通过
next指针降级到下一个调度类。 - 递归检查:重复步骤 1-2,直到找到可运行进程或到达
idle_sched_class。 - 最终保障:如果所有调度类都没有可运行进程,选择 idle 进程。
设计思想:这种层次结构保证了高优先级任务(如实时任务)能够优先获得 CPU,而普通任务在系统空闲时才能运行。同时,通过
next 指针的遍历机制,实现了调度类的“优先级继承”效果。
4.4 调度类与调度实体的协作
调度实体和调度类在运行时紧密协作,共同完成进程调度:
- 调度类负责“算法”:定义如何选择下一个运行进程、如何管理运行队列、如何处理抢占等。
- 调度实体负责“数据”:存储每个进程的调度状态(vruntime、权重、执行时间等),供调度类算法使用。
- 运行队列(rq)作为桥梁:每个 CPU 有一个运行队列,包含多个子队列(cfs_rq、rt_rq、dl_rq),分别由对应的调度类管理。
4.4.1 典型调度流程
// 简化后的调度主路径
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;
// 1. 从最高优先级调度类开始尝试
for_each_class(class) {
p = class->pick_next_task(rq);
if (p)
return p; // 找到可运行进程
}
// 2. 所有调度类都无任务,返回 idle 进程
return rq->idle;
}
理解要点:
for_each_class 宏遍历调度类链表,从 stop_sched_class 开始,依次调用 pick_next_task。每个调度类内部使用自己的数据结构(如 CFS 的红黑树、RT 的优先级数组)来选择最优进程。这种分层设计使得内核可以轻松添加新的调度策略,只需实现 sched_class 接口并插入到链表中即可。
4.5 本章小结
| 概念 | 核心作用 | 关键字段/方法 |
|---|---|---|
| 调度实体 | 可调度单元,存储调度状态 | vruntime、load、run_node |
| 调度类 | 调度算法实现,定义调度策略 | pick_next_task、enqueue_task、task_tick |
| 调度类层次 | 按优先级组织调度类,保证实时性 | next 指针、for_each_class 遍历 |
| 运行队列 | 每个 CPU 的调度数据结构容器 | cfs_rq、rt_rq、dl_rq |