路由表的结构与算法分析--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))


以上就是查找算法的全部了,至于说为何这么简单,还是要看另一头的操作,就是插入操作,那个操作就复杂多了,这就叫做起来难,用起来容易。在查找算法中,父辈们往往把责任往子孙们身上推卸,等到子孙解决不了了,再回溯给父辈们,真是有意思。

 

你可能感兴趣的:(路由表的结构与算法分析--trie查找)