contributed by < steven1lung >
在 Linux 核心中,有許多地方都有使用到紅黑樹。但是我們可能會想說為什麼要使用紅黑樹,而不是其他的二元樹像是:AVL Tree 之類。雖然二者平均時間複雜度都是 ,但因行為和特性的落差,有不同的應用場景。
以下簡述 AVL Tree 和紅黑樹的差異:
下方介紹 Linux 如何運用 alignment 將這個額外的 1 位元也省略
總而言之我們可以分兩種場景來選擇要使用的樹:
在 Linux Kernel 中有許多地方都有使用到紅黑樹,例如:hr_timer
使用紅黑樹來記錄 timer requests、ext3
檔案系統使用紅黑樹來追蹤目錄、VMA(Virtual Memory Area)也是用紅黑樹來紀錄。
而在 CFS 中,需要頻繁地插入跟刪除節點(任務),所以我覺得這是當初開發者會選擇使用紅黑樹的考量。
Linux 紅黑數跟一般紅黑樹不一樣得地方在有對速度進行優化,少了一層的 indirection 並且有更好的 cache locality。並且使用的方法是將 rb_node
嵌入要使用的結構中,透過 container_of
存取 data,而不是在 rb_node
裡宣告 data 指標。
Linux 紅黑樹需要使用者去定義 tree search 跟 insert 函式。
看了
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 核心開發者利用這特性,將其中一個沒用到的位元拿來標注紅和黑這二種顏色。
Linux Kernel 風格資料結構都會用一個大的 struct
包住我們使用的資料結構(list_head
、rb_node
),再透過巨集 container_of
從節點去存取外面的 container(也就是包住節點的結構體)。
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 我覺得對其他人閱讀你的程式碼時會更好被理解、或是自己在看時更容易懂當初的寫法,是一個小但是有幫助的技巧。
我們先看一個最常被使用到的函式:
這個函式就是把 node
的親代節點設為 parent
,並且將 rb_link
指向 node
。放入 parent
的左或是右子樹則是依照 rb_link
。
__rb_insert
可以到 lib/rbtree.c 看,程式碼太長就不放上來了。__rb_insert
做的事情就是插入節點並且進行平衡(rotate)。
當要存取親代節點時,可以透過巨集來存取:
這個巨集會捨棄最右邊的兩個位元,並且將結果轉成節點指標,就可以得到親代節點的位址了。
在 rbtree_augmented
中定義了紅色為 0,黑色為 1。由此可知,如果一個節點是紅色的,那就可以直接存取 __rb_parent_color
,但還是透過巨集方式統一比較好。
第七行的 rb_color(rb)
會存取這個節點的親代節點 __rb_parent_color
的顏色,因為知道黑色是 1,所以透過 (pc) & 1
就可以知道顏色了。
我們先看設定親代節點所使用的函式:
這個函式所做的事情就是將 p
設定為 rb
的親代節點,rb_color(rb)
是 p
的顏色跟 p
的位址去做 |
就一起設定好親代節點的位址跟顏色了。
也有可以針對親代節點去設定顏色的函式:
首先要定義好紅黑樹節點外的 container 結構:
我們就可以透過 container_of(ptr, type, member)
從 rb_node
存取 struct mytype
。
接著就需要定義 search 的函式,想法也很直覺:比對現在節點的 keystring
如果小於就往左子節點找,大於就往右子節點找。範例如下:
插入的做法就需要先使用 search
找到要插入的位置,新增節點,再進行平衡跟重新著色。
第 6 到 17 行是找出要插入的位置,作法是用一個指向節點位址的指標(指標的指標)來儲存新增的節點要在的位址。
第 20 行就是將找到的 *new
位址插入一個新的節點,將 *new
位址放入節點並連接 parent
。
從 11 或是 14 可以知道
new
是左、右子節點指標的指標,可以透過*new
存取左、右子節點
要迭代過每個節點的方式是透過先呼叫 rb_first
或是 rb_last
,就會回傳一個紅黑樹節點的指標。之後再用這個指標去呼叫 rb_next
或是 rb_prev
就可以存取下一個或是上一個節點了。
回傳 NULL 就代表沒有下一個了
將所有節點的 keystring
輸出的範例(由小到大):