Linux内核进程调度CFS算法与组调度配置案例 - 第4节

4.1 红黑树:CFS 调度器的核心数据结构

CFS(Completely Fair Scheduler)使用红黑树(Red-Black Tree)来组织就绪队列中的进程。红黑树是一种自平衡二叉搜索树,其插入、删除和查找操作的时间复杂度均为 O(log n),能够高效地支持 CFS 对进程 vruntime 的排序与选择。

核心概念: 红黑树以进程的 vruntime 为键值进行排序。最左侧节点(即最小 vruntime 节点)始终是下一个被调度执行的进程。

4.1.1 红黑树的 5 条基本性质

性质编号性质描述作用
1每个节点要么是红色,要么是黑色定义颜色属性
2根节点是黑色保证树结构稳定
3所有叶子节点(NIL)都是黑色简化边界处理
4红色节点的两个子节点都是黑色防止连续红色节点
5从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点保证树大致平衡

4.1.2 红黑树插入操作

红黑树的插入分为两个阶段:

  1. BST 插入: 按照二叉搜索树规则,将新节点插入到合适位置,初始颜色设为红色。
  2. 修复平衡: 通过旋转和颜色调整,恢复红黑树的 5 条性质。

插入修复的 3 种主要情况:

  • 情况 1: 叔节点为红色 → 父节点和叔节点变黑,祖父节点变红,向上递归。
  • 情况 2: 叔节点为黑色,且新节点是右子节点 → 左旋父节点,转为情况 3。
  • 情况 3: 叔节点为黑色,且新节点是左子节点 → 右旋祖父节点,交换颜色。
红黑树插入修复流程 插入新节点(红) 父节点颜色? 黑色:无需修复 红色:检查叔节点 叔节点红/黑?

4.2 __enqueue_entity 与 __dequeue_entity 实现

这两个函数是 CFS 操作红黑树的底层接口,负责将调度实体(sched_entity)插入或移除就绪队列的红黑树。

4.2.1 __enqueue_entity 实现

当进程变为可运行状态时,调用此函数将其加入红黑树:

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    int leftmost = 1;

    /* 1. 在红黑树中查找插入位置 */
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);
        if (entity_before(se, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = 0;
        }
    }

    /* 2. 插入节点并更新最左标记 */
    rb_link_node(&se->run_node, parent, link);
    rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);

    if (leftmost)
        cfs_rq->rb_leftmost = &se->run_node;
}
关键点: entity_before() 比较两个实体的 vruntime,确保红黑树按 vruntime 升序排列。leftmost 标记用于快速获取最小 vruntime 节点。

4.2.2 __dequeue_entity 实现

当进程被抢占或主动睡眠时,将其从红黑树中移除:

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    /* 1. 如果移除的是最左节点,更新最左指针 */
    if (cfs_rq->rb_leftmost == &se->run_node) {
        struct rb_node *next = rb_next(&se->run_node);
        cfs_rq->rb_leftmost = next;
    }

    /* 2. 从红黑树中删除节点 */
    rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}
注意: 删除操作后,红黑树的平衡由 rb_erase() 内部的红黑树算法自动维护。CFS 不需要手动触发重新平衡。

4.3 最小 vruntime 节点获取

CFS 每次调度时,需要快速找到 vruntime 最小的进程。红黑树的最左侧节点即为目标。

4.3.1 获取方式

方法代码实现时间复杂度
直接访问最左指针cfs_rq->rb_leftmostO(1)
红黑树遍历查找rb_first(&cfs_rq->tasks_timeline)O(log n)

CFS 在 __pick_first_entity() 函数中优先使用 rb_leftmost 缓存:

struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = cfs_rq->rb_leftmost;

    if (!left)
        return NULL;

    return rb_entry(left, struct sched_entity, run_node);
}
红黑树最小 vruntime 节点获取 N2 N1 N3 N0 rb_leftmost → N0 N0 的 vruntime 最小 下次调度选择 N0

4.4 红黑树操作与 CFS 调度流程整合

以下表格总结了红黑树操作在 CFS 调度周期中的位置:

调度阶段红黑树操作调用函数
进程唤醒/创建插入节点__enqueue_entity
进程主动睡眠删除节点__dequeue_entity
时钟 tick 更新更新 vruntime,可能触发节点位置调整update_curr + 重新入队
调度决策获取最小 vruntime 节点__pick_first_entity
进程被抢占删除节点(出队)dequeue_task_fair
性能保障: 红黑树的 O(log n) 操作保证了 CFS 在大规模进程(数千个)下仍能保持高效的调度决策。实际测试中,1000 个进程的红黑树查找深度不超过 10 层。