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调度器如何通过红黑树管理任务。

调度类与调度实体协作架构 调度类 (sched_class) 接口层 pick_next_task() | enqueue_task() | dequeue_task() | task_tick() | check_preempt_curr() stop_sched_class → dl_sched_class → rt_sched_class → fair_sched_class → idle_sched_class 运行队列 (rq / cfs_rq) 每个CPU一个主运行队列 rq,包含 cfs_rq、rt_rq、dl_rq cfs_rq 使用红黑树按 vruntime 组织调度实体 min_vruntime | nr_running | h_nr_running | load 调度实体 A vruntime: 1024 load.weight: 1024 on_rq: 1 调度实体 B vruntime: 2048 load.weight: 2048 on_rq: 1 调度实体 C vruntime: 3072 load.weight: 512 on_rq: 1 task_struct A pid: 1001 | policy: NORMAL prio: 120 | sched_class: fair task_struct B pid: 1002 | policy: NORMAL prio: 110 | sched_class: fair task_struct C pid: 1003 | policy: NORMAL prio: 130 | sched_class: fair 调度类通过标准接口操作运行队列,运行队列管理调度实体,调度实体内嵌于task_struct中

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任务被“饿死”。以下场景是生产环境中常见的饥饿诱因:

  1. RT任务泛滥:大量SCHED_FIFO或SCHED_RR任务持续运行,由于rt_sched_class优先级高于fair_sched_class,CFS任务可能长时间得不到CPU。
  2. Deadline任务超限:SCHED_DEADLINE任务在截止时间前持续占用CPU,导致CFS任务被推迟。
  3. cgroup组调度嵌套:在CFS组调度中,父组的调度实体vruntime异常会导致整个子组内的任务集体饥饿。
  4. 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()等系统调用接口操作。