4. 红黑树基础:性质、旋转与内核实现

红黑树(Red-Black Tree)是 Linux 内核进程调度器 runqueue 中管理 CFS(完全公平调度器)就绪队列的核心数据结构。CFS 使用红黑树按进程的虚拟运行时间(vruntime)排序,实现 O(log n) 的插入、删除和查找操作。本章深入剖析红黑树的数学性质、旋转操作、插入与删除算法,并解读内核源码树中 rbtree.h 的实现细节。

学习目标:理解红黑树的 5 条核心性质;掌握左旋与右旋的几何变换;熟悉插入与删除的修复流程;能够在内核源码中定位红黑树操作的关键函数。

4.1 红黑树的 5 条核心性质

红黑树是一棵自平衡的二叉搜索树,每个节点额外存储一个颜色位(红色或黑色)。它通过以下 5 条不变量保证树的高度不超过 2log₂(n+1),从而确保最坏情况下的操作效率。

编号 性质描述 违反后果
1 每个节点要么是红色,要么是黑色 颜色属性失效
2 根节点必须是黑色 路径黑高不一致
3 每个叶子节点(NIL)是黑色 空指针视为黑色哨兵
4 红色节点的两个子节点必须是黑色(不能有连续红色) 红色节点出现红色子节点
5 从任意节点到其每个叶子节点的所有路径包含相同数量的黑色节点(黑高相等) 树不平衡

性质 4 和性质 5 是红黑树平衡性的核心保证。性质 4 限制了红色节点不能相邻,性质 5 强制所有路径的黑色节点数相等,两者共同约束树的高度。

内核视角:在 CFS 调度器中,红黑树的键值是进程的 vruntime。性质 5 保证了最左节点(vruntime 最小)的查找路径不会过长,从而快速选出下一个要运行的进程。
红黑树结构示例(满足 5 条性质) 25 (黑) 15 (红) 35 (红) 10 (黑) 20 (黑) 30 (黑) 40 (黑) NIL NIL NIL NIL NIL NIL NIL NIL 每条从根到 NIL 的路径都有 2 个黑色节点(黑高 = 2),且无连续红色节点

4.2 旋转操作:左旋与右旋

旋转是红黑树维持平衡的基本操作,它改变节点之间的父子关系,但不破坏二叉搜索树的中序有序性。旋转分为左旋(Left Rotate)和右旋(Right Rotate),两者互为逆操作。

4.2.1 左旋(Left Rotate)

左旋以节点 x 为轴心,将 x 的右子节点 y 提升为父节点,x 下降为 y 的左子节点。y 原来的左子树变为 x 的右子树。

4.2.2 右旋(Right Rotate)

右旋以节点 y 为轴心,将 y 的左子节点 x 提升为父节点,y 下降为 x 的右子节点。x 原来的右子树变为 y 的左子树。

左旋与右旋操作对比 左旋(以 x 为轴心) x y β α → y x β α 右旋(以 y 为轴心) y x γ α → x y γ α
内核实现要点:在 rbtree.h 中,旋转操作通过修改节点的 rb_left、rb_right 和 rb_parent 指针完成。旋转后需要更新父节点对子节点的引用,并重新设置根节点(如果旋转涉及根)。

4.3 插入算法与修复流程

红黑树的插入分为两步:首先按照二叉搜索树的规则插入新节点(初始颜色为红色),然后通过修复操作恢复红黑树性质。插入可能违反性质 2(根为黑色)或性质 4(无连续红色),修复流程根据叔节点(uncle)的颜色分为三种情况。

4.3.1 插入步骤

  1. BST 插入:从根开始,比较键值,找到插入位置,将新节点作为红色叶子插入。
  2. 检查父节点颜色:如果父节点是黑色,插入完成(性质 4 未违反)。
  3. 修复循环:当父节点是红色时,进入修复流程。

4.3.2 修复的三种情况

情况 条件 操作
情况 1 叔节点是红色 将父节点和叔节点改为黑色,祖父节点改为红色,将当前节点上移两层到祖父节点
情况 2 叔节点是黑色,且当前节点是父节点的右子节点(左-右型) 对父节点左旋,转换为情况 3
情况 3 叔节点是黑色,且当前节点是父节点的左子节点(左-左型) 将父节点改为黑色,祖父节点改为红色,对祖父节点右旋
对称性:上述情况假设父节点是祖父节点的左子节点。如果父节点是右子节点,则操作完全对称(左旋变右旋,右旋变左旋)。
红黑树插入修复流程图 插入红色节点 父节点是红色? 否 插入完成 叔节点是红色? 是 情况1:变色+上移 当前节点是 右子节点? 是 情况2:左旋父节点 情况3:变色+右旋祖父节点

4.4 删除算法与修复流程

红黑树的删除比插入更复杂。删除节点后,如果被删除节点是黑色,则会破坏性质 5(黑高相等),需要调用修复函数 __rb_erase_color 来恢复平衡。删除修复根据兄弟节点(sibling)的颜色和子节点情况分为四种情况。

4.4.1 删除步骤

  1. BST 删除:找到要删除的节点。如果节点有两个子节点,用后继节点(右子树最小节点)替换值,然后删除后继节点。
  2. 记录颜色:记录实际被删除节点(或替换节点)的原始颜色。
  3. 修复条件:如果被删除节点是黑色,则调用修复函数。

4.4.2 删除修复的四种情况

情况 条件 操作
情况 1 兄弟节点是红色 将兄弟改为黑色,父节点改为红色,对父节点左旋(或右旋),更新兄弟引用
情况 2 兄弟节点是黑色,且兄弟的两个子节点都是黑色 将兄弟改为红色,将当前节点上移到父节点
情况 3 兄弟节点是黑色,兄弟的左子节点是红色,右子节点是黑色 将兄弟的左子节点改为黑色,兄弟改为红色,对兄弟右旋,更新兄弟引用
情况 4 兄弟节点是黑色,兄弟的右子节点是红色 将兄弟颜色改为父节点颜色,父节点改为黑色,兄弟的右子节点改为黑色,对父节点左旋(或右旋),结束
内核优化:Linux 内核的 rbtree.h 使用 __rb_erase_color 函数实现删除修复。该函数通过循环和 goto 语句高效处理四种情况,避免递归调用,适合内核的实时性要求。

4.5 内核中的红黑树实现:rbtree.h

Linux 内核的红黑树实现位于 include/linux/rbtree.h 和 lib/rbtree.c。内核版本(如 6.x)对经典实现进行了优化,使用 rb_node 结构体和 rb_root 根节点结构。

4.5.1 核心数据结构

// include/linux/rbtree.h
struct rb_node {
    unsigned long  __rb_parent_color;  // 父节点指针与颜色位(最低位)
    struct rb_node *rb_right;
    struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));

struct rb_root {
    struct rb_node *rb_node;  // 指向根节点,空树时为 NULL
};

内核将父节点指针和颜色位压缩在一个 unsigned long 字段中:最低位(bit 0)存储颜色(0 表示红色,1 表示黑色),高位存储父节点地址。这种技巧节省了内存,并利用对齐特性保证指针地址的最低位始终为 0。

4.5.2 关键宏与内联函数

宏/函数 功能 实现要点
rb_parent(r) 获取节点的父节点指针 清除最低位:(struct rb_node *)((r)->__rb_parent_color & ~3)
rb_color(r) 获取节点颜色 读取最低位:((r)->__rb_parent_color & 1)
rb_set_color(r, c) 设置节点颜色 修改最低位,保留父指针高位
rb_link_node(node, parent, rb_link) 将节点链接到树中 设置父节点和左右指针,颜色初始化为红色
rb_insert_color(node, root) 插入后修复红黑树性质 实现插入修复的三种情况 ← 上一章 📖 返回目录 下一章 →