红黑树及其在Linux内存管理中的应用详解
# 背景
普通的二叉查找树在极端情况下可退化成链表,此时的增删查效率比较低。平衡的二叉树(如AVL、红黑树等)能较好的解决这个问题。
本文首先介绍了红黑树的五个重要性质,然后详细介绍了红黑树重要的两个操作——插入和删除的原理。最后将红黑树与Linux中虚拟内存的管理进行结合,用代码展示了红黑树插入、删除的实现过程。
# 红黑树的性质
每颗红黑树必须满足的五条性质:
- 节点共有红、黑两种颜色
- 根节点是黑色
- 叶子节点是黑色(叶子是NIL节点)
- 若一个节点是红色,那它的两个孩子都是黑色(每个叶子到根的路径上不能有两个连续的红色节点)。
- 从任一节点到其子孙的所有简单路径都包含相同数目的黑色节点。
# 红黑树的查找路径长度
以上五条性质可以保证任一节点到其叶子的长度不会过长(即树的高度不至于过高)。具体来说,利用上述性质4和性质5的约束,可保证:
任一节点到其叶子的长度 <= 最短路径 × 2。
(注意,这里的最短路径指的是某节点到其叶子的最短路)
证明方法如下:
当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。
此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。举例说明一下,请看下图:
总结:通过证明红黑树任一节点其叶子节点的路径上界,表明红黑树大致上是平衡的。
# 红黑树的操作
红黑树的查找方法与二叉搜索树完全一样。所以插入和删除节点的方法前半部分与二叉搜索树完全一样,而后半部分添加了一些为了满足其性质的操作。
红黑树的插入、删除操作都依赖基本的平衡二叉树的旋转操作,这里就不介绍了,可以自行查阅相关资料。
(一)插入
当在一个红黑树插入一个节点时必须初始化此节点的颜色为红色,但如果新节点父节点也为红色,将会违背红黑树的性质:一条路径上不能出现相邻的两个红色节点。这时就要通过一系列操作来使红黑树保持平衡。
情况1:
当前红黑树为空,即插入的节点是根节点。此时需要将节点的颜色由红色变为黑色以满足性质2。
情况2:
插入节点N的父节点P为黑色,此时满足性质4和性质5,不需要调整。
情况3:
插入节点N的父节点P是红色,叔叔节点U也是红色,由性质4得P和U的父节点G为黑色。
此时由于N和P均为红色,破坏了性质4,需要进行调整。这种情况下,先将P和U的颜色染成黑色,再将G的颜色染成红色。此时经过G路径上的黑色节点的数量不变,性质5仍然满足。但需要注意的是G染成红色后,可能和它的父节点形成连续的红色节点,此时需要递归向上调整。
情况4:
某次调整后,子树中节点N的父节点为红色,叔叔节点U为黑色。节点N是P的左孩子,且节点P是G的左孩子。
此时对G进行右旋,调整P和G的位置,并交换颜色。使得性质4被满足。
注意:情况4中的节点N必然不能是新插入的节点,因为其父节点P是红色,只有P有两个叶子且均为黑色时才能满足:树中根节点到任一叶子的黑色节点数相同(性质5)。所以N必然新节点,但是N所处的为止有可能是调整过后导致。
情况5:
插入节点N的父节点为红色,叔叔节点U为黑色。节点N是P的右孩子,且节点P是G的左孩子。
此时先对节点P进行左旋,调整N与P的位置。接下来按照情况4来处理,以满足性质4。
注意:与情况4相同,N节点也必然不是新插入的节点,详见情况4-注意。
(二)删除
相较于插入操作,红黑树的删除操作则要更为复杂一些。因为红黑树是有序的,所以首先我们要保证删除某个节点N之后红黑树还是有序的。由于其删除操作过于繁琐,所以我们将它分为两个过程:(1)删除节点、(2)恢复平衡
(1)删除节点
删除操作首先要确定待删除节点有几个孩子,如果有两个孩子,则不能直接删除节点。而是要先找到该节点的前驱(即节点左子树中最大的节点)或者后继(即节点右子树中最小的节点),当然习惯上我们使用后继而不是前驱,然后将后继节点的值复制到要删除的节点中,然后再将原本的后继节点删除。
由于后继节点至多只有一个孩子节点(否则这个节点肯定不是子树中最大或者最小的),这样我们就把原来要删除的节点要修改两个孩子的问题转化为只调整一个节点的问题。能这样做的原因是我们并不关心最终被删除的节点是否是我们开始想要删除的那个节点,只要节点里的值最终被删除了就行,至于树的结构如何变化,这个不是我们关心的。
相比较之下,若要删除节点只有一个孩子,那么情况就相对简单很多了。在下面我会首先分析只有一个孩子的情况。
在展开说明之前,为了方便我们先进行一些节点名称代号的定义。这里假设最终被删除的节点为N,其孩子节点为C。后继节点为S,其左孩子为SL,右孩子为SR。接下来的讨论是建立在节点N被删除,节点S替换N的基础上进行的。
在上面的基础上,接下来就可以展开讨论了。红黑树删除有6种情况,分别是:
情况1
待删除节点N最多只有一个孩子节点,这种情况相对简单。
- 图1,只有一个孩子,此时N的孩子节点C同时也是后继节点S。C必然只能是红色(性质5),则N只能是黑色(性质4)。此时直接将C变成黑色继承N即可。
- 图2,直接删除N即可,不破坏红黑树的性质。
- 图3,直接删除N后,破坏了性质5,需要进行重新平衡,(2)恢复平衡中会进行讲解。
情况2
待删除节点N有两个孩子,这时首先需要找到N的后继节点S,此时又有两种情况。第一种S是N的右孩子,此时只需要将后继节点S继承N的位置,将N的左孩子(如果有)变为S的左孩子即可(S一定没有左孩子,见下方说明);第二种情况是S不是N的右孩子,说明是N的右子树中的点。下面我将用例子来说明这两种情况下判断是否需要重新平衡的依据是相同的:S是否有右子树。
S是N的右孩子的情况下,S是否有右子树的情况分别如下,注意X节点存在的意义就是使初始的红黑树是平衡的,所以我们不必要在意N的左子树的结构。
1)S有右子树,此时无论其他节点的颜色如果,删除N后直接使用S替换即可。所有可能的情况如下:
2)S没有右子树,此时替换就会产生冲突。
S是N的右子树中的点,S是否有右子树的情况分别如下。
1)S有右子树,此时无论其他节点的颜色如果,删除N后直接使用S替换即可,所有可能的情况都可以抽象为下面的三种情况。注意此时以下的结构中S和SR的颜色是确定的。
(2)S没有右子树,此时替换显然会产生冲突。
总结(1):
上面描述了删除操作的第一步——删除节点。这里有比较多的情况,我们会有疑惑:为什么S没有右子树的情况下会发生不平衡呢?
大家可以思考下,我总结规律就是:因为我们使用S替换N时相当于删除了S节点,如果S节点是叶子节点(即上述没有右子树),且S又是一个黑色节点,这时删掉S必然会破坏红黑树的性质5造成不平衡。
注意:后继节点S不可能有左孩子SL,因为如果有左孩子则它的左孩子更有可能成为待删除结点N的后继。因而S结点要不没有孩子,要不则只有右孩子。
(2)恢复平衡
在(1)删除节点中,我们得到结论:若找到的后继结点是叶子节点且颜色是黑色,就需要在替换完成后对红黑树进行再平衡。下面我们就针对这种情况进一步分析。
由于篇幅受限,这里仅拿后继节点S是待删除结点N的右孩子为例。即如下图所示:
情况1
后继节点S其兄弟节点X为红色,根据性质5,它肯定有两个黑色的孩子。此类情况做如下的调整:
情况2:
后继节点S其兄弟节点X为黑色,且有一个左孩子(可以断定左孩子是红色的)。此类情况做如下的调整:
情况3
后继节点S其兄弟节点X为黑色,且有一个右孩子(可以断定右孩子是红色的)。此类情况做如下的调整:
情况4
后继节点S其兄弟节点X为黑色,且有两个节点(可以断定,左右节点都是红色的)。这个和情况2是相同的。
情况5
后继节点S其兄弟节点X为黑色,且没有子节点。
此时N的子树是平衡了,但是删掉S之后,可能上面的树发生不平衡。所以需要递归向上寻找不平衡的点,例如:
总结(2):
红黑树删除节点之后的恢复平衡操作比较复杂,涉及的情况较多。
结论
红黑树在每一次插入和删除节点之后都会用O(logn)是时间对树的结构进行调整,以保持树的平衡。
红黑树的插入和删除操作比一般的二叉树操作要复杂许多,与红黑树的性质密切相关。掌握其插入删除操作对理解红黑树的概念有很大帮助。
# 红黑树的应用:Linux内核虚拟内存的管理
Linux系统借助MMU建立了虚拟内存系统,虚拟内存使得进程在系统层面拥有连续的地址空间,更使得进程可以分配到比物理内存更大的运行空间。例如在32位系统上,每个进程理论上可以获得2^32=4GB的地址空间。
使用红黑树有以下好处:
( 1)在红黑树中查找一个虚拟内存区域的速度快。使用双向链表查找需要O(n)的时间复杂度,红黑树中可以提升到O(logn)。
( 2)增加一个新的区域时,先在红黑树中找到刚好在新区域前面的区域,然后向链表和树中插入新区域,可以避免扫描链表。
进程的虚拟空间
在Linux内核中,进程的虚拟空间主要有两个数据结构来描述(定义在mm_types.h文件中):
- mm_strcut结构描述了一个进程的整个虚拟空间
- vm_area_struct结构描述了虚拟地址空间的一个虚拟内存区域(VMA)
一个进程的虚拟地址空间中可能有多个虚拟内存区域(以下简称VMA)。在mm_strcut结构中定义了两个指针:
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb; /* Red-Black tree of VMAs */
int map_count; /* number of VMAs */
...
}
mmap指向进程的第一个VMA,vm_area_struct中定义了前后指针,所以进程的VMAs组成一个双向链表。VMA使用起始地址和结束地址描述,链表按起始地址增序排序;mm_rb指向红黑树的根,树中的每个节点也是一个VMA,在树中所有的VMA其左孩子指针指向相邻的低地址VMA,右孩子指向相邻的高地址VMA。他们俩的组织结构如下图所示:
vm_area_struct结构体中主要是定义一个VMA的起始和结束的虚拟地址。
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb; /* 红黑树节点 */
struct mm_struct *vm_mm; /* The address space we belong to. */ /* 此VMA属于的进程地址空间 */
}
Linux内核中红黑树的实现
上面说mm_struct的结构成员mm_rb指向红黑树的根节点。红黑树的定义在/include/linux/rbtree.h中实现:
struct rb_node {
unsigned long __rb_parent_color; /* 颜色(Red/Black) */
struct rb_node *rb_right; /* The Right Children */
struct rb_node *rb_left; /* The Left Children */
}
struct rb_root {
struct rb_node *rb_node; /* 红黑树的根节点 */
};
Linux内核lib/rbtree.c中提供了红黑树的相关操作算法及其红黑树操作接口:
(一)插入操作的实现
为进程的虚拟空间添加一块新的VMA,需要执行的操作有:(1)找到VMA插入的正确位置。(2)在红黑树中插入该节点。(3)调整红黑树以满足其性质。
(1)寻找位置的实现方法可以拿mm/vmalloc.c中的__insert_vmap_area
函数举例,rb_link_node
实现了新VMA节点的插入。
完成之后需要插入的节点va->rb_node
的parent指针已经正确设置。但是整棵树还需要做调整以符合红黑树的性质。
struct rb_node **p = &vmap_area_root.rb_node;
struct rb_node *parent = NULL;
struct rb_node *tmp;
while (*p) { /* 寻找新节点插入的位置 */
struct vmap_area *tmp_va;
parent = *p;
tmp_va = rb_entry(parent, struct vmap_area, rb_node); /* 得到红黑树节点所属的VMA */
if (va->va_start < tmp_va->va_end)
p = &(*p)->rb_left;
else if (va->va_end > tmp_va->va_start)
p = &(*p)->rb_right;
else
BUG();
}
rb_link_node(&va->rb_node, parent, p); /* 设置 rb_node 的 parent 指针 */
(2)rb_insert_color
是供外部调用的插入函数,真正实现插入算法的函数是__rb_insert
,该函数同时实现了。
为了与红黑树操作理论对应,这里只展示新插入节点N在其祖父节点P的左子树的情况,右子树情况对称同理。
static __always_inline void
__rb_insert(struct rb_node *node, struct rb_root *root,
void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
struct rb_node *parent = rb_red_parent(node), *gparent, *tmp;
while (true) {
/*
* Loop invariant: node is red
*
* If there is a black parent, we are done.
* Otherwise, take some corrective action as we don't
* want a red root or two consecutive red nodes.
*/
/* while 循环退出的条件:(1)寻找到根节点 (2)parent 为黑色 */
//---------------------------------------------------------- 情况1 & 情况2
if (!parent) {
rb_set_parent_color(node, NULL, RB_BLACK);
break;
} else if (rb_is_black(parent))
break;
/* 到了这里,node 的父节点颜色一定是 Red */
gparent = rb_red_parent(parent);
tmp = gparent->rb_right;
if (parent != tmp) { /* parent == gparent->rb_left */
if (tmp && rb_is_red(tmp)) {
//-------------------------------------------------- 情况3
/*
* Case 1 - color flips
*
* G g
* / \ / \
* p u --> P U
* / /
* n n
*
* However, since g's parent might be red, and
* 4) does not allow this, we need to recurse
* at g.
*/
rb_set_parent_color(tmp, gparent, RB_BLACK);
rb_set_parent_color(parent, gparent, RB_BLACK);
node = gparent;
parent = rb_parent(node);
rb_set_parent_color(node, parent, RB_RED);
continue;
}
tmp = parent->rb_right;
if (node == tmp) {
//-------------------------------------------------- 情况5
/*
* Case 2 - left rotate at parent
*
* G G
* / \ / \
* p U --> n U
* \ /
* n p
*
* This still leaves us in violation of 4), the
* continuation into Case 3 will fix that.
*/
parent->rb_right = tmp = node->rb_left;
node->rb_left = parent;
if (tmp)
rb_set_parent_color(tmp, parent,
RB_BLACK);
rb_set_parent_color(parent, node, RB_RED);
augment_rotate(parent, node);
parent = node;
tmp = node->rb_right;
}
//------------------------------------------------------ 情况4
/*
* Case 3 - right rotate at gparent
*
* G P
* / \ / \
* p U --> n g
* / \
* n U
*/
gparent->rb_left = tmp; /* == parent->rb_right */
parent->rb_right = gparent;
if (tmp)
rb_set_parent_color(tmp, gparent, RB_BLACK);
__rb_rotate_set_parents(gparent, parent, root, RB_RED);
augment_rotate(gparent, parent);
break;
} else {
... /* parent == gparent->rb_right */
}
}
}
(二)删除操作的实现
与原理部分的讲解结构相同,Liunx内核源码的删除操作实现也是分为两步:(1)删除节点,判断是否需要rebalance。(2)重新调整树的结构,恢复平衡。
(1)删除节点,使用rebalance变量标记是否需要进行重新调整。代码在:include/linux/rbtree_augmented.h
中。
static __always_inline struct rb_node *
__rb_erase_augmented(struct rb_node *node, struct rb_root *root,
const struct rb_augment_callbacks *augment)
{
struct rb_node *child = node->rb_right;
struct rb_node *tmp = node->rb_left;
struct rb_node *parent, *rebalance;
unsigned long pc;
if (!tmp) { /* 待删除节点的左孩子为空 */
//----------------------------------------------------情况 1
/*
* Case 1: node to erase has no more than 1 child (easy!)
*
* Note that if there is one child it must be red due to 5)
* and node must be black due to 4). We adjust colors locally
* so as to bypass __rb_erase_color() later on.
*/
pc = node->__rb_parent_color;
parent = __rb_parent(pc);
__rb_change_child(node, child, parent, root);
if (child) {
/* 待删除仅有右孩子 */
child->__rb_parent_color = pc;
rebalance = NULL;
} else /* 待删除节点无孩子 */
rebalance = __rb_is_black(pc) ? parent : NULL;
tmp = parent;
} else if (!child) {
//----------------------------------------------------情况 1
/* Still case 1, but this time the child is node->rb_left */
/* 待删除仅有左孩子 */
tmp->__rb_parent_color = pc = node->__rb_parent_color;
parent = __rb_parent(pc);
__rb_change_child(node, tmp, parent, root);
rebalance = NULL;
tmp = parent;
} else {
//----------------------------------------------------情况 2
/* 待删除有两个孩子节点 child = node->rb_right */
struct rb_node *successor = child, *child2;
tmp = child->rb_left;
if (!tmp) {
/* 后继节点就是N的右孩子 */
/*
* Case 2: node's successor is its right child
*
* (n) (s)
* / \ / \
* (x) (s) -> (x) (c)
* \
* (c)
*/
parent = successor;
child2 = successor->rb_right;
augment->copy(node, successor);
} else {
/*
* Case 3: node's successor is leftmost under
* node's right child subtree
*
* (n) (s)
* / \ / \
* (x) (y) -> (x) (y)
* / /
* (p) (p)
* / /
* (s) (c)
* \
* (c)
*/
do {
parent = successor;
successor = tmp;
tmp = tmp->rb_left;
} while (tmp); /* 找后继 */
child2 = successor->rb_right;
WRITE_ONCE(parent->rb_left, child2);
WRITE_ONCE(successor->rb_right, child);
rb_set_parent(child, successor);
augment->copy(node, successor);
augment->propagate(parent, successor);
}
/* 将N的左子树移植到S节点 */
tmp = node->rb_left;
WRITE_ONCE(successor->rb_left, tmp);
rb_set_parent(tmp, successor);
/* N的父节点与S建立关系 */
pc = node->__rb_parent_color;
tmp = __rb_parent(pc);
__rb_change_child(node, successor, tmp, root);
/* child2 = successor->rb_right */
if (child2) { /* 节点S有右孩子 */
successor->__rb_parent_color = pc;
rb_set_parent_color(child2, parent, RB_BLACK);
rebalance = NULL;
} else {
unsigned long pc2 = successor->__rb_parent_color;
successor->__rb_parent_color = pc;
rebalance = __rb_is_black(pc2) ? parent : NULL;
}
tmp = successor;
}
augment->propagate(tmp, NULL);
return rebalance;
}
(2)恢复平衡。因为替换待删除结点N的可能是前驱也可能是后继。当然我们默认情况下都是选择后继,但是某些情况可能不存在后继,此时就必须选择前驱来替代。但限于篇幅,本文中我们不讨论前驱的情况,所以省去了相关代码。
/* parent 是 successor 的父节点, root 是红黑树的根节点 */
static __always_inline void
____rb_erase_color(struct rb_node *parent, struct rb_root *root,
void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
struct rb_node *node = NULL, *sibling, *tmp1, *tmp2;
while (true) {
/*
* Loop invariants:
* - node is black (or NULL on first iteration)
* - node is not the root (parent is not NULL)
* - All leaf paths going through parent and node have a
* black node count that is 1 lower than other leaf paths.
*/
sibling = parent->rb_right;
if (node != sibling) { /* node == parent->rb_left */ /* 替换节点是左孩子 */
/* 省略S是左孩子(前驱)的代码 */
} else { /* 替换节点是右孩子(后继) */
sibling = parent->rb_left; /* sibling = brother of S */
if (rb_is_red(sibling)) {
//----------------------------------------------- 情况1
/* Case 1 - right rotate at parent */
parent->rb_left = tmp1 = sibling->rb_right;
sibling->rb_right = parent;
rb_set_parent_color(tmp1, parent, RB_BLACK);
__rb_rotate_set_parents(parent, sibling, root,
RB_RED);
augment_rotate(parent, sibling);
sibling = tmp1;
}
tmp1 = sibling->rb_left; /* tmp1 = S的兄弟节点的左孩子 */
if (!tmp1 || rb_is_black(tmp1)) { /* 兄弟节点没有左孩子 || 左孩子是黑色 */
tmp2 = sibling->rb_right; /* 兄弟节点的右孩子 */
if (!tmp2 || rb_is_black(tmp2)) { /* 右孩子也不存在 || 右孩子是黑色 */
/* Case 2 - sibling color flip */
rb_set_parent_color(sibling, parent,
RB_RED); /* 设置兄弟节点为红 */
if (rb_is_red(parent))
rb_set_black(parent);
else {
//------------------------------------------ 情况5(递归)
node = parent;
parent = rb_parent(node);
if (parent)
continue;
}
break;
}
/* Case 3 - right rotate at sibling */
//-------------------------------------------------- 情况3
sibling->rb_right = tmp1 = tmp2->rb_left;
tmp2->rb_left = sibling;
parent->rb_left = tmp2;
if (tmp1)
rb_set_parent_color(tmp1, sibling,
RB_BLACK);
augment_rotate(sibling, tmp2);
tmp1 = sibling;
sibling = tmp2;
}
/* Case 4 - left rotate at parent + color flips */
//------------------------------------------------------ 情况4
parent->rb_left = tmp2 = sibling->rb_right;
sibling->rb_right = parent;
rb_set_parent_color(tmp1, sibling, RB_BLACK);
if (tmp2)
rb_set_parent(tmp2, parent);
__rb_rotate_set_parents(parent, sibling, root,
RB_BLACK);
augment_rotate(parent, sibling);
break;
}
}
}
# 总结
据我了解红黑树不仅是在Linux内核的内存管理中有体现,在Linux的进程调度、JAVA HashMap数据结构的实现中都用到了红黑树,感觉还是一个比较重要的知识点。之前一直觉得这个红黑树从名字上看是一个比较难的知识点,没有尝试去学习。今天正好借助完成图论大作业的机会,将红黑树及其在Linux内核虚拟内存管理的应用相关的知识进行整理记录下来,既能算是图论课程的一个拓展作业,也方便我以后用到时再进行查看。