4. 红黑树节点结构:struct rb_node 详解

在 Linux 内核进程调度器的红黑树实现中,struct rb_node 是构成红黑树的基本单元。每个等待运行的进程(调度实体)都通过嵌入一个 rb_node 结构体来组织到红黑树中。该结构体的设计极其精巧,尤其是 __rb_parent_color 字段,它通过一个字段同时存储了父节点指针和节点颜色信息,极大地节省了内存空间。

4.1 struct rb_node 结构定义

内核中 struct rb_node 的定义位于 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))));
关键设计思想:利用指针地址的低位对齐特性(long 类型指针地址最低 2 位始终为 0),将红黑树的颜色信息(红/黑)存储在最低位,从而用一个字段同时承载父节点指针和颜色标记。

4.2 __rb_parent_color 字段的巧妙设计

该字段是红黑树节点设计的精髓所在。它通过位操作实现了“一字段两用”:

位域 用途 说明
Bit 0(最低位) 颜色标记 0 表示红色(Red),1 表示黑色(Black)
Bit 1 ~ Bit 63 父节点指针 存储父节点的实际地址(右移 1 位后的值)

这种设计依赖于一个前提:所有 struct rb_node 结构体在内存中都是按 sizeof(long) 对齐的。在 64 位系统上,这意味着地址的最低 3 位(bit 0、1、2)始终为 0,因此最低位可以安全地用于存储颜色信息。

4.3 核心操作宏与内联函数

内核提供了一组宏和内联函数来操作 __rb_parent_color 字段,这些操作是红黑树插入和查找算法的基础:

// 从 __rb_parent_color 中提取父节点指针(清除最低位)
#define rb_parent(r)   ((struct rb_node *)((r)->__rb_parent_color & ~3))

// 从 __rb_parent_color 中提取颜色值(仅保留最低位)
#define rb_color(r)    ((r)->__rb_parent_color & 1)

// 判断节点是否为红色
#define rb_is_red(r)   (!rb_color(r))

// 判断节点是否为黑色
#define rb_is_black(r)  rb_color(r)

// 设置节点颜色为红色(将最低位置 0)
static inline void rb_set_red(struct rb_node *rb) {
    rb->__rb_parent_color &= ~1;
}

// 设置节点颜色为黑色(将最低位置 1)
static inline void rb_set_black(struct rb_node *rb) {
    rb->__rb_parent_color |= 1;
}

// 设置父节点指针(保留颜色位不变)
static inline void rb_set_parent(struct rb_node *rb, struct rb_node *p) {
    rb->__rb_parent_color = (rb->__rb_parent_color & 3) | (unsigned long)p;
}

// 同时设置父节点和颜色
static inline void rb_set_parent_color(struct rb_node *rb,
                                       struct rb_node *p, int color) {
    rb->__rb_parent_color = (unsigned long)p | color;
}
位运算技巧解析:宏 rb_parent(r) 使用 & ~3 而不是 & ~1,这是因为在 64 位系统上,为了兼容性,内核会清除最低 2 位(bit 0 和 bit 1)。实际上,颜色信息只占用 bit 0,bit 1 保留未使用,但清除 bit 1 可以确保指针的正确性。

4.4 字段设计的内存节省效果

这种设计带来的内存节省在进程调度器的大规模红黑树中非常显著:

对比项 传统设计(分开存储) 内核设计(合并存储) 节省量
父节点指针 8 字节(64 位) 8 字节(与颜色共用) 0
颜色字段 4 字节(int) 0 字节(嵌入指针低位) 4 字节
节点总大小 28 字节(含 padding) 24 字节(3 个指针) 4 字节
100 万节点总内存 ~28 MB ~24 MB ~4 MB
注意事项:直接使用 rb_parent(r) 宏时,必须确保传入的节点指针 r 不为 NULL。如果节点是根节点,其父节点指针为 NULL(即 __rb_parent_color 的低位仅包含颜色信息,高位为 0)。内核代码中通常通过 RB_EMPTY_ROOT 等宏来检查根节点。

4.5 节点结构内存布局示意图

下图展示了 struct rb_node 在 64 位系统中的内存布局,以及 __rb_parent_color 字段的位分配情况:

struct rb_node 内存布局(64位系统) struct rb_node(24 字节) __rb_parent_color 8 字节(unsigned long) Bit 1 ~ 63:父节点指针 Bit 0:颜色(0=红,1=黑) 颜色位 rb_right 8 字节(右孩子指针) rb_left 8 字节(左孩子指针) 利用指针地址低位对齐特性,将颜色信息嵌入父节点指针的最低有效位,节省 4 字节/节点

4.6 插入操作中的颜色与父节点操作流程

在红黑树插入过程中,__rb_parent_color 字段的更新是核心操作之一。以下流程图展示了插入一个新节点时,该字段的初始化与调整过程:

插入节点时 __rb_parent_color 字段操作流程 步骤1:新节点初始化为红色 __rb_parent_color = 父节点地址 | 0 步骤2:插入到红黑树中 通过 rb_link_node() 设置父节点指针 步骤3:检查红黑树性质是否被破坏 父节点为红色 → 需要调整(连续红色节点违规) 步骤4:执行颜色调整与旋转 通过 rb_set_red() / rb_set_black() 更新颜色位 步骤5:根节点强制设为黑色 rb_set_black(root) — 保证根节点始终为黑色 整个过程中,__rb_parent_color 字段通过位操作同时维护父节点指针和颜色状态,无需额外存储空间

4.7 查找操作中的颜色与父节点使用

在查找操作中,__rb_parent_color 字段主要用于以下场景:

  • 父节点回溯:当需要从当前节点向上回溯到父节点时,使用 rb_parent(node) 宏提取父节点指针。
  • 颜色判断:在插入后的修复过程中,需要频繁判断叔父节点、祖父节点的颜色,通过 rb_is_red() 和 rb_is_black() 宏快速获取。
  • 根节点检测:当 rb_parent(node) == NULL 时,说明当前节点是树的根节点。
性能优势:由于颜色信息直接存储在指针的低位,读取颜色和父节点指针都只需要一次内存访问(读取 __rb_parent_color 字段),然后通过位运算即可分离出所需信息。相比分开存储需要两次内存访问,这种设计在缓存友好性和指令流水线效率上都有明显优势。

4.8 与其他红黑树实现的对比

实现方案 节点大小(64位) 颜色存储方式 内存访问次数 典型应用
Linux 内核实现 24 字节 嵌入父节点指针低位 1 次(读取后位运算) 进程调度器、内存管理
传统教科书实现 28 字节 独立的 int 或 enum 字段 2 次(指针 + 颜色) 教学示例、通用库
C++ STL map 实现 32 字节 独立的 bool 字段(含 padding) 2 次(指针 + 颜色) 用户态通用容器

通过这种精巧的位域设计,Linux 内核在保持红黑树完整功能的同时,将每个节点的内存开销降低了约 14%,这对于需要管理数十万甚至数百万调度实体的进程调度器来说,是一个非常重要的优化。