linux红黑树结构体为什么只有3个指针?

linux红黑树结构体为什么只有3个指针?

struct rb_node {
  struct rb_node *rb_parent_color;
  struct rb_node *rb_right;
  struct rb_node *rb_left;
};

为什么 rb_node 里没有提供存储key, value节点信息的指针?它是怎么记录红黑树节点颜色信息的?只有这些东西是怎么做到让其它结构体继承并调用的?

前言

众所周知,红黑树是一种自平衡的二叉搜索树,用于保持数据的有序性并支持高效的插入、删除和查找操作。它遵循以下性质:

节点颜色:每个节点是红色或黑色。
根节点:根节点是黑色。
红色节点限制:红色节点不能有红色子节点,即红色节点的两个子节点必须是黑色。
黑色平衡:从任何节点到其所有子节点的路径上,黑色节点的数量必须相同。
balabala…

红黑树通过其自平衡特性,能够确保对数时间复杂度的操作,满足高效的插入、删除和查找需求,操作的平均时间复杂度为O(logn)。总之红黑树在许多计算机科学应用中非常重要,如操作系统的进程调度、数据库索引和内存管理等。

不过本文不是要讲红黑树的原理,也不讲红黑树调整子树的代码实现,而是要讲为什么linux能提供让其它代码复用的红黑树实现。

Linux

在2003年发布的 Linux 2.6.0 内核版本,红黑树在 lib/rbtree.c 文件中首次引入并实现,成为内核的标准数据结构之一。红黑树提供一种高效的数据结构用于各种内核子系统,如进程调度、内存管理、文件系统等,成为了内核的一个重要组成部分。

然鹅,linux中定义的红黑树结构体真的就只有结构:指向parent以及左右节点的指针。没有作为排序依据的key,没有用于存放数据的value,与颜色相关的也只有一个指针变量。

// 真的就是这么定义的
struct rb_node {
  struct rb_node *rb_parent_color;
  struct rb_node *rb_right;
  struct rb_node *rb_left;
};

而大家的使用方式也很简单,只需要这么用即可

struct node {
  struct rb_node rbn;
  int key;
  int value;
}

大家编码时,通常将节点中的所有要素都定义在一起,或是将数据节点单独定义并作为结构节点的成员。而这种数据与结构分离的编码方式与大家的编码常识严重不符。

int main() {
  struct node n1;
  struct rb_node *rbn1 = n1.rbn.rb_left;
  return 0;
}

我们能很容易获取到 node 的 rb_node 的结构信息,然后获取到 rb_node 的 rb_left,但 rb_left 是 rb_node 类型的结构体,它里面是没有数据的。我们又该怎么获取到rb_left的数据呢?

于是经过我的仔细研究,发现linux这套红黑树的实现离不开C语言最大的特性:指针。

数据-结构分离

由于C语言在存储结构体的时候,会依据结构体成员大小,申请一小片连续的空间存放。例如,上述的rb_node占用12B大小的空间,node占用20B大小的空间。而对于存放顺序,则会依据成员定义的顺序在这片空间中存放。对于上面给出的定义,实际上是这么存储结构体实例的:
在这里插入图片描述
我们可以通过&node+0,&node+20,&node+24的方式计算出rb_node,key,value的地址,反过来,我们也可以用&rb_node-0,&key-20,&value-24的方式计算node的地址。

#include <stddef.h>

#define container_of(ptr, type, member) \
  ((type *)((char *)(ptr)-)offsetof(type, member))
  1. stddef.h中提供了offsetof(type, member)用于计算 member 在 type 中的偏移量。这个偏移量是从 type 结构体的起始地址到 member 成员的距离,单位是字节。
  2. (char *)(ptr)将 ptr 转换为 char *: char * 类型的指针允许按字节操作指针地址。这是因为 char 类型的大小为 1 字节,所以 char * 可以用于进行字节级别的指针运算。
  3. (char *)(ptr) - offsetof(type, member)计算结构体的起始地址: 从 ptr 的地址中减去 member 的偏移量,得到 type 结构体的起始地址。
  4. ((type *)((char *)(ptr) - offsetof(type, member)))将计算得到的地址转换为 type: 最后,将得到的地址转换回 type * 指针,这样你就可以获得指向包含该成员的结构体的指针。

于是我们便能通过container_of(rbn, struct node, rbn)的方式获取包含这个 rb_node 的 node 的指针。

使用时,一般将 rb_node 作为自己 node 的第一个成员,我想,这大概是考虑到计算偏移量时的优化。如果不开启编译优化,ALU只需计算 &rb_node - 0;而开启编译优化后,编译器推理发现 rb_node 与 node 地址相同,便可以省去这次地址计算。

颜色编码

接下来我们分析 rb_node 是怎么巧妙的存储红黑树节点颜色信息的。

在 rb_node 中,节点颜色信息通过编码在 rb_parent_color 指针中来存储。标准的 C 语言指针不能直接存储额外的元数据,但内核开发者常常采用一些巧妙的技术,将颜色信息隐藏在指针字段中。

  1. 位操作:颜色信息被编码在 rb_parent_color 指针的最低有效位(LSB),其他位则保留用于存储父节点地址。这种方法巧妙的将节点颜色信息与父节点指针合并存储。
  2. 掩码提取:使用掩码(mask)来提取或设置颜色信息和父节点指针,确保了在访问节点颜色时不会干扰实际的父节点指针,反之亦然。
#include <stdint.h>

struct rb_node {
  struct rb_node *rb_parent_color;
  struct rb_node *rb_right;
  struct rb_node *rb_left;
};

// 颜色常量
#define RED 1
#define BLACK 0

// 设置节点颜色
void rb_set_color(struct rb_node *node, int color) {
  uintptr_t parent_color = (uintptr_t)(node->rb_parent_color);
  node->rb_parent_color = (struct rb_node *)(parent_color | color);
}

// 获取节点颜色
int rb_get_color(struct rb_node *node) {
  uintptr_t parent_color = (uintptr_t)(node->rb_parent_color);
  return (parent_color & 1);
}

// 设置父节点
void rb_set_parent(struct rb_node *node, struct rb_node *parent) {
  uintptr_t parent_color = (uintptr_t)(node->rb_parent_color);
  node->rb_parent_color =
      (struct rb_node *)((parent_color & 1) | (uintptr_t)parent);
}

// 获取父节点
struct rb_node *rb_get_parent(struct rb_node *node) {
  uintptr_t parent_color = (uintptr_t)(node->rb_parent_color);
  return (struct rb_node *)(parent_color & ~1);
}

示例

最后为了进一步方便大家理解,我提供了一份Windows也能跑的示例代码让大家跑着玩玩。

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

#define container_of(ptr, type, member) \
  ((type *)((char *)(ptr)-offsetof(type, member)))

void print_struct_area(const void *addr, size_t size) {
  const unsigned char *ptr = (const unsigned char *)addr;
  for (size_t i = 0; i < size; ++i) {
    printf("0x%02x ", ptr[i]);
    if ((i + 1) % 4 == 0) {
      printf("\n");
    }
  }
  printf("\n");
}

struct rb_node {
  int position_flag_value; // 用于给大家展示rb_node调试打印信息
  struct rb_node *rb_parent_color;
  struct rb_node *rb_right;
  struct rb_node *rb_left;
};

struct node {
  int key;
  int value;
  struct rb_node rbn;
};

int main() {
  struct node *n1 = (struct node *)calloc(1, sizeof(struct node));
  if (n1) {
    n1->key = 1;
    n1->value = 1;
    n1->rbn.position_flag_value = 4;
    n1->rbn.rb_parent_color = NULL;
    n1->rbn.rb_right = NULL;
    n1->rbn.rb_left = NULL;
  }
  struct rb_node *rbn = &n1->rbn;
  struct node *n2 = container_of(rbn, struct node, rbn);
  print_struct_area(n1, sizeof(struct node));
  print_struct_area(n2, sizeof(struct node));
  free(n1);
  return 0;
}

运行结果

0x01 0x00 0x00 0x00 
0x01 0x00 0x00 0x00
0x04 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

0x01 0x00 0x00 0x00
0x01 0x00 0x00 0x00
0x04 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值