4. 红黑树基础:性质、旋转与内核实现
红黑树(Red-Black Tree)是 Linux 内核进程调度器 runqueue 中管理 CFS(完全公平调度器)就绪队列的核心数据结构。CFS 使用红黑树按进程的虚拟运行时间(vruntime)排序,实现 O(log n) 的插入、删除和查找操作。本章深入剖析红黑树的数学性质、旋转操作、插入与删除算法,并解读内核源码树中 rbtree.h 的实现细节。
4.1 红黑树的 5 条核心性质
红黑树是一棵自平衡的二叉搜索树,每个节点额外存储一个颜色位(红色或黑色)。它通过以下 5 条不变量保证树的高度不超过 2log₂(n+1),从而确保最坏情况下的操作效率。
| 编号 | 性质描述 | 违反后果 |
|---|---|---|
| 1 | 每个节点要么是红色,要么是黑色 | 颜色属性失效 |
| 2 | 根节点必须是黑色 | 路径黑高不一致 |
| 3 | 每个叶子节点(NIL)是黑色 | 空指针视为黑色哨兵 |
| 4 | 红色节点的两个子节点必须是黑色(不能有连续红色) | 红色节点出现红色子节点 |
| 5 | 从任意节点到其每个叶子节点的所有路径包含相同数量的黑色节点(黑高相等) | 树不平衡 |
性质 4 和性质 5 是红黑树平衡性的核心保证。性质 4 限制了红色节点不能相邻,性质 5 强制所有路径的黑色节点数相等,两者共同约束树的高度。
vruntime。性质 5 保证了最左节点(vruntime 最小)的查找路径不会过长,从而快速选出下一个要运行的进程。
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 的左子树。
rbtree.h 中,旋转操作通过修改节点的 rb_left、rb_right 和 rb_parent 指针完成。旋转后需要更新父节点对子节点的引用,并重新设置根节点(如果旋转涉及根)。
4.3 插入算法与修复流程
红黑树的插入分为两步:首先按照二叉搜索树的规则插入新节点(初始颜色为红色),然后通过修复操作恢复红黑树性质。插入可能违反性质 2(根为黑色)或性质 4(无连续红色),修复流程根据叔节点(uncle)的颜色分为三种情况。
4.3.1 插入步骤
- BST 插入:从根开始,比较键值,找到插入位置,将新节点作为红色叶子插入。
- 检查父节点颜色:如果父节点是黑色,插入完成(性质 4 未违反)。
- 修复循环:当父节点是红色时,进入修复流程。
4.3.2 修复的三种情况
| 情况 | 条件 | 操作 |
|---|---|---|
| 情况 1 | 叔节点是红色 | 将父节点和叔节点改为黑色,祖父节点改为红色,将当前节点上移两层到祖父节点 |
| 情况 2 | 叔节点是黑色,且当前节点是父节点的右子节点(左-右型) | 对父节点左旋,转换为情况 3 |
| 情况 3 | 叔节点是黑色,且当前节点是父节点的左子节点(左-左型) | 将父节点改为黑色,祖父节点改为红色,对祖父节点右旋 |
4.4 删除算法与修复流程
红黑树的删除比插入更复杂。删除节点后,如果被删除节点是黑色,则会破坏性质 5(黑高相等),需要调用修复函数 __rb_erase_color 来恢复平衡。删除修复根据兄弟节点(sibling)的颜色和子节点情况分为四种情况。
4.4.1 删除步骤
- BST 删除:找到要删除的节点。如果节点有两个子节点,用后继节点(右子树最小节点)替换值,然后删除后继节点。
- 记录颜色:记录实际被删除节点(或替换节点)的原始颜色。
- 修复条件:如果被删除节点是黑色,则调用修复函数。
4.4.2 删除修复的四种情况
| 情况 | 条件 | 操作 |
|---|---|---|
| 情况 1 | 兄弟节点是红色 | 将兄弟改为黑色,父节点改为红色,对父节点左旋(或右旋),更新兄弟引用 |
| 情况 2 | 兄弟节点是黑色,且兄弟的两个子节点都是黑色 | 将兄弟改为红色,将当前节点上移到父节点 |
| 情况 3 | 兄弟节点是黑色,兄弟的左子节点是红色,右子节点是黑色 | 将兄弟的左子节点改为黑色,兄弟改为红色,对兄弟右旋,更新兄弟引用 |
| 情况 4 | 兄弟节点是黑色,兄弟的右子节点是红色 | 将兄弟颜色改为父节点颜色,父节点改为黑色,兄弟的右子节点改为黑色,对父节点左旋(或右旋),结束 |
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) |
插入后修复红黑树性质 | 实现插入修复的三种情况 |