1. 程式人生 > >路由表的結構與演算法分析--trie查詢

路由表的結構與演算法分析--trie查詢

  linux 中的路由查詢演算法一點也不比那些大型的專業路由器的查詢演算法差,所謂的專業路由器就是在很大程度上用硬體實現了一些常用的軟體功能,比如思科的 路由器竟然採用過什麼256 叉樹,這種瘋狂的以空間換時間的做法在通用的計算機作業系統---linux 上實現是不現實的,但是確實是可能的。linux 的路由表具有高度的可擴充套件性,內建了256 張路由表,對於策略路有的實現相當方便,預設使用雜湊表查詢演算法,那種方法在我提到的另一篇名為《路由表的結構 與演算法分析》裡面已經解釋得很詳細了,因此我這裡主要說說trie 查詢演算法。這個查詢演算法是基於樹的,首先熟悉一下資料結構。
  trie

演算法中將路由表抽象成一個trie 結構:

struct trie {

         struct node *trie;// 一切的查詢從這裡開始。

 #ifdef CONFIG_IP_FIB_TRIE_STATS

         struct trie_use_stats stats;

 #endif

 };

結構很緊湊,幾乎沒有什麼沒有用的東西,下面看一下node

 struct node {

         unsigned long parent;

         t_key key;

 };

有人就要問了,這個東西能做什麼呢?這不一切斷了嗎?其實,linux

在核心中大量使用了面向物件的特性,這裡給出的node 結構作為 基類 ,真正管事情的是tnode (後面說插入的時候我會給出它們是怎麼聯絡起來的):

 struct tnode {

         unsigned long parent;

         t_key key;

         unsigned char pos;              // 這個欄位指出本node 要比較的位在32ip 中的偏移

         unsigned char bits;             // 這個欄位指出表示這個節點的孩子節點的位數,比如如果有2 個孩子,那麼需要1

位,0 表示第一個孩子,1 表示第二個孩子;如果有4 個孩子就需要2 位,以此類推。

         unsigned int full_children;     // 這個欄位表示孩子中有幾個是full 的,所謂的full 就是在插入操作的時候不能把這個孩子作為新插入的孩子的孩子從而擴充套件了。

         unsigned int empty_children;    // 這個表示空孩子的個數

         union {

                 struct rcu_head rcu;

                 struct work_struct work;

         };

         struct node *child[0];// 為了一個定長的結構

 };

上面的註釋都很拗口,我開始讀程式碼的時候也是費盡周折才搞明白的,還作了n 多個實現,寫了n 多個測試程式碼,我會盡量在下面的敘述中把問題理清,但是真正的理解還是得做實驗。

 struct leaf {     // 此為一個葉子節點,表示一條路由

         t_key key;// 節點健值

         unsigned long parent;

         struct hlist_head list;

         struct rcu_head rcu;

 }; 

 struct leaf_info {   // 此處存放具體的路由

         struct hlist_node hlist;

         struct rcu_head rcu;

         int plen;

         struct list_head falh;

 };

從這些資料結構可以看出,linux 的資料結構相當精巧,善於組合小物件來形成一個可管理的大系統,這就是核心裡面的面向物件的思想,這些小小的資料結構 起到的作用就是管理資料,讓資料具有聯絡,具有層次感,從而組成的大系統也就有了可管理的結構,如果不說這些結構就是路由,那麼我完全可以將它們用於檔案 系統,另一個例子可參見linux2.6 核心裡的kobject 。當然最普遍的例子就是著名的list_head 了。 好了,基礎設施就是這麼多,下面就開始利用這些資料結構來進行優美的查找了。
  linux
trie 樹是動態調整的,它的插入演算法解釋動態調整的動作,對比一下傳統bsd 核心的radix 樹查詢,也是動態調整的,但是bsd radix 查詢演算法中的樹節點只有最多2 個,是一顆二叉樹,最新的bsd 核心實現了trie 查詢,正如我前面文章說的,但是它將ip 地址分為等長的四個部 分,然後每個部分分別進行匹配,很明確,就是4 個部分,樹的叉數也就確定了,就是28 次方叉樹,比較適合大型機器上的並行流水計算,事實證明,很多硬體 路有器的硬體設計所用的演算法正是和新版bsd 一樣的演算法。而linuxtrie 演算法完全是動態的,可能在有的時候是二叉樹,有的時候是232 次方叉 數,視當時情況而定。下面先列出一個從核心原始碼中弄出來的圖:

   Example:   n 是一個內部節點而tp 是它的父親。

   _________________________________________________________________

   | i | i | i | i | i | i | i | N | N | N | S | S | S | S | S | C |

-----------------------------------------------------------------

     0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15

   _________________________________________________________________

   | C | C | C | u | u | u | u | u | u | u | u | u | u | u | u | u |

-----------------------------------------------------------------

    16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31

   tp->pos = 7

   tp->bits = 3

   n->pos = 15

   n->bits = 4

這 個圖非常直觀,就是說對於tp ,它的第7 位(從0 開始)往後開始不同,就是說它的孩子們的前7 位都是一樣的,查詢的時候如果一個鍵值的前7 位和tp 一樣, 那麼下一步應該取tp 的哪個孩子完全由該鍵值的7893 位決定,為什麼,因為tp 的孩子節點就3 位,為789 ;到了該孩子節點後就該重複前面的 過程了,該孩子節點從15 位往後不同,具有24 次方個孩子,下一步該走向何方由查詢鍵值的15161718 決定,決定該向哪個tp 的孫子前進,但 是如果查詢鍵的15 位之前和n 的鍵不同怎麼辦,還要繼續往下去嗎?當然不必了,往下去是沒有意義的,但是如果n 的鍵值在和查詢鍵值不同的那一位以及那一位 往後都是0 的話,那麼一直可以到一個葉子節點,它就是該次查詢的結果,可能是一條普適路由,於是在這種情況下還是要向下的,直到不符合全為0 的條件後開始 回溯,回溯就有可能找到嗎?實際上回溯根本不可能找到一條精確路由,這個結果在你找到一個和nn->pos 前不同位的那一刻就決定了,那回溯幹什 麼,回溯是為了找到一條普適路由,僅此而已,下面用程式碼說明上述過程:

static int fn_trie_lookup(struct fib_table *tb, const struct flowi *flp, struct fib_result *res)

 {

         struct trie *t = (struct trie *) tb->tb_data;

         int plen, ret = 0;

         struct node *n;

         struct tnode *pn;

         int pos, bits;

         t_key key = ntohl(flp->fl4_dst);

         int chopped_off;

         t_key cindex = 0;

         int current_prefix_length = KEYLENGTH;

         struct tnode *cn;

         t_key node_prefix, key_prefix, pref_mismatch;

         int mp;

         rcu_read_lock(); 

         n = rcu_dereference(t->trie);     // 從根開始

         if (!n)// 還沒有路由資訊

                 goto failed;

         if (IS_LEAF(n)) { // 如果是葉子節點就檢檢視是不是咱要的,僅此一步定乾坤。

                 if ((ret = check_leaf(t, (struct leaf *)n, key, &plen, flp, res)) <= 0)

                         goto found;

                 goto failed;

         }

         pn = (struct tnode *) n;// 開始正規的trie 樹的遍歷查詢,第一次查詢的實際上是根節點,偏移為0

         chopped_off = 0; 

         while (pn) {

                 pos = pn->pos;// 得到當前節點的比較位偏移量,指示此位後不同。

                 bits = pn->bits;

                 if (!chopped_off)// 以下尋找孩子,沒有回溯的情況下chopped_off 始終為0

                         // 一會解釋以下這個函式

                         cindex = tkey_extract_bits(MASK_PFX(key, current_prefix_length), pos, bits);

                 n = tnode_get_child(pn, cindex);

                 if (n == NULL) {

                         goto backtrace;

                 }

                 if (IS_LEAF(n)) {// 如果得到的孩子是葉子,那麼定乾坤的時候到了。

                         if ((ret = check_leaf(t, (struct leaf *)n, key, &plen, flp, res)) <= 0)

                                 goto found;

                         else

                                 goto backtrace;

                 }

                 cn = (struct tnode *)n;// 開始和此孩子比較

                 if (current_prefix_length < pos+bits) {// 在尋找普適路由的情況下會出現這種情況,也就是說,下面的操作意義在於一旦發現這條路由有一位不是0 那麼就不可能是普適路由,於是回溯。

                      if (tkey_extract_bits(cn->key, current_prefix_length,

                             cn->pos - current_prefix_length) != 0 ||

                             !(cn->child[0]))

                                 goto backtrace;

                 }

                 node_prefix = MASK_PFX(cn->key, cn->pos);// 得到孩子節點到它的pos 為止的字首

                 key_prefix = MASK_PFX(key, cn->pos);     // 得到查詢鍵到當前孩子節點的pos 為止的字首

                 pref_mismatch = key_prefix^node_prefix;// 比較兩個字首

                 mp = 0;// 為了找到查詢鍵和當前孩子節點從左邊數第一個不同的位置而設定的一個變數

                 if (pref_mismatch) {// 如果有不同的位,那麼就:1. 可能回溯;2. 可能是一條普適路由。

                         while (!(pref_mismatch & (1<<(KEYLENGTH-1)))) {// 此迴圈找到二者從左邊數第一個不同的位的偏移。

                                 mp++;

                                 pref_mismatch = pref_mismatch <<1;

                         }

                         key_prefix = tkey_extract_bits(cn-& gt;key, mp, cn->pos-mp);// 此操作就是看看該孩子節點從和查詢建不同的位開始一直到它的pos 是不是全0 ,若是,那麼它 有可能是一條普適路由的一部分,若否,則只有回溯去查詢更大範圍的普適路由了

                         if (key_prefix != 0)

                                 goto backtrace;

                         if (current_prefix_length >= cn->pos)

                                 current_prefix_length = mp;// 注意,非回溯的情況下只在這裡更新current_prefix_length ,它的目的就是查詢普適路由,注意,事故已經發生了,我們在挽救。

                 }

                 pn = (struct tnode *)n; /* Descend */

                 chopped_off = 0;

                 continue; 

 backtrace:

                 chopped_off++;// chopped_off 遞增,表示要回溯,回溯就不要找孩子節點了,因為在chopped_off 大於的情況下,表示孩子都已經測試過了,不匹配,需要再向上尋找更大範圍的普適路由。

                 /* As zero don't change the child key (cindex) */

                 while ((chopped_off <= pn->bits) && !(cindex & (1<<(chopped_off-1))))

                         chopped_off++;

                // 以下就是回溯的具體實施了,就是一些位運算了。

                 if (current_prefix_length > pn->pos + pn->bits - chopped_off)

                         current_prefix_length = pn->pos + pn->bits - chopped_off; 

                 if (chopped_off <= pn-& gt;bits) {// 得到可能的 下一個 孩子節點,在33 行選擇的孩子已經失敗,那麼要選擇下一個了,有下一個嗎?實際上可能有的,比如這次查詢失 敗的孩子在pnbits 是二進位制1101 ,那麼如果有一個孩子的相應位是11001000 就是回溯的物件,這也是這裡位運算的目的。

                         cindex &= ~(1 << (chopped_off-1));

                 } else {// 如果沒有,那麼也就只能回到更上一級的爺爺那裡了。

                         if (NODE_PARENT(pn) == NULL)

                                 goto failed;

                         /* Get Child's index */

                         cindex = tkey_extract_bits(pn->key, NODE_PARENT(pn)->pos, NODE_PARENT(pn)->bits);

                         pn = NODE_PARENT(pn);

                         chopped_off = 0;

                         goto backtrace;

                 }

         }

 failed:

         ret = 1;

 found:

         rcu_read_unlock();

         return ret;

 }

最後看看那個被忽略的函式:

 static inline t_key tkey_extract_bits(t_key a, int offset, int bits)

 {// 這個函式的本質就是混略掉我們當前考慮的以低的位和以高的位從而得到一個索引一樣的資料,畢竟那些高位以前已經考慮過了,而低位暫時還用不到。

         if (offset < KEYLENGTH)

                 return ((t_key)(a << offset)) >> (KEYLENGTH - bits);

         else

                 return 0;

 }

efine MASK_PFX(k, l) (((l)==0)?0:(k >> (KEYLENGTH-l)) << (KEYLENGTH-l))

以上就是查詢演算法的全部了,至於說為何這麼簡單,還是要看另一頭的操作,就是插入操作,那個操作就複雜多了,這就叫做起來難,用起來容易。在查詢演算法中,父輩們往往把責任往子孫們身上推卸,等到子孫解決不了了,再回溯給父輩們,真是有意思。