3. 红黑树数据结构基础
红黑树(Red-Black Tree)是 CFS 调度器选择就绪队列的核心数据结构。它是一棵自平衡的二叉查找树,能够在 O(log n) 时间内完成插入、删除和查找操作。理解红黑树的 5 大性质、旋转操作和颜色规则,是掌握 CFS 调度器“按虚拟时间排序”机制的前提。
面试高频提示: 面试官常问“为什么 CFS 选择红黑树而不是 AVL 树或堆?”核心答案:红黑树插入/删除的旋转次数更少(最多 2 次旋转),适合调度器频繁的进程入队/出队场景;AVL 树更严格平衡但旋转更多,堆无法支持按虚拟时间有序遍历。
3.1 红黑树 5 大性质
红黑树通过以下 5 条性质保证树的高度不超过 2log₂(n+1),从而确保 O(log n) 的时间复杂度:
| 编号 | 性质描述 | 违反后果 |
|---|---|---|
| 1 | 每个节点要么是红色,要么是黑色 | 颜色属性失效,无法进行平衡判断 |
| 2 | 根节点必须是黑色 | 可能导致红-红冲突向上传递到根 |
| 3 | 所有叶子节点(NIL)都是黑色 | 空指针视为黑色,简化双红修正 |
| 4 | 红色节点的两个子节点必须是黑色(不能有连续红色) | 出现红-红冲突,需旋转或变色修复 |
| 5 | 从任意节点到其每个叶子节点的所有路径包含相同数量的黑色节点 | 黑高不等,树失去平衡保证 |
黑高(Black-Height)概念: 从节点 x 到叶子节点的路径上黑色节点的数量(不包含 x 本身)。性质 5 保证了所有路径的黑高相等,这是红黑树平衡性的核心度量。
3.2 左旋与右旋操作
旋转是红黑树维持平衡的基本操作,在不破坏二叉查找树性质的前提下,改变节点间的父子关系。CFS 调度器在插入新进程或更新进程虚拟时间后,通过旋转修复红黑树。
3.2.1 左旋(Left Rotation)
左旋将节点 x 的右子节点 y 提升为 x 的父节点,x 成为 y 的左子节点。适用于右子树过重的情况。
/* 左旋伪代码 */
LEFT-ROTATE(T, x)
y = x.right // 设置 y
x.right = y.left // y 的左子树变为 x 的右子树
if y.left != T.nil
y.left.parent = x
y.parent = x.parent // 连接 x 的父节点到 y
if x.parent == T.nil
T.root = y
else if x == x.parent.left
x.parent.left = y
else
x.parent.right = y
y.left = x // 将 x 放在 y 的左边
x.parent = y
3.2.2 右旋(Right Rotation)
右旋是左旋的对称操作,将节点 y 的左子节点 x 提升为 y 的父节点,y 成为 x 的右子节点。适用于左子树过重的情况。
/* 右旋伪代码 */
RIGHT-ROTATE(T, y)
x = y.left // 设置 x
y.left = x.right // x 的右子树变为 y 的左子树
if x.right != T.nil
x.right.parent = y
x.parent = y.parent // 连接 y 的父节点到 x
if y.parent == T.nil
T.root = x
else if y == y.parent.left
y.parent.left = x
else
y.parent.right = x
x.right = y // 将 y 放在 x 的右边
y.parent = x
面试避坑: 旋转操作的时间复杂度是 O(1),只改变指针指向,不涉及节点颜色变化。颜色变化在旋转之后通过
RB-INSERT-FIXUP 或 RB-DELETE-FIXUP 完成。面试时不要混淆“旋转”和“变色”的先后顺序。
3.3 节点颜色规则与修复流程
红黑树的颜色规则是平衡的核心机制。插入新节点时,默认将节点染为红色,然后通过修复流程消除红-红冲突。删除节点时,如果删除的是黑色节点,会破坏黑高,需要通过修复恢复性质 5。
3.3.1 插入修复的 3 种情况
| 情况 | 触发条件 | 操作 | 后续 |
|---|---|---|---|
| Case 1 | 叔节点为红色 | 父节点、叔节点变黑,祖父节点变红 | 将祖父节点作为新插入节点继续向上检查 |
| Case 2 | 叔节点为黑色,且插入节点是右子节点 | 对父节点左旋,转换为 Case 3 | 旋转后原父节点变为子节点 |
| Case 3 | 叔节点为黑色,且插入节点是左子节点 | 父节点变黑,祖父节点变红,对祖父节点右旋 | 修复完成,树恢复平衡 |
CFS 调度器中的实际应用: 当新进程被创建并加入就绪队列时,内核调用
rb_link_node() 将节点插入红黑树,然后调用 rb_insert_color() 执行上述修复流程。插入键值为进程的 vruntime,红黑树按 vruntime 从小到大排序。
3.3.2 删除修复的核心思想
删除黑色节点会导致经过该节点的路径黑高减 1。修复策略是:将“额外黑色”向上传递,直到遇到红色节点(将其染黑)或到达根节点。删除修复涉及 4 种情况,核心操作包括旋转和变色。
3.4 红黑树在 CFS 中的角色总结
| CFS 组件 | 红黑树角色 | 键值 | 操作频率 |
|---|---|---|---|
| 就绪队列 (cfs_rq) | 按 vruntime 排序的进程树 | vruntime | 每次调度周期更新 |
| 调度实体 (sched_entity) | 红黑树节点 | vruntime | 进程创建/唤醒/睡眠时插入/删除 |
| pick_next_entity | 查找最左节点(最小 vruntime) | — | 每次调度触发 |
| update_curr | 更新 vruntime 后可能需要旋转 | — | 时钟 tick 触发 |
面试加分点: 红黑树在 CFS 中并非“完全公平”的数学保证,而是实现“按 vruntime 有序”的高效数据结构。真正的公平性由 vruntime 的递推公式和权重计算保证。红黑树只是让调度器能在 O(log n) 时间内找到“最需要运行”的进程。