3. 调度实体与调度类:task_struct中的调度字段、sched_entity结构体、调度类接口
在Linux内核中,CFS(完全公平调度器)并非直接操作task_struct,而是通过调度实体(sched_entity)和调度类(sched_class)两层抽象来实现可扩展的调度架构。理解这两层结构是排查vruntime异常和任务饥饿问题的前提。
3.1 task_struct中的调度相关字段
每个进程在内核中对应一个task_struct结构体,其中包含与调度直接相关的关键字段。这些字段是调度器决策的原始数据来源。
| 字段名 | 类型 | 说明 |
|---|---|---|
se |
struct sched_entity |
CFS调度实体,包含vruntime、权重等核心调度数据 |
rt |
struct sched_rt_entity |
实时调度实体,用于RT调度类 |
dl |
struct sched_dl_entity |
Deadline调度实体,用于SCHED_DEADLINE |
prio |
int |
动态优先级,范围0~139(0最高) |
static_prio |
int |
静态优先级,由nice值决定(-20~19映射为100~139) |
normal_prio |
int |
常规优先级,基于static_prio和调度策略计算 |
rt_priority |
unsigned int |
实时优先级,仅用于RT线程(0~99) |
sched_class |
const struct sched_class * |
指向该进程所属调度类的指针 |
policy |
unsigned int |
调度策略:SCHED_NORMAL, SCHED_FIFO, SCHED_RR等 |
nr_cpus_allowed |
int |
允许运行的CPU数量 |
cpus_ptr |
const struct cpumask * |
CPU亲和性掩码指针 |
故障排查要点:当任务出现饥饿时,首先检查policy字段是否被意外修改为SCHED_FIFO或SCHED_RR,以及prio是否被非预期提升。这些字段的异常变化会直接导致CFS调度失效。
3.2 sched_entity结构体:CFS的核心数据载体
sched_entity是CFS调度器直接操作的对象,每个CFS任务(包括线程组和cgroup)都内嵌一个sched_entity实例。它记录了调度决策所需的所有运行时信息。
// include/linux/sched.h (简化版)
struct sched_entity {
/* 负载权重 */
struct load_weight load; // 包含weight和inv_weight
unsigned long runnable_weight; // 可运行权重(CFS组调度用)
/* 红黑树节点 */
struct rb_node run_node; // 红黑树节点,按vruntime排序
struct list_head group_node; // 组调度链表节点
unsigned int on_rq; // 是否在运行队列中
/* vruntime相关 */
u64 vruntime; // 虚拟运行时间(核心指标)
u64 prev_sum_exec_runtime; // 上次切换时的实际运行时间
u64 sum_exec_runtime; // 总实际运行时间
/* 统计与迁移 */
u64 nr_migrations; // 跨CPU迁移次数
struct sched_statistics statistics; // 调度统计信息
#ifdef CONFIG_FAIR_GROUP_SCHED
/* 组调度相关 */
int depth; // 层级深度
struct sched_entity *parent; // 父调度实体
struct cfs_rq *cfs_rq; // 所属CFS运行队列
struct cfs_rq *my_q; // 如果是组,指向其子运行队列
#endif
};
vruntime异常定位:当排查任务饥饿时,直接读取se->vruntime并与同队列其他任务的vruntime比较。如果某个任务的vruntime远小于其他任务(差值超过sysctl_sched_latency),说明该任务可能被“优待”或存在时钟偏差。
3.3 调度类接口:多态调度架构
Linux内核通过调度类(sched_class)实现可扩展的调度策略。每个调度类定义一组标准接口,内核通过task_struct->sched_class指针调用对应的方法。这种设计使得不同调度策略可以共存于同一系统。
// kernel/sched/sched.h (简化版)
struct sched_class {
/* 调度类名称 */
const char *name;
/* 入队/出队 */
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 (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
struct task_struct *(*pick_next_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 (*set_curr_task) (struct rq *rq);
void (*prio_changed) (struct rq *rq, struct task_struct *p, int oldprio);
/* 负载均衡 */
void (*set_cpus_allowed) (struct task_struct *p, const struct cpumask *newmask);
void (*rq_online) (struct rq *rq);
void (*rq_offline) (struct rq *rq);
/* 组调度 */
struct task_struct *(*pick_task) (struct rq *rq);
};
调度类优先级顺序:内核按以下优先级遍历调度类选择任务:stop_sched_class > dl_sched_class > rt_sched_class > fair_sched_class > idle_sched_class。如果系统中有大量RT或Deadline任务,CFS任务可能被“压制”而出现饥饿。
3.4 调度类与调度实体的协作关系
下图展示了调度类、调度实体和运行队列之间的协作关系,以及CFS调度器如何通过红黑树管理任务。
3.5 CFS调度类的核心实现
fair_sched_class是CFS调度器的具体实现,它定义了所有调度类接口的回调函数。以下是CFS调度类的主要接口实现及其作用:
| 接口函数 | CFS实现 | 作用 |
|---|---|---|
enqueue_task |
enqueue_task_fair |
将任务插入CFS红黑树,更新min_vruntime和负载 |
dequeue_task |
dequeue_task_fair |
从CFS红黑树移除任务,更新负载统计 |
pick_next_task |
pick_next_task_fair |
选择红黑树最左节点(vruntime最小)作为下一个运行任务 |
task_tick |
task_tick_fair |
时钟滴答处理:更新vruntime,检查是否需要抢占 |
check_preempt_curr |
check_preempt_wakeup |
唤醒时检查新任务是否可以抢占当前任务 |
set_curr_task |
set_curr_task_fair |
设置当前运行任务,更新se->exec_start |
prio_changed |
prio_changed_fair |
优先级变化时重新计算权重和vruntime |
3.6 vruntime在调度实体中的更新机制
vruntime是CFS调度决策的核心指标,其更新逻辑直接决定了任务的调度顺序。理解vruntime的更新路径对于排查任务饥饿至关重要。
// kernel/sched/fair.c (简化版)
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
// 计算自上次更新以来的实际运行时间
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
// 核心:更新vruntime
// vruntime += delta_exec * (NICE_0_LOAD / curr->load.weight)
curr->vruntime += calc_delta_fair(delta_exec, &curr->load);
// 更新cfs_rq的min_vruntime
update_min_vruntime(cfs_rq);
// 检查是否超过调度延迟,触发抢占
if (entity_is_long_sleeper(curr))
check_preempt_tick(cfs_rq, curr);
}
vruntime异常根因分析:
- vruntime增长过快:任务权重过低(nice值过高),导致
calc_delta_fair放大delta_exec。表现为任务频繁被抢占,CPU时间片极短。 - vruntime增长过慢:任务权重过高(nice值过低),或
exec_start未正确更新(时钟中断丢失)。表现为任务长时间占用CPU,其他任务饥饿。 - vruntime回退:任务睡眠后唤醒时,
place_entity可能将vruntime设置为cfs_rq->min_vruntime,如果min_vruntime异常偏小,会导致该任务“插队”到红黑树最左端。
3.7 调度类选择与任务饥饿的关联
当系统同时存在多种调度类任务时,调度类的优先级顺序可能导致CFS任务被“饿死”。以下场景是生产环境中常见的饥饿诱因:
- RT任务泛滥:大量SCHED_FIFO或SCHED_RR任务持续运行,由于
rt_sched_class优先级高于fair_sched_class,CFS任务可能长时间得不到CPU。 - Deadline任务超限:SCHED_DEADLINE任务在截止时间前持续占用CPU,导致CFS任务被推迟。
- cgroup组调度嵌套:在CFS组调度中,父组的调度实体vruntime异常会导致整个子组内的任务集体饥饿。
- CPU亲和性限制:任务被绑定到某个CPU,而该CPU上存在高优先级任务,导致该任务无法迁移到空闲CPU。
快速诊断命令:
# 查看所有任务的调度策略和优先级
ps -eo pid,policy,prio,nice,comm --sort=-prio
# 查看每个CPU的运行队列状态
cat /proc/sched_debug | grep -A 20 "cpu#"
# 查看CFS运行队列的vruntime分布
cat /proc/sched_debug | grep -E "min_vruntime|vruntime"
3.8 调度实体与调度类的故障排查清单
当怀疑调度实体或调度类导致任务饥饿时,按以下清单逐项排查:
| 检查项 | 方法 | 异常指标 |
|---|---|---|
| 调度策略 | cat /proc/[pid]/sched | grep policy |
非SCHED_NORMAL(0)可能导致CFS失效 |
| 调度类指针 | cat /proc/[pid]/sched | grep sched_class |
应为fair_sched_class,若为rt或dl则异常 |
| vruntime差值 | cat /proc/sched_debug | grep -A 5 "cfs_rq" |
单个任务vruntime与min_vruntime差值超过sysctl_sched_latency(默认6ms) |
| 负载权重 | cat /proc/[pid]/sched | grep se.load |
权重异常偏大或偏小(正常范围:3~88761) |
| CPU时间分布 | pidstat -p [pid] 1 5 |
长时间未获得CPU时间(%CPU持续为0) |
| 调度类优先级 | cat /proc/sched_debug | grep "rt_rq\|dl_rq" |
RT或DL运行队列有持续运行的任务 |
关键警告:不要在生产环境中直接修改task_struct中的调度字段。任何对policy、prio、se.vruntime的手动修改都可能导致调度器状态不一致,引发系统崩溃或死锁。应始终通过sched_setscheduler()、setpriority()等系统调用接口操作。