4. 红黑树与就绪队列:红黑树数据结构、cfs_rq 结构体、min_vruntime 的作用

CFS 调度器的核心数据结构是 红黑树(Red-Black Tree) 和 cfs_rq(CFS Run Queue)。红黑树用于按 vruntime 排序所有可运行的任务,而 cfs_rq 则管理该队列的全局状态。理解这两个结构以及 min_vruntime 的作用,是定位 vruntime 异常和任务饥饿问题的基础。

4.1 红黑树数据结构

CFS 使用红黑树来维护就绪队列中的任务。红黑树是一种自平衡的二叉搜索树,其关键特性保证了调度器的高效性:

  • 键值(Key):每个节点的键值是任务的 vruntime。
  • 排序规则:左子节点的 vruntime 小于父节点,右子节点的 vruntime 大于父节点。
  • 最左节点:树中最左侧的节点(即 vruntime 最小的节点)是下一个被调度执行的任务。
  • 时间复杂度:插入、删除和查找操作的时间复杂度均为 O(log n),其中 n 是就绪队列中的任务数。
关键点: 红黑树的自平衡特性确保了调度器不会因为任务数量的增加而出现性能退化。即使有数千个任务,查找下一个执行任务的操作仍然非常快。
CFS 红黑树结构示意图 vruntime=100 vruntime=50 vruntime=150 vruntime=20 vruntime=80 vruntime=130 vruntime=180 红黑树关键特性 最左节点(vruntime=20)是下一个被调度执行的任务。 插入、删除、查找时间复杂度均为 O(log n)。

4.2 cfs_rq 结构体

cfs_rq 是每个 CPU 上的 CFS 就绪队列,它封装了红黑树以及管理该队列所需的所有元数据。以下是 cfs_rq 结构体的核心字段:

字段名 类型 描述
tasks_timeline struct rb_root_cached 红黑树的根节点,包含一个缓存的最左节点指针,用于快速获取下一个任务。
min_vruntime u64 队列中所有任务的最小 vruntime 值,用于新任务和唤醒任务的 vruntime 归一化。
nr_running unsigned int 当前就绪队列中可运行的任务数量。
load struct load_weight 队列的总负载权重,用于计算调度周期和 vruntime 的增量。
exec_clock u64 该队列上所有任务的总执行时间(用于统计和调试)。
调试提示: 在排查任务饥饿问题时,首先检查 cfs_rq->nr_running 是否远大于 1,以及 cfs_rq->min_vruntime 是否出现异常跳跃。这些字段可以通过 /proc/sched_debug 或内核 tracepoint 查看。

4.3 min_vruntime 的作用

min_vruntime 是 CFS 调度器中一个至关重要的全局变量。它代表了当前就绪队列中所有任务的最小 vruntime 值。其核心作用包括:

  • 新任务归一化:当新任务创建时,其初始 vruntime 被设置为 min_vruntime,确保新任务不会因为 vruntime 过小(获得过多 CPU 时间)或过大(被延迟调度)而破坏公平性。
  • 唤醒任务归一化:当任务从睡眠状态被唤醒时,其 vruntime 会被调整为 min_vruntime(或略大于它),以防止睡眠较久的任务在唤醒后立即抢占 CPU。
  • 防止 vruntime 溢出:通过定期更新 min_vruntime,可以避免所有任务的 vruntime 无限增长,从而防止 64 位整数溢出。
  • 公平性基准:所有任务的 vruntime 都相对于 min_vruntime 进行比较,确保调度器始终选择“最需要 CPU”的任务。
min_vruntime 作用流程图 min_vruntime 新任务创建 vruntime = min_vruntime 任务唤醒 vruntime = max(vruntime, min_vruntime) 周期性更新 min_vruntime = 最左节点 vruntime min_vruntime 异常的影响 如果 min_vruntime 被错误地设置得过大,新任务和唤醒任务将获得过高的 vruntime,导致它们被延迟调度, 从而引发任务饥饿。反之,如果 min_vruntime 过小,新任务可能过度抢占 CPU。

4.4 关键代码路径

以下代码展示了 CFS 如何从红黑树中选取下一个任务,以及 min_vruntime 在任务入队时的更新逻辑:

// 选取下一个任务(核心调度路径)
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
    // 从红黑树缓存中直接获取最左节点(vruntime 最小的任务)
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left)
        return NULL;
    return rb_entry(left, struct sched_entity, run_node);
}

// 任务入队时更新 min_vruntime
static void update_min_vruntime(struct cfs_rq *cfs_rq)
{
    u64 vruntime = cfs_rq->min_vruntime;
    struct sched_entity *curr = cfs_rq->curr;

    // 如果当前任务存在,考虑其 vruntime
    if (curr)
        vruntime = curr->vruntime;

    // 如果红黑树非空,考虑最左节点的 vruntime
    if (cfs_rq->nr_running) {
        struct sched_entity *se = __pick_next_entity(cfs_rq);
        if (se)
            vruntime = min(vruntime, se->vruntime);
    }

    // 更新 min_vruntime,确保其单调递增
    cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
}
故障排查要点: 如果 min_vruntime 更新逻辑出现 bug(例如,由于整数溢出或条件判断错误),可能导致 min_vruntime 停滞不前或异常跳跃。这会使新任务和唤醒任务获得不合理的 vruntime,进而引发任务饥饿。在调试时,建议通过 tracepoint 或 /proc/sched_debug 监控 min_vruntime 的变化趋势。

4.5 红黑树与 cfs_rq 的协作关系

下表总结了红黑树和 cfs_rq 在 CFS 调度中的协作关系:

组件 职责 与 min_vruntime 的关系
红黑树 按 vruntime 排序所有就绪任务,提供 O(log n) 的插入/删除/查找。 最左节点的 vruntime 是更新 min_vruntime 的主要依据。
cfs_rq 管理队列元数据(nr_running、load、exec_clock),封装红黑树。 维护 min_vruntime 字段,并在每次任务入队/出队时更新。
min_vruntime 作为公平性基准,归一化新任务和唤醒任务的 vruntime。 由 cfs_rq 维护,受红黑树最左节点影响。