多种路由查找算法原理

概述

由于IP协议没有方向,甚至它都没有会话的概念,因此路由必然要是双向的,否则数据就有去无回了。
路由由三元素组成:目标地址,掩码,下一跳。注意,路由项中其实没有输出端口-它是链路层概念,Linux操作系统将路由表和转发表混为一谈。

概念

  • 路由项通过两种途径加入内核:
  • 通过用户态路由协议进程或者用户静态配置配置加入
  • 主机自动发现的路由

所谓自动发现的路由实际上是“发现了一个路由项和一个转发表”,其含义在主机某一个网卡启动的时候生效,比如eth0启动,那么系统生成下列路由表项/转发项:往eth0同一IP网段的包通过eth0发出。

  • 路由框架的层次
    路由大致分为两个要素,也可以看成两个层次。第一个层次是路由表项的生成;第二个层次是主机对路由表项的查找。

  • 路由表项生成算法
    生成路由表项的方式有两种,第一种是管理员手工配置,第二种为通过路由协议动态生成。

Linux的路由查找-哈希查找算法

这是Linux操作系统的经典的路由查找算法,直到现在还是默认的路由查找算法。然而它很简单。由于它的简单性,内核(kernel)开发组一直很推崇它,虽然它有这样那样的局限性,但由于Linux内核的哲学就是“够用即可”,因为Linux几乎从来不被用于专业的核心网络路由系统,因此哈希查找法一直都是默认的选择。

查找过程

在这里插入图片描述
查找顺序如下图所示:
在这里插入图片描述

为了实现最长前缀匹配,从最长的掩码开始匹配,每一个掩码都有一个哈希表,目的IP地址哈希到这些哈希表的特定的桶中,然后遍历其冲突链表得到最终结果。

  • 分析
    哈希查找算法是基于掩码的遍历来实现严格的最长前缀匹配的,也就是说如果一条最终将要通过默认网关发出的数据报,它起码要匹配32次才能得到结果。 这种方式十分类似于传统的Netfilter的filter表的过滤方式-一个一个尝试匹配,而不像HiPac的过滤方式,是基于查找的。接下来我们会看到,高性能的路由器在查找路由的时候使用的都是基于查找型数据结构的方式,最常用的就是查找树了。

局限性

一个特定的哈希函数只适合一定数量的匹配项,几乎很难找到一个通用的哈希函数,能够适应从几个匹配项到几千万个匹配项的情形。
一般而言,随着匹配项的增加,哈希碰撞也会随着增加,并且其时间复杂性不可控,这是一个很大的问题,这个问题阻止了哈希路由查找算法走向核心专用路由器,限制了Linux路由的规模,它根本不可能使用哈希来应对大型互联网络或者BGP之类的域间路由协议产生的大量路由信息。

核心路由器上,使用哈希算法无疑是不妥的,必定需要找到一种算法,使得其查找的时间复杂度限制在一个范围。基于树的查找算法可以做到这一点,实际上,很多的路由器都是使用基于树的查找算法来实现的。我们先从Linux的trie树开始。便于查阅代码(虽然本文不分析代码…)。

Linux的LC-Trie树查找算法

trie算法分为三大块,第一块是查找,第二块是插入/删除,第三块是平衡。

基本理论

我们可以通过电话号码来认识trie树,trie树本质上是一棵检索树,和全球电话号码簿一样,我们知道,电话号码有三部分组成:国家码+地区号+号码,比如086+372+5912345,如果从美国拨出这个号码,首先要决定送往哪个国家,所要做的就是用确定位数的国家码和出口交换机的转发表的国家码部分进行匹配,发现086正好是中国,然后该号码到达中国后;再匹配区号,发现要送往安阳市,最后到达安阳市;然后将请求发往5912345这个号码。

现在的问题是,在每一个环节如何使用最快的方式检索到请求下一步要发往哪里?
我想最好的方式就是使用 “桶算法”,举个例子,在美国的电话请求出口处放置一张表,表项有X个,其中X代表全球所有国家和地区的总和,中国的国家码是086,那么它就是第86个表项,这样直接取第86个表项,得到相应的交换信息,电话请求通过信息中指示的链路发往中国…

trie树,其实和上述的结构差不多,只不过上述结构的检索分段是固定的,比如电话号码就是3位10进制数字等,且匹配检测索引的位置也是固定的,比如电话号码的地区号就是从第4位十进制数字开始等。
对于trie树而言,需要检测的位置不是固定的,它用pos表示,而检测索引的长度也不固定,它由bits表示,我们把每一个检测点定为一个CheckNode,它的结构体如下:

CheckNode{
    int pos;
    int bits;
    Node children[1<<bits];
}
union Node{
    Leaf entry;
    CheckNode node;
}

pos和bits是一个CheckNode的核心,pos指示从哪一位开始检测,bits指示了孩子结点数组,直接取key[pos…pos+bits]即可直接取到孩子结点。
在这里插入图片描述

trie树的插入

  • 第一步,如果一个CheckNode节点都没有,则创建根CheckNode节点,并且创建一个叶子,结束。注意,每一个路由项都是一片叶子。如果已经有了根CheckNode,则需要计算新节点插入的位置。
  • 第二步,计算插入位置前的位置匹配。步骤如下:

根据已有CheckNode的pos/bits信息,从根开始执行一系列比较:

  • 1).取出根CheckNode
  • 2).设当前CheckNode为PreCheckNode
  • 3).判断是否需要继续匹配。
  • 4).如果需要继续匹配,则看看自己是其哪个孩子或者该孩子的分支,并且取出该孩子Child-CheckNode为当前CheckNode,回到2。
  • 5).如果不需要继续匹配,退出匹配过程

其中判断CheckNode是否需要继续匹配其Child-CheckNode的算法如下:
在这里插入图片描述
NewKey和CheckNode在上述的蓝色虚线区域内只要有不同的bit,则不必再和Child-CheckNode继续匹配了,可以确定,NewKey肯定插入后作为PreCheckNode的某个孩子了。如果需要继续匹配,判断是哪个孩子的方式如下:
在这里插入图片描述
确定插入位置并且插入,步骤如下:

0).如果没有发生第二步中的和Child-CheckNode不匹配的情形,则直接将NewKey作为叶子作为PreCheckNode的第NewKey[PreCheckNode的pos…PreCheckNode的pos+PreCheckNode的bits]插入,结束。否则执行下面的步骤,处理和Child-CheckNode的冲突
1).创建一个CheckNode,然后看下图:
在这里插入图片描述
假设上图中的绿色圈起来的位是Child-CheckNode和NewKey首次不匹配的地方,记为miss,那么NewKey将创建一个新的CheckNode,记为NewNode,其POS为miss,其bits为1,这样原来的Child-CheckNode就成了NewNode的一个孩子,而待插入的NewKey创建一个新的叶子,作为NewNode的另一个孩子。NewNode代替Child-CheckNode作为PreCheckNode的孩子插入其孩子数组中。

基本上,上述的过程已经很清楚了,然而给出一个例子会更好些,接下来我给出一个例子,依次插入3条路由项:
1:192.168.10.0/24
2:192.168.20.0/24
3:2.232.20.0/24
然后我们看图说话,首先看一下比特图:
在这里插入图片描述
接下来看一下插入trie的情形:
在这里插入图片描述

trie平衡以及多路trie

如果我们现在还想不到作为路由表的trie树长什么样子,我们可以先考虑一下页表,毕竟这是实现虚拟内存的关键,处理器设计者一定会选择一种相当高效的方式来从虚拟地址查找物理地址的,页表使用分段索引的方式来快速定位页表项,也就是说将一个虚拟地址分为N段,每一段定位一个索引,然而将这些索引层接起来就是最终的页表项。
如果把页表结构从页目录展开来看的话,页表结构就是一棵大分叉的树,足有4096叉,然而却不高,也就两层到四层。我们想一下它为何如此高效,因为它比较矮小,索引可以快速定位树的分支,最终快速到达叶子。
但是,树矮小的代价是什么?时间复杂度小了,空间复杂度一般都会变大。它太耗内存了。因此最好的方案就是,树不能太高,也不能太矮。多路的trie树就是这样设计的。极端情况下,多路trie树会退化成一个链表或者进化成一棵“2的32次方”叉的只有两层的树:

链表情形-bits=0:
在这里插入图片描述
多叉树情形-bits=32
在这里插入图片描述
多路trie的本质在于其“多路”,而多路的本质在于CheckNode的bits字段。看一下上面讲查入时的例子,此时我们又多了一个路由项从而多了一个节点,首先看比特图:
在这里插入图片描述
再看一下多路trie树:
在这里插入图片描述
所谓的平衡操作很简单,每次插入新的节点都会平衡这棵树,原则如下:

  • 1).如果太高了,那么就压胖它。
    使该CheckNode的pos不变,bits加1,使得其孩子的容量增大一倍,然后依次将其孩子重新加入新的CheckNode,加入过程中递归执行平衡操作。

  • 2).如果太胖了,那就拉高它。
    使该CheckNode的pos不变,bits减1,使得其孩子的容量减少一倍,然后依次将其孩子重新加入新的CHeckNode,加入过程中递归执行平衡操作。

    总之,Linux实现的trie树是动态变化的,这种动态变化的优点是可以根据系统当前的负载以及内存情况动态对trie树的形态做出调整,使得资源的总体利用率提高,然而也有缺点,那就是算法本身太复杂,不适合做扩展,最重要的是不适合用硬件实现。

trie树的查找

回溯优化

总的评价

  • 哈希算法
    哈希函数的可扩展性很差,我本身也不是很喜欢这个东西,虽然Linux内核中大量使用了哈希,但是正是这些哈希限制了Linux支持应用的规模,寻找好的哈希函数简直太难了,如果这会儿你的西墙倒了,并且你此时并不在乎东墙,那么你就用哈希吧,拆了东墙补西墙!
  • 树算法
    树算法是不错的选择,确定性强,而且越是简单的树实际上效率越高,这是为什么呢?因为易于用硬件实现,专业级的硬件还是要比单纯使用cpu的软件效率高几个级别的。
    是设计高效复杂的纯软件算法还是用硬件实现一个简单然而并不怎么高效的算法,这是一个问题。基本上可以确定,一般而言,纯硬件实现的遍历要比纯软件实现的哈希好很多,硬件是信号,电流驱动的,而软件依赖cpu指令,时钟周期等…

本文基本就介绍了路由查找使用的两种树,第一种是二叉树,如下图:
在这里插入图片描述
第二种是256叉树,如下图:
在这里插入图片描述
另外一种树,多路动态的trie树,实际上是介于退化成链表的二叉树和2的32次方叉树之间的一种树。

参考

https://blog.csdn.net/dog250/article/details/6596046
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值