2. 调度实体与调度类:task_struct中的调度字段、sched_entity结构体、调度类接口定义
在Linux内核中,进程调度并非直接操作task_struct的全部字段,而是通过调度实体(Scheduling Entity)和调度类(Scheduling Class)两层抽象来实现。这种设计使得CFS(完全公平调度器)与其他调度策略(如实时调度)能够共存于同一系统,且互不干扰。本节将深入剖析这三个核心概念及其在源码中的具体实现。
2.1 task_struct中的调度相关字段
每个进程在内核中由task_struct表示,其中包含了调度器所需的所有元数据。以下是与调度直接相关的关键字段:
| 字段名 | 类型 | 作用描述 |
|---|---|---|
prio |
int |
动态优先级,调度器实际使用的优先级值(数值越小优先级越高) |
static_prio |
int |
静态优先级,由nice值计算得出,仅在调整nice值时改变 |
normal_prio |
int |
常规优先级,基于静态优先级和调度策略计算 |
rt_priority |
unsigned int |
实时进程优先级(0-99),仅用于实时调度类 |
sched_class |
const struct sched_class * |
指向当前进程所属调度类的指针,是调度策略多态的关键 |
se |
struct sched_entity |
CFS调度实体,包含虚拟运行时间、权重等核心调度数据 |
rt |
struct sched_rt_entity |
实时调度实体,用于实时进程的调度管理 |
dl |
struct sched_dl_entity |
Deadline调度实体,用于EDF(最早截止时间优先)调度 |
state |
volatile long |
进程状态(运行、可中断睡眠、不可中断睡眠等) |
on_rq |
int |
标记进程是否在就绪队列上 |
设计要点:
sched_class指针是Linux调度器实现策略与机制分离的核心。调度器通过该指针调用不同调度类的函数,而不需要关心具体实现细节。这种面向对象的设计思想(C语言通过函数指针实现)使得添加新的调度策略变得非常容易。
2.2 sched_entity结构体:CFS的调度单元
sched_entity是CFS调度器操作的基本单位,它独立于task_struct存在,使得CFS可以调度单个进程,也可以调度进程组(组调度)。其核心字段如下:
// 位于 include/linux/sched.h
struct sched_entity {
/* 负载权重,用于计算时间片分配比例 */
struct load_weight load;
/* 红黑树节点,用于将调度实体插入CFS就绪队列的红黑树 */
struct rb_node run_node;
/* 所属的CFS就绪队列 */
struct cfs_rq *cfs_rq;
/* 所属的组就绪队列(仅当该实体代表一个任务组时有效) */
struct cfs_rq *my_q;
/* 虚拟运行时间,CFS调度的核心度量 */
u64 vruntime;
/* 上一次更新统计信息的时间戳 */
u64 prev_sum_exec_runtime;
/* 总执行时间 */
u64 sum_exec_runtime;
/* 进程已使用的虚拟时间片 */
u64 exec_start;
/* 用于记录调度实体被插入就绪队列的次数 */
unsigned long nr_migrations;
/* 统计信息:是否在就绪队列上、是否被延迟等 */
#ifdef CONFIG_FAIR_GROUP_SCHED
struct sched_entity *parent;
/* 组调度相关的链表节点 */
struct list_head group_node;
#endif
};
关键理解:
vruntime(虚拟运行时间)是CFS调度算法的灵魂。CFS总是选择vruntime最小的调度实体来运行。每次时钟中断时,当前运行进程的vruntime会增加,增加量等于实际运行时间乘以权重系数。权重越大的进程,其vruntime增长越慢,从而获得更多的CPU时间。
下图展示了sched_entity与task_struct、cfs_rq之间的关系:
2.3 调度类接口定义
调度类(struct sched_class)是Linux内核调度框架中最重要的抽象接口。它定义了一组函数指针,每个调度策略(CFS、实时、Deadline、空闲)都需要实现这些接口。调度器核心代码通过统一的方式调用这些接口,而不需要知道具体调度策略的实现细节。
// 位于 kernel/sched/sched.h
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);
/* 主动放弃CPU */
void (*yield_task) (struct rq *rq);
/* 检查是否可抢占当前进程 */
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
/* 选择下一个要运行的进程 */
struct task_struct *(*pick_next_task)(struct rq *rq);
/* 将进程放回就绪队列(通常与pick_next_task配对) */
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
#ifdef CONFIG_SMP
/* 在SMP系统中选择目标CPU */
int (*select_task_rq)(struct task_struct *p, int cpu, int sd_flag, int flags);
/* 迁移任务到另一个CPU */
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
/* 任务唤醒时的CPU选择 */
void (*task_woken)(struct rq *this_rq, struct task_struct *task);
#endif
/* 设置调度参数 */
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);
#ifdef CONFIG_FAIR_GROUP_SCHED
/* 组调度相关:任务组变更时的处理 */
void (*task_change_group)(struct task_struct *p, int type);
#endif
};
重要提示: 调度类接口的设计遵循开闭原则——对扩展开放,对修改封闭。开发者可以编写新的调度类(如实现一个自定义的能耗感知调度器),只需实现上述接口,然后将其注册到调度类链表中即可,无需修改调度核心代码。
2.4 调度类优先级链
Linux内核中的调度类按照优先级顺序链接在一起,形成一个单向链表。调度器在选择下一个进程时,按照优先级从高到低的顺序遍历各个调度类,直到找到可运行的进程。
| 调度类 | 优先级顺序 | 适用场景 | 核心算法 |
|---|---|---|---|
stop_sched_class |
最高(0) | 停止CPU的紧急任务 | 直接停止当前CPU |
dl_sched_class |
1 | 有严格截止时间的实时任务 | EDF(最早截止时间优先) |
rt_sched_class |
2 | POSIX实时进程(SCHED_FIFO/SCHED_RR) | 固定优先级抢占 |
fair_sched_class |
3 | 普通分时进程(SCHED_NORMAL/SCHED_BATCH) | CFS(完全公平调度) |
idle_sched_class |
最低(4) | 空闲进程(CPU无事可做时) | 运行空闲线程 |
下图展示了调度类优先级链以及调度器选择进程的流程:
2.5 调度实体与调度类的协同工作
理解调度实体和调度类如何协同工作,是掌握CFS调度器的关键。以下是它们之间的协作流程:
- 进程创建时:
sched_fork()函数根据进程的调度策略设置task_struct->sched_class指针,并初始化对应的调度实体(如se字段)。 - 进程加入就绪队列: 调度核心调用
sched_class->enqueue_task(),CFS类将sched_entity插入红黑树。 - 进程选择时:
pick_next_task()按优先级遍历调度类链表,CFS类从红黑树中选出vruntime最小的sched_entity。 - 时钟中断时:
sched_class->task_tick()更新当前进程的vruntime,并检查是否需要抢占。 - 进程退出时:
sched_class->task_dead()清理调度实体占用的资源。
组调度扩展: 当启用
CONFIG_FAIR_GROUP_SCHED 时,一个 sched_entity 可以代表一个任务组(如Docker容器中的所有进程)。该实体的 my_q 指向一个子 cfs_rq,子队列中再包含该组内各个进程的 sched_entity。这种嵌套结构使得CFS能够实现层级化的CPU时间分配,是容器化环境中CPU资源隔离的基础。
通过这种分层设计,Linux内核实现了调度策略的高度模块化和可扩展性。开发者可以专注于实现特定调度类的接口,而调度核心框架则负责统一的进程管理和CPU分配逻辑。