---
tags: Linux Kernel
---
# Linux 紅黑樹探討
contributed by < [steven1lung](https://github.com/steven1lung) >
[Documentation](https://www.kernel.org/doc/Documentation/rbtree.txt)
在 Linux 核心中,有許多地方都有使用到紅黑樹。但是我們可能會想說為什麼要使用紅黑樹,而不是其他的二元樹像是:AVL Tree 之類。雖然二者平均時間複雜度都是 $O(\log n)$,但因行為和特性的落差,有不同的應用場景。
以下簡述 AVL Tree 和紅黑樹的差異:
* 平衡
- AVL Tree 比紅黑樹還要平衡(左右子樹的高度不能差超過 2),但是會在平衡自己的時候花費比紅黑樹多的時間。
- 如果考慮到要更快速地去 search 一個節點,那 AVL tree 會比較適合。
- 紅黑樹的優點在於雖然沒有到完全平衡(最長路徑 $\le$ 2 倍最短路徑),但是紅黑樹會在平衡自己時達到 $O(1)$ (最多 3 個 rotation)的複雜度。
* 空間
- AVL 的節點會需要宣告 factor (height) 的變數給每個節點來作為平衡的參考,而紅黑樹只需要 1 位元的變數來區分顏色(紅、黑)。
> 下方介紹 Linux 如何運用 alignment 將這個額外的 1 位元也省略
總而言之我們可以分兩種場景來選擇要使用的樹:
1. 插入跟刪除較多:紅黑樹
2. 查詢節點較多:AVL Tree
在 Linux Kernel 中有許多地方都有使用到紅黑樹,例如:`hr_timer` 使用紅黑樹來記錄 timer requests、`ext3` 檔案系統使用紅黑樹來追蹤目錄、VMA(Virtual Memory Area)也是用紅黑樹來紀錄。
而在 CFS 中,需要頻繁地插入跟刪除節點(任務),所以我覺得這是當初開發者會選擇使用紅黑樹的考量。
## Linux 紅黑樹介紹
Linux 紅黑數跟一般紅黑樹不一樣得地方在有對速度進行優化,少了一層的 indirection 並且有更好的 cache locality。並且使用的方法是將 `rb_node` 嵌入要使用的結構中,透過 `container_of` 存取 data,而不是在 `rb_node` 裡宣告 data 指標。
**Linux 紅黑樹需要使用者去定義 tree search 跟 insert 函式。**
### 紅黑樹節點
```c
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
```
> 看了 `rb_node` 跟 `list_head` 之後,發現 Linux 風格的資料結構都不會包含 data,而是會將 `rb_node` 或是 `list_head` 這些資料結構包在一個更大的結構體裡面。
一個紅黑樹節點包含:親代節點、左子節點、右子節點、顏色。而在 Linux 宣告的程式碼中,很巧妙地將親代節點跟顏色一起宣告,使用 `unsigned long __rb_parent_color` 將指向親代節點的指標跟自身的顏色合併。
透過 `__attribute__((aligned(sizeof(long))))` 讓編譯器知道 `struct rb_node` 會對齊 `sizeof(long)`,這樣就會讓指標最低 2 個位元是沒有使用到的,就可以把顏色的資料放進去其中一個位元中。
舉例來說,一個節點指標如果指向 `0xF724315C` ,這個位址轉成二進制會變成 `...1100`,最低 2 個位元會是 `0`,Linux 核心開發者利用這特性,將其中一個沒用到的位元拿來標注紅和黑這二種顏色。
### container_of
Linux Kernel 風格資料結構都會用一個大的 `struct` 包住我們使用的資料結構(`list_head`、`rb_node`),再透過巨集 `container_of` 從節點去存取外面的 container(也就是包住節點的結構體)。
```c
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
```
```c
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
```
`container_of` 的實作就是先將位址 `0` 轉成結構體指標,再取得 member(節點)的位址。因為是從 `0` 開始,所以就會得出這個 member 在結構體裡的偏移量。最後再將原本 member 記憶體位址扣掉這個偏移量,就可以得到 container struct 的位址了!
#### `container_of` or `rb_entry`
關於什麼時候要使用 `container_of` 還是 `rb_entry`(或是其他的 `XXXX_entry`,依照你正在使用的資料結構),作者的說法是當你是要存取節點的 container structure 時,會使用 `container_of`。如果要存取節點 container structure 裡的 member,就會使用 `XXXX_entry`。雖然 `XXXX_entry` 是一個巨集,且定義跟 `container_of` 一樣,透過這樣來區分是要存取大的 container 還是裡頭的 member 我覺得對其他人閱讀你的程式碼時會更好被理解、或是自己在看時更容易懂當初的寫法,是一個小但是有幫助的技巧。
### rb_link_node
我們先看一個最常被使用到的函式:
```c
static inline void rb_link_node(struct rb_node *node, struct rb_node *parent,
struct rb_node **rb_link)
{
node->__rb_parent_color = (unsigned long)parent;
node->rb_left = node->rb_right = NULL;
*rb_link = node;
}
```
這個函式就是把 `node` 的親代節點設為 `parent`,並且將 `rb_link` 指向 `node`。放入 `parent` 的左或是右子樹則是依照 `rb_link`。
### rb_insert_color
```c
void rb_insert_color(struct rb_node *node, struct rb_root *root)
{
__rb_insert(node, root, dummy_rotate);
}
```
`__rb_insert` 可以到 [lib/rbtree.c](https://elixir.bootlin.com/linux/v5.10/source/lib/rbtree.c#L85) 看,程式碼太長就不放上來了。`__rb_insert` 做的事情就是插入節點並且進行平衡(rotate)。
### 親代節點
當要存取親代節點時,可以透過巨集來存取:
```c
#define rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & ~3))
```
這個巨集會捨棄最右邊的兩個位元,並且將結果轉成節點指標,就可以得到親代節點的位址了。
### 顏色
```c=
#define RB_RED 0
#define RB_BLACK 1
#define __rb_color(pc) ((pc) & 1)
#define __rb_is_black(pc) __rb_color(pc)
#define __rb_is_red(pc) (!__rb_color(pc))
#define rb_color(rb) __rb_color((rb)->__rb_parent_color)
#define rb_is_red(rb) __rb_is_red((rb)->__rb_parent_color)
#define rb_is_black(rb) __rb_is_black((rb)->__rb_parent_color)
```
在 `rbtree_augmented` 中定義了紅色為 0,黑色為 1。由此可知,如果一個節點是紅色的,那就可以直接存取 `__rb_parent_color`,但還是透過巨集方式統一比較好。
第七行的 `rb_color(rb)` 會存取這個節點的親代節點 `__rb_parent_color` 的顏色,因為知道黑色是 1,所以透過 `(pc) & 1` 就可以知道顏色了。
我們先看設定親代節點所使用的函式:
```c
static inline void rb_set_parent(struct rb_node *rb, struct rb_node *p)
{
rb->__rb_parent_color = rb_color(rb) | (unsigned long)p;
}
```
這個函式所做的事情就是將 `p` 設定為 `rb` 的親代節點,`rb_color(rb)` 是 `p` 的顏色跟 `p` 的位址去做 `|` 就一起設定好親代節點的位址跟顏色了。
也有可以針對親代節點去設定顏色的函式:
```c
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;
}
```
## 簡單的紅黑樹例子
### 決定架構
首先要定義好紅黑樹節點外的 container 結構:
```c
struct mytype {
struct rb_node node;
char *keystring;
};
```
我們就可以透過 `container_of(ptr, type, member)` 從 `rb_node` 存取 `struct mytype`。
### Search 實作
接著就需要定義 search 的函式,想法也很直覺:比對現在節點的 `keystring` 如果小於就往左子節點找,大於就往右子節點找。範例如下:
```c
struct mytype *my_search(struct rb_root *root, char *string)
{
struct rb_node *node = root->rb_node;
while (node) {
struct mytype *data = container_of(node, struct mytype, node);
int result;
result = strcmp(string, data->keystring);
if (result < 0)
node = node->rb_left;
else if (result > 0)
node = node->rb_right;
else
return data;
}
return NULL;
}
```
### Insert 實作
插入的做法就需要先使用 `search` 找到要插入的位置,新增節點,再進行平衡跟重新著色。
```c=
int my_insert(struct rb_root *root, struct mytype *data)
{
struct rb_node **new = &(root->rb_node), *parent = NULL;
/* Figure out where to put new node */
while (*new) {
struct mytype *this = container_of(*new, struct mytype, node);
int result = strcmp(data->keystring, this->keystring);
parent = *new;
if (result < 0)
new = &((*new)->rb_left);
else if (result > 0)
new = &((*new)->rb_right);
else
return FALSE;
}
/* Add new node and rebalance tree. */
rb_link_node(&data->node, parent, new);
rb_insert_color(&data->node, root);
return TRUE;
}
```
第 6 到 17 行是找出要插入的位置,作法是用一個指向**節點位址**的指標(指標的指標)來儲存新增的節點要在的位址。
第 20 行就是將找到的 `*new` 位址插入一個新的節點,將 `*new` 位址放入節點並連接 `parent`。
> 從 11 或是 14 可以知道 `new` 是左、右子節點指標的指標,可以透過 `*new` 存取左、右子節點
### 迭代紅黑樹里的節點
```c
struct rb_node *rb_first(struct rb_root *tree);
struct rb_node *rb_last(struct rb_root *tree);
struct rb_node *rb_next(struct rb_node *node);
struct rb_node *rb_prev(struct rb_node *node);
```
要迭代過每個節點的方式是透過先呼叫 `rb_first` 或是 `rb_last`,就會回傳一個紅黑樹節點的指標。之後再用這個指標去呼叫 `rb_next` 或是 `rb_prev` 就可以存取下一個或是上一個節點了。
> 回傳 NULL 就代表沒有下一個了
將所有節點的 `keystring` 輸出的範例(由小到大):
```c
struct rb_node *node;
for (node = rb_first(&mytree); node; node = rb_next(node))
printk("key=%s\n", rb_entry(node, struct mytype, node)->keystring);
```
## Source code
[linux/rbtree.h](https://elixir.bootlin.com/linux/v5.10/source/include/linux/rbtree.h)
[linux/rbtree_augmented.h](https://elixir.bootlin.com/linux/v5.10/source/include/linux/rbtree_augmented.h#L146)
[lib/rbtree.c](https://elixir.bootlin.com/linux/v5.10/source/lib/rbtree.c)