绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Linux内核中的红黑树
2020-05-19 09:31:42

红黑树是平衡二叉树的一种,它有很好的性质,树中的结点都是有序的,而且因为它本身就是平衡的,所以查找也不会出现非常恶劣的情况,基于二叉树的操作的时间复杂度是O(log(N))。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的。

先到include/linux/rbtree.h中看一下红黑树的一些定义,如下:

struct rb_node
{
unsigned long rb_parent_color;
#define RB_RED 0
#define RB_BLACK 1
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));

struct rb_root只是struct rb_node*的一个包装,这样做的好处是看起来不用传递二级指针了。不错,很简单。再看一下下面几个重要的宏,细心的你一定会发现,rb_parent_color其实没那么简单,Andrea Arcangeli在这里使用了一个小的技巧,不过非常棒。正如名字所暗示,这个成员其实包含指向parent的指针和此结点的颜色!它是怎么做到的呢?很简单,对齐起了作用。既然是sizeof(long)大小的对齐,那么在IA-32上,任何rb_node结构体的地址的低两位肯定都是零,与其空着不用,还不如用它们表示颜色,反正颜色就两种,其实一位就已经够了。

这样,提取parent指针只要把rb_parent_color成员的低两位清零即可:

  1. #define rb_parent(r) ((struct rb_node *)((r)->rb_parent_color & ~3))
复制代码

取颜色只要看后一位即可:

  1. #define rb_color(r) ((r)->rb_parent_color & 1)
复制代码

测试颜色和设置颜色也是水到渠成的事了。需要特别指出的是下面的一个内联函数:

  1. static inline void rb_link_node(struct rb_node * node, struct rb_node * parent, struct rb_node ** rb_link);
复制代码

它把parent设为node的父结点,并且让rb_link指向node。

我们把重点集中在lib/rbtree.c上,看看一些和红黑树相关的重要算法。开始之前我们一起回忆一下红黑树的规则:


1. 每个结点要么是红色要么是黑色;

2. 根结点必须是黑色;

3. 红结点如果有孩子,其孩子必须都是黑色;

4. 从根结点到叶子的每条路径必须包含相同数目的黑结点。

这四条规则可以限制一棵排序树是平衡的。

__rb_rotate_left是把以root为根的树中的node结点进行左旋,__rb_rotate_right是进行右旋。这两个函数是为后面的插入和删除服务,而不是为外部提供接口。

新插入的结点都设为叶子,染成红色,插入后如果破坏了上述规则,通过调整颜色和旋转可以恢复,二叉树又重新平衡。插入操作的接口函数是

  1. void rb_insert_color(struct rb_node *node, struct rb_root *root);
复制代码

它把已确定父结点的node结点融入到以root为根的红黑树中,具体算法的分析可以参考[1]中第14.3节,这里的实现和书中的讲解几乎完全一样。怎么确定node的父结点应该在调用rb_insert_color之前通过手工迭带完成。值得指出的一点是,虽然插入操作需要一个循环迭代,但是总的旋转次数不会超过两次!所以效率还是很乐观的。

删除操作多多少少都有点麻烦,它要先执行像普通二叉查找树的“删除”,然后根据删除结点的颜色来判断是否执行进一步的操作。删除的接口是:

  1. void rb_erase(struct rb_node *node, struct rb_root *root);
复制代码

其实它并没有真正删除node,而只是让它和以root为根的树脱离关系,后它还要判断是否调用__rb_erase_color来调整。具体算法的讲解看参考[1]中第13.3和14.4节,__rb_erase_color对应书中的RB-DELETE-FIXUP,此处的实现和书上也基本上一致。

其余的几个接口就比较简单了。

  1. struct rb_node *rb_first(struct rb_root *root);
复制代码

在以root为根的树中找出并返回小的那个结点,只要从根结点一直向左走就是了。

  1. struct rb_node *rb_last(struct rb_root *root);
复制代码

是找出并返回大的那个,一直向右走。

  1. struct rb_node *rb_next(struct rb_node *node);
复制代码

返回node在树中的后继,这个稍微复杂一点。如果node的右孩子不为空,它只要返回node的右子树中小的结点即可;如果为空,它要向上查找,找到迭带结点是其父亲的左孩子的结点,返回父结点。如果一直上述到了根结点,返回NULL。

  1. struct rb_node *rb_prev(struct rb_node *node);
复制代码

返回node的前驱,和rb_next中的操作对称。

  1. void rb_replace_node(struct rb_node *victim, struct rb_node *new, struct rb_root *root);
复制代码

用new替换以root为根的树中的victim结点。

红黑树接口使用的一个典型例子如下:

  1. static inline struct page * rb_search_page_cache(struct inode * inode,
  2.                                                  unsigned long offset)
  3. {
  4.         struct rb_node * n = inode->i_rb_page_cache.rb_node;
  5.         struct page * page;

  6.         while (n)
  7.         {
  8.                 page = rb_entry(n, struct page, rb_page_cache);

  9.                 if (offset < page->offset)
  10.                         n = n->rb_left;
  11.                 else if (offset > page->offset)
  12.                         n = n->rb_right;
  13.                 else
  14.                         return page;
  15.         }
  16.         return NULL;
  17. }

  18. static inline struct page * __rb_insert_page_cache(struct inode * inode,
  19.                                                    unsigned long offset,
  20.                                                    struct rb_node * node)
  21. {
  22.         struct rb_node ** p = &inode->i_rb_page_cache.rb_node;
  23.         struct rb_node * parent = NULL;
  24.         struct page * page;

  25.         while (*p)
  26.         {
  27.                 parent = *p;
  28.                 page = rb_entry(parent, struct page, rb_page_cache);

  29.                 if (offset < page->offset)
  30.                         p = &(*p)->rb_left;
  31.                 else if (offset > page->offset)
  32.                         p = &(*p)->rb_right;
  33.                 else
  34.                         return page;
  35.         }

  36.         rb_link_node(node, parent, p);

  37.         return NULL;
  38. }

  39. static inline struct page * rb_insert_page_cache(struct inode * inode,
  40.                                                  unsigned long offset,
  41.                                                  struct rb_node * node)
  42. {
  43.         struct page * ret;
  44.         if ((ret = __rb_insert_page_cache(inode, offset, node)))
  45.                 goto out;
  46.         rb_insert_color(node, &inode->i_rb_page_cache);
  47. out:
  48.         return ret;
  49. }
复制代码

因为红黑树的这些良好性质和实现中接口的简易性,它被广泛应用到内核编程中,大大提高了内核的效率。



参考资料:


1. Introduction to Algorithms, Thomas H. Cormen, Charles E. Leiserson, and Ronald L. Rivest, MIT Press.

2. Understanding the Linux Kernel, 3rd Edition, Daniel P. Bovet, Marco Cesati, O'Reilly.

3. Linux Kernel 2.6.19 source code.  


文章来源CU社区:Linux内核中的红黑树


分享好友

分享这个小栈给你的朋友们,一起进步吧。

内核源码
创建时间:2020-05-18 13:36:55
内核源码精华帖内容汇总
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 飘絮絮絮丶
    专家
戳我,来吐槽~