面對./src/core子目錄中71個源檔案,有點無從下手。瀏覽包含主函數的nginx.c檔案,發現nginx使用了很多自行封裝的資料結構,不弄清楚這是些什麼樣的資料結構就很難理解主函數中操作的意義。於是我們挑看起來基礎的資料結構開始研究。組織nginx所有資料結構的是ngx_core.h檔案。它首先包含了ngx_config.h,我們在ngx_config.h中發現了三個類型定義。
1、ngx_int_t、ngx_uint_t、ngx_flag_t
nginx.c中看到的第一個陌生資料類型是ngx_int_t,在nginx_config.h中找到了它的定義。
typedef intptr_t ngx_int_t;typedef uintptr_t ngx_uint_t;typedef intptr_t ngx_flag_t;
順藤摸瓜找到了三個資料類型的定義。本科c入門教學中並沒有對intptr_t/uintptr_t的介紹,我在c的stdint.h標頭檔中發現了它們的定義。
/* Types for `void *' pointers. */#if __WORDSIZE == 64# ifndef __intptr_t_definedtypedef long int intptr_t;# define __intptr_t_defined# endiftypedef unsigned long int uintptr_t;#else# ifndef __intptr_t_definedtypedef int intptr_t;# define __intptr_t_defined# endiftypedef unsigned int uintptr_t;#endif
首先注釋說這兩種類型是“void *”的指標類型,儘管字面上看,intptr_t和uintptr_t確實是整型指標類型和無符號整型指標類型,但是讓人摸不著頭腦,為什麼要使用整型作為整型的指標類型呢?先放一放,看後面的宏,機器是64位字長則intptr_t為long int,uintptr_t為unsigned long int,正好我機器上是64位編譯器,sizeof()了一下,是8個位元組64位,小於64位字長的intptr_t為int,uintptr_t為unsigned int,查表得知32位編譯器下int和unsigned為4個位元組,16位編譯器下為2個位元組。那麼intptr_t/uintptr_t應該是會隨著平台字長變化而發生對應變化的整數型別。經過瞭解,發現《深入分析Linux核心源碼》中對此的解釋是,系統核心在操作記憶體時,將記憶體當做一個大數組,而指標就是數組索引/下標,核心程式員使用這種特殊的整型來接受記憶體位址值、操作記憶體相比使用指標更加直觀,不容易犯錯。看起來,nginx中,只是單純的想要使用一些平台相關的int、unsigned int類型變數而已。
2、ngx_rbtree_t
2.1、什麼是紅/黑樹狀結構
作為一個曾經常年在ACM比賽裡划水的退役隊員,對紅/黑樹狀結構這樣的有名資料結構還是比較敏感的。紅/黑樹狀結構是一種特殊約束形式下的平衡二叉尋找樹實現。學過資料結構課的同學應該知道,課本上的最早的自平衡二叉樹AVL樹嚴格的要求子樹的高度差不超過2,以獲得根結點到所有葉結點距離基本相同(平衡)的特性。
紅/黑樹狀結構不追求嚴格的平衡,而是通過5個約束實現基本平衡:
①結點是紅色或黑色;
②根是黑色;
③葉結點是黑色;
④紅色結點的子結點都是黑色;
⑤任一結點到其葉結點的簡單路徑中黑色結點數相同。
AVL樹根到葉結點最長距離與最短距離的比不超過2。紅/黑樹狀結構的約束也保證了這一特性(最長路徑是紅黑相間,最短路徑是全黑,這種情況下最長路徑剛好是最短路徑的2倍長)。
既然是平衡二叉尋找樹的一種實現,那麼紅/黑樹狀結構自然是內部有序的,同時跟AVL樹一樣支援O(log2n)時間複雜度的尋找、插入和刪除。
相比AVL樹,紅黑可以保證在每次插入或刪除操作之後的重平衡過程中,全樹拓撲結構的更新僅涉及常數個結點。儘管最壞情況下需對O(log2n)個結點重染色,但就分攤意義(平均效率)而言,僅為O(1)個。但是因為沒有嚴格約束樹的平衡特性,紅/黑樹狀結構的左右子樹高度差比AVL樹要大。
2.2、ngx_rbtree.h
機會難得,我們就把nginx的源碼作為素材來深入瞭解一下紅/黑樹狀結構的實現。首先是結點的結構:
typedef ngx_uint_t ngx_rbtree_key_t;typedef ngx_int_t ngx_rbtree_key_int_t;typedef struct ngx_rbtree_node_s ngx_rbtree_node_t;struct ngx_rbtree_node_s { ngx_rbtree_key_t key;//平台相關的無符號整型關鍵字 ngx_rbtree_node_t *left;//左子結點指標 ngx_rbtree_node_t *right;//右子結點指標 ngx_rbtree_node_t *parent;//父結點指標 u_char color;//結點顏色 u_char data;//結點資料};
然後是紅/黑樹狀結構的結構定義:
typedef struct ngx_rbtree_s ngx_rbtree_t;//“_s”是結構體“_t”是類型//下面是一個函數指標變數類型的定義,是紅/黑樹狀結構插入函數的指標//參數有樹根結點、插入結點和哨兵結點的指標typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t *root, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);struct ngx_rbtree_s { ngx_rbtree_node_t *root;//根節點指標 ngx_rbtree_node_t *sentinel;//哨兵結點指標 ngx_rbtree_insert_pt insert;//插入函數指標}; 將函數指標變數作為結構體成員變數以達成可以把結構體當做類來使用(既有成員變數又有成員方法)的效果,這種手法在nginx的源碼中相當普遍。關於函數,nginx還有一種更神奇的手段——宏:
#define ngx_rbtree_init(tree, s, i) \ ngx_rbtree_sentinel_init(s); \ (tree)->root = s; \ (tree)->sentinel = s; \ (tree)->insert = i//這裡insert函數指標的賦值實現了多態
藉助宏來達成內嵌函式的效果(函數實現如果比較簡單,就乾脆把實現過程整個搬到類中),令人費解的是,C不是沒有內聯關鍵字,甚至同一個標頭檔中就有一個內嵌函式的定義。研究內嵌函式之前,下面還有幾個宏要看一看:
#define ngx_rbt_red(node) ((node)->color = 1)#define ngx_rbt_black(node) ((node)->color = 0)#define ngx_rbt_is_red(node) ((node)->color)#define ngx_rbt_is_black(node) (!ngx_rbt_is_red(node))#define ngx_rbt_copy_color(n1, n2) (n1->color = n2->color) /* a sentinel must be black */#define ngx_rbtree_sentinel_init(node) ngx_rbt_black(node)
nginx源碼中的變數都很容易看懂以至於我們不怎麼需要查資料或找注釋。color置1染紅置0染黑,color為1則結點為紅色,不為紅色的則為黑色,複製結點顏色即複製color值,哨兵結點一定要染成黑色。
static ngx_inline ngx_rbtree_node_t *ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel){ while (node->left != sentinel) { node = node->left; } return node;}
ngx_inline是一個宏,實際值就是關鍵字inline。這個內嵌函式非常好懂,目的看起來是尋找以任意結點為根結點的子樹中結點值最小的結點。實現方法是找到紅/黑樹狀結構子樹最邊緣的左子結點。那麼我們有理由猜測,哨兵結點是空結點或邊緣標識。
2.3、紅/黑樹狀結構的結點插入
接下來我們來深入ngx_rbtree.c看看nginx如何?幾個關鍵的紅/黑樹狀結構方法。
voidngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node){ //根結點指標的指標,或者根結點指標數組,會有多個根結點嗎,令人費解 //臨時結點指標 //哨兵結點指標,推測哨兵在每次查詢時可能都不一樣,也許指待插位置 //變數不分行,我寫注釋都很不方便 ngx_rbtree_node_t **root, *temp, *sentinel; /* a binary tree insert */ root = (ngx_rbtree_node_t **) &tree->root;//樹根指標的指標賦給了root sentinel = tree->sentinel;//哨兵指標賦給了哨兵指標 if (*root == sentinel) {//特判,如果根是哨兵,即樹是空的 node->parent = NULL;//新插入的結點變成了根 node->left = sentinel;//新結點的左子結點是哨兵 node->right = sentinel;//新結點的右子結點也是哨兵 ngx_rbt_black(node);//新根染黑 *root = node;//確認新結點為新根 return;//插入結束 } //樹初始化時給了insert指標一個函數地址 //查看前面的宏ngx_rbtree_init(tree, s, i) //發現只是把指定結點染黑,同時賦為根和哨兵,給insert指標指定一個函數 //ngx_rbtree.c中有兩個參數表符合的可選函數:插入值、插入計時器值 //稍後來看兩種插入分別如何?又有什麼區別 tree->insert(*root, node, sentinel); /* re-balance tree */ //如果新結點不是根且其父結點是紅的,迴圈 while (node != *root && ngx_rbt_is_red(node->parent)) { //如果父結點是左子結點,獲得父結點的右兄弟 if (node->parent == node->parent->parent->left) { temp = node->parent->parent->right; //如果父結點的右兄弟是紅的 if (ngx_rbt_is_red(temp)) { ngx_rbt_black(node->parent);//父結點染黑 ngx_rbt_black(temp);//父結點的右兄弟染黑 ngx_rbt_red(node->parent->parent);//父結點的父結點染紅 node = node->parent->parent;//父結點的父結點成為當前結點 } else {//如果父結點的右兄弟是黑的 if (node == node->parent->right) {//如果新結點是右子結點 node = node->parent;//父結點成為新node ngx_rbtree_left_rotate(root, sentinel, node);//node左旋 } ngx_rbt_black(node->parent);//node的父結點染黑 //node的父結點的父結點染紅 ngx_rbt_red(node->parent->parent); ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//node的父結點的父結點右旋 } } else {//如果父結點是右子結點,獲得父結點的左兄弟 temp = node->parent->parent->left; //如果父結點的左兄弟是紅的 if (ngx_rbt_is_red(temp)) { ngx_rbt_black(node->parent);//父結點染黑 ngx_rbt_black(temp);//父結點的左兄弟染黑 ngx_rbt_red(node->parent->parent);//父結點的父結點染紅 node = node->parent->parent; } else {//如果父結點的左兄弟是黑的 if (node == node->parent->left) {//如果新結點是左子結點 node = node->parent;//父結點成為當前結點 ngx_rbtree_right_rotate(root, sentinel, node); //當前結點右旋 } ngx_rbt_black(node->parent);//當前結點染黑 //當前結點父結點的父結點染紅 ngx_rbt_red(node->parent->parent); ngx_rbtree_left_rotate(root, sentinel, node->parent->parent);//當前結點的父結點的父結點左旋 } } } ngx_rbt_black(*root);//根結點染黑}
然後是對應ngx_rbtree_insert_pt指標的基礎的結點插入函數:
voidngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel){ ngx_rbtree_node_t **p;//雖然無關緊要,但兩層指標令人費解 for ( ;; ) {//無條件迴圈或者說死迴圈,等同於while(1)但節省了一個字元 p = (node->key < temp->key) ? &temp->left : &temp->right; if (*p == sentinel) {//在二叉樹中尋找新結點合適的葉結點位置 break; } temp = *p; } //令新結點佔據合適的哨兵位置成為新的葉結點,染紅,產生新哨兵 *p = node; node->parent = temp; node->left = sentinel; node->right = sentinel; ngx_rbt_red(node);}
ngx_rbtree_insert_timer_value函數跟ngx_rbtree_insert_value函數唯一區別就是判斷大小時,採用了兩個值相減,避免溢出。
以上是插入結點涉及的函數,老實說我不太喜歡這麼長的函數實現,換我自己寫肯定分塊了。分支操作太多,看代碼邏輯已經亂了,我們需要畫幾個圖。首先,如果樹為空白:
如果樹中只有一個根結點:
如果C>A:
如果C,C染紅,B染黑A染紅,A右旋。右旋函數如下:
static ngx_inline voidngx_rbtree_right_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node){ ngx_rbtree_node_t *temp; temp = node->left; node->left = temp->right;//左子結點指向原左子結點的右結點 if (temp->right != sentinel) {//如果左子結點的右結點不為哨兵 temp->right->parent = node;//左子結點的右子結點掛在右旋結點上 } temp->parent = node->parent;//左子結點掛在右旋結點的父結點上 if (node == *root) {//如果右旋結點為根節點 *root = temp;//根節點賦為左子結點 } else if (node == node->parent->right) {//如果右旋結點為右子結點 node->parent->right = temp;//左子結點掛父結點右邊 } else {//否則左子結點掛父結點左邊 node->parent->left = temp; } temp->right = node;//右旋結點掛左子結點右邊 node->parent = temp;}
顯然B將成為新的根,左C右A:
如果B,會先做一次左旋再做一次右旋,其實除開染色過程,我覺得這跟AVL樹的插入過程沒有什麼區別:
其他的插入情景要麼與以上幾個對稱,要麼發生在樹的其他子樹中,實際過程完全一樣。LL型右旋,RR型左旋,LR型先右旋後左旋,RL型先左旋後右旋。與AVL樹不同的是,插入結點時紅/黑樹狀結構左旋或右旋的判定條件明確為附近一兩個結點的顏色,其他過程沒有任何區別。
2.4、紅/黑樹狀結構的結點刪除
據說紅/黑樹狀結構和AVL樹的區別主要體現在刪除節點時,我們就來看一看。我剛說什麼來著,刪除結點的函數體更長了,足足165行,我決定分段研究,先看第一部分:
if (node->left == sentinel) {//如果左子結點是哨兵或左右子結點都是哨兵 temp = node->right;//獲得右子結點,後面讓它接替node位置 subst = node;//node賦給subst } else if (node->right == sentinel) {//如果右子結點是哨兵 temp = node->left;//獲得左子結點,後面讓它接替node位置 subst = node;//node賦給subst } else {//如果左右子結點都不是哨兵 subst = ngx_rbtree_min(node->right, sentinel);//獲得右子樹中最小的結點 if (subst->left != sentinel) {//如果右子樹的最小結點的左子結點不是哨兵 temp = subst->left;//獲得右子樹的最小結點的左子結點 } else {//否則獲得右子樹最小結點的右子結點 temp = subst->right; }//看起來subst將被從原位置刪掉然後接替node的位置}
下面我們來看看temp和subst要幹什麼用:
if (subst == *root) {//如果subst是根 *root = temp;//temp接替根 ngx_rbt_black(temp);//染黑temp /* DEBUG stuff */ node->left = NULL;//清空了待刪結點 node->right = NULL; node->parent = NULL; node->key = 0; return;} red = ngx_rbt_is_red(subst);//獲得subst是否是紅色 if (subst == subst->parent->left) {//如果subst是左子結點 subst->parent->left = temp;//把接替結點掛到subst位置 } else {//如果subst是右子結點 subst->parent->right = temp;//把接替結點掛到subst位置} 下一段:
if (subst == node) {//如果subst是待刪結點 temp->parent = subst->parent;//接替結點直接接替,刪除完成 } else {//如果subst不是待刪結點 if (subst->parent == node) {//如果subst的父結點就是待刪結點 temp->parent = subst;//接替結點掛在subst上 } else {//如果待刪結點比subst的父結點更高 temp->parent = subst->parent;//把接替結點掛在subst的父結點上 } //subst接替待刪結點node的位置,複製待刪結點跟周圍結點的關係 subst->left = node->left; subst->right = node->right; subst->parent = node->parent; ngx_rbt_copy_color(subst, node);//複製顏色 if (node == *root) {//如果待刪結點是根 *root = subst;//subst接替根 } else {//如果待刪結點不是根,subst接替它 if (node == node->parent->left) { node->parent->left = subst; } else { node->parent->right = subst; } } if (subst->left != sentinel) {//如果subst左子結點不是哨兵 subst->left->parent = subst;//subst的左子結點放棄node,掛上來 } if (subst->right != sentinel) {//如果subst右子結點不是哨兵 subst->right->parent = subst;//subst右子結點放棄node,掛上來 }}//清空待刪結點node/* DEBUG stuff */node->left = NULL;node->right = NULL;node->parent = NULL;node->key = 0;//如果subst是紅色,紅/黑樹狀結構約束依然被遵守,刪除工作就可以結束了if (red) { return;}
看起來結點的刪除過程已經順利完成了,但是如果subst是黑色,我們需要修複紅/黑樹狀結構的約束。下面這一段代碼的主角是接替subst位置的temp結點:
//當subst的接替結點不是根且為黑色,迴圈while (temp != *root && ngx_rbt_is_black(temp)) { if (temp == temp->parent->left) {//如果temp是左子結點 w = temp->parent->right;//獲得其右兄弟 if (ngx_rbt_is_red(w)) {//如果temp的右兄弟是紅色 ngx_rbt_black(w);//染黑temp的右兄弟 ngx_rbt_red(temp->parent);//染紅temp的父結點 //temp的父結點左旋 ngx_rbtree_left_rotate(root, sentinel, temp->parent); w = temp->parent->right;//獲得temp的新右兄弟 } //如果temp右兄弟的左右子結點都是黑的 if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) { ngx_rbt_red(w);//染紅temp的右兄弟 temp = temp->parent;//獲得temp的父結點為新temp } else {//如果temp右兄弟的子結點不全為黑 if (ngx_rbt_is_black(w->right)) {//如果其右子結點是黑色 ngx_rbt_black(w->left);//染黑左子結點 ngx_rbt_red(w);//染紅temp的右兄弟 ngx_rbtree_right_rotate(root, sentinel, w);//右兄弟右旋 w = temp->parent->right;//獲得temp的新右兄弟 } //temp右兄弟複製temp父結點顏色 ngx_rbt_copy_color(w, temp->parent); ngx_rbt_black(temp->parent);//染黑temp父結點 ngx_rbt_black(w->right);//染黑temp右兄弟的右子結點 //temp父結點左旋 ngx_rbtree_left_rotate(root, sentinel, temp->parent); temp = *root;//獲得根 } } else {//如果temp是右子結點,做對稱的事 w = temp->parent->left; if (ngx_rbt_is_red(w)) { ngx_rbt_black(w); ngx_rbt_red(temp->parent); ngx_rbtree_right_rotate(root, sentinel, temp->parent); w = temp->parent->left; } if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) { ngx_rbt_red(w); temp = temp->parent; } else { if (ngx_rbt_is_black(w->left)) { ngx_rbt_black(w->right); ngx_rbt_red(w); ngx_rbtree_left_rotate(root, sentinel, w); w = temp->parent->left; } ngx_rbt_copy_color(w, temp->parent); ngx_rbt_black(temp->parent); ngx_rbt_black(w->left); ngx_rbtree_right_rotate(root, sentinel, temp->parent); temp = *root; } } }ngx_rbt_black(temp);//染黑當前temp
跟插入結點時一樣亂,我們梳理一下。
首先忽略紅/黑樹狀結構的約束進行刪除:
①如果刪除的是一個葉結點,即沒有後繼或後繼全為哨兵的結點,直接刪除即可;
②如果只有一個後繼,讓其替換待刪除結點即可;
③如果有兩個後繼,需要從樹的邊緣選擇一個結點,有兩種等價的選擇,待刪結點左子樹的最大結點和右子樹的最小結點,nginx選擇的是後者,以這個結點的鍵與值(key與value/data)替換待刪結點的鍵與值,然後刪除這個替身。
不論是①、②情景中的待刪結點還是③情景中替身,在源碼中都是subst。下面要圍繞著它來進行討論。
以上是不考慮紅/黑樹狀結構平衡性的純拓撲結構變動。下面要考慮是否調整樹的拓撲結構使樹重新平衡,是否調整結點的顏色使樹重新符合紅/黑樹狀結構的約束條件。我們知道紅/黑樹狀結構有一條關鍵約束是任意結點到其子樹中葉結點的簡單路徑中黑色結點數相同。那麼如果subst是一個紅色結點,我們不需要對紅/黑樹狀結構做任何調整,它仍是一棵紅/黑樹狀結構;如果subst是黑色的,所有經過subst的簡單路徑上都會少一個黑色結點數,所以需要進行調整。
下面來根據不同情景分情況討論,因為二叉樹的情景左右顛倒時調整方式也可以左右顛倒,我們只討論subst是左子結點的情況。設剛接替subst的temp為X,X的新右兄弟為W。從經過簡化的源碼來看,關於結點顏色的變化很令人費解,我們不妨先來看一看:
①W為紅色:將W染黑,將X與W的父結點X->parent染紅,X->parent左旋,W重設為X的新右兄弟,然後轉入情景①、②或③;
②W為黑色,W兩個後繼都是黑色:將W染紅,X重設為X->parent;
③W為黑色,W右子結點為黑色:將W左子結點染黑,將W染紅,W右旋,W重設為X的新右兄弟,然後將X->parent的顏色賦給W,將X->parent染黑,X->parent左旋,根賦給temp;
④W為黑色,W右子結點為紅色:將W左子結點染黑,將W染紅,W右旋,W重設為X的新右兄弟,然後將X->parent的顏色賦給W,將X->parent染黑,將W右子結點染黑,X->parent左旋,根賦給temp。
最後還要把temp染黑。我們可以看到情景①中進行了一次左旋,情景②只進行了染色,情景③、④都進行了一次右旋和一次左旋。情景①處理結束時一定還要轉入別的情景,情景②、③、④的出現則標誌著本次調整的結束。那麼,紅/黑樹狀結構刪除結點後的調整過程中,依情景①迴圈出現的次數,調整過程中旋轉的最多見的次數將是1次、2次、3次,再往上次數越多越罕見(依情景①迴圈出現的次數),最多旋轉次數將可能到達樹高即log2n次。生產環境中,刪除結點後平均每次調整中旋轉的次數就像分析源碼之前提到的,將是常數規模的。
接下來我打算以逐步翻新版本的方式重寫紅/黑樹狀結構,更精細、直觀地瞭解紅/黑樹狀結構這一資料結構。而在重寫之前,我們需要瞭解,nginx的紅黑中所有的葉結點,都是哨兵(sentinel),這在調整紅/黑樹狀結構時達成了對紅/黑樹狀結構的一種最佳化。通過增加一層全黑的子結點,紅/黑樹狀結構中實際有值的子樹裡,就允許在子結點出現紅色結點了。雖然我沒有證明,但這常數規模地增加了刪除結點時的旋轉次數,也促進了插入新結點時進行調整的機率(增加了在紅色結點下插入新結點的機率),同樣增加了旋轉的次數。而旋轉將壓縮紅/黑樹狀結構子樹的高度,提高查詢效率。
在由樸素到精緻地重寫紅/黑樹狀結構的過程中,我將由少到多地考慮使用nginx對紅/黑樹狀結構的最佳化,或者加入我自己的最佳化。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
以上就介紹了nginx的資料結構1——ngx_int_t與ngx_rbtree_t,包括了方面的內容,希望對PHP教程有興趣的朋友有所協助。