2025年在一棵IPv4地址树中彻底理解IP路由表的各种查找过程

在一棵IPv4地址树中彻底理解IP路由表的各种查找过程1 IPv4 地址空间树 IPv4 的整个地址空间可以构成一棵完美的二叉树 因为它完全占满了整个 4G 的地址空间 这棵树如下所示 需要指明的是 完全画出这幅图是不可能的 如果一个节点的直径小到 1mm 这意味你要拿放大镜去看小圆圈里存储的信息 我并没有在圈圈里写任何信息 我怕它们被有损压缩了

大家好,我是讯享网,很高兴认识大家。

1.IPv4地址空间树

IPv4的整个地址空间可以构成一棵完美的二叉树,因为它完全占满了整个4G的地址空间。这棵树如下所示:


讯享网

需要指明的是,完全画出这幅图是不可能的,如果一个节点的直径小到1mm(这意味你要拿放大镜去看小圆圈里存储的信息[我并没有在圈圈里写任何信息,我怕它们被有损压缩了...模拟情况下用放大镜可见,数字图片一旦被有损压缩,拿放大镜看到的就是一个个方块,学名阻碍进步的马赛克-希腊/罗马的遗产]),那么最下层将会有4000km长,正好是东北黑龙江的漠河到西藏日喀则的距离,如果看完整个图,相当于环中国旅行了...所以我只能画一部分,由于不可能展示整张图,所以我也没有必要将节点直径缩至1mm了。
       通过这个图,你会发现,给定一个IP地址,你可以从ROOT开始,逐bit比较游历,最终在最底层找到它的位置,这个过程是如此快,只需要32步,以至于你根本不能想象漠河到日喀则的距离,但是你如果听说过纸张对折到月球,估计就彻底理解了,这就是指数的魅力和危险。脑子里装进这张图,然后在中间加入一些信息,你将彻底理解IP路由表的查找过程,我们现在就开始。以下的内容仅仅考虑IPv4。

2.路由查找

路由查找就是为一个目标IP地址找到一个下一跳,即找到一个路由项。路由项的key是一个bit前缀,比如192.168.0.0/16,172.16.20.0/24,1.2.3.0/28,3.4.5.6/32等,而这些key作为IP的表达方式,所不同的是它们仅仅考虑32位中的某些连续的位而不一定非要是全部,我们会发现,对于32位前缀的路由项key,它们对应最底层叶子节点的某些节点,而对于小于32位前缀的路由项key,它们正好对应一些中间结点。因此我们基于IPv4地址树而得到了一棵新的树,即带有路由节点的IP地址树:


路由查找的过程这下就很明晰了,既找到一个输入目标IP地址在游历整棵树过程中经过的那个最精确的路由项节点。我们可以发现,越靠近叶子的路由项越精确,因为它“马上就要到达目的地”了。
       路由节点作为中间结点或者叶子节点,我们有两种方式得达到它。

2.1.自叶子而上查找

假设我们已经知道了输入IP地址在最底层的位置,那么向ROOT游历,遇到的第一个路由节点肯定是最精确的。然而假设并不代表实际,这是一种执果索因法,我们并不知道输入IP的实际位置,我们也不想从漠河游历到日喀则,因此如何缩短路径就是要解决的问题。我们的问题是:
1).有32位前缀的精确路由吗?如果有,里面有没有和输入IP地址一样的呢?如果有,那就是它了,如果没有的话...
2).有31位前缀的路由吗?如果有,有哪个或者哪些(负载均衡?度量?...)的前31位值和输入IP的前31位相等呢?如果有,那就是它了,如果没有的话...
3).有30位前缀的路由吗?如果有,有哪个或者哪些(负载均衡?度量?...)的前30位值和输入IP的前30位相等呢?如果有,那就是它了,如果没有的话...
4).有29位前缀的路由吗?如果有,有哪个或者哪些(负载均衡?度量?...)的前29位值和输入IP的前30位相等呢?如果有,那就是它了,如果没有的话...
...
逻辑是简单的,问题是我们怎么知道到底有没有,算法上如何去实施。答案当然也很显然。那就是把所有的路由项按照前缀自大到小进行排序,每个前缀拥有一个链表,每个链表节点都是一个路由项,如下:
32 prefix list:node32-1,node32-2,node32-3,node32-4
31 prefix list:node31-1,node31-2,node31-3,node31-4...
30 prefix list:null
29 prefix list:node29-1,node29-2
...
最简单的方式就是拿输入IP地址依次从上到下,每个链表从左到右去遍历上述结构。然而事情到了如此明了的地步,是个人应该都可以想到使用HASH来加快计算效率的吧。因此上述结构变成了:
32 prefix hashlist:hash1(node32-1,node32-4),hash2(node32-3),hash3(node32-2)
31 prefix hashlist:hash1(node31-8,node31-5,node31-3),hash2(node31-1),hash3(...)
30 prefix hashlist:hash1(null),hahs2(null),hash3(null)
29 prefix hashlist:hash1(node29-2),hash1(null),hash2(node29-2)
...
这下就免除了每个前缀链表遍历的困境,只需要计算 bucket=hashcalc(IP_input) ,然后在每一个前缀hash表的hash[bucket]冲突链表中遍历一小部分就好了。
       自下而上查找总是从最精确的前缀开始的,旨在找到第一个匹配的路由项,而下面要详细描述的自上而下的查找则是从最不精确的前缀开始,旨在找到最后一个匹配的路由项,咋一看,自下而上占了优势,然而事实上,自上而下的方案也不赖。
       自下而上的算法是如此显然,以至于再多增加一些笔墨就显得多余,所以接下来的大部分篇幅,我想留给另外一种截然相反的算法。
注解 :自下而上的算法难道不就是Linux路由表hash查找的算法吗?

2.2.自根而下查找

从IPv4地址空间树(简称地址树)来看,由于它是一棵二叉树,精确匹配了32位IPv4地址的每一位,因此沿着根而下,最终肯定能到达目标IP地址,而这一路上最后经过的那个路由节点(黑色)就是所要查找的路由,这是很显然的,从带有路由的IPv4地址树上我们可以很容易看出这一点。
      虽然沿着IPv4地址树自上而下查找浪费不了太多时间,最多也就匹配32次,然而构建一颗完整的IPv4地址树却需要太大的空间。而这是要避免的,这么大的空间不仅仅难于利用核心缓存,同时它真的是不必要的,因为有更加优化的方式来解决路由查找问题。为此,我们需要对这棵带有路由的IPv4地址树做一些变换。
      那棵庞大的IPv4地址树仅仅是为了给出一个理解路由查找原理的方式,实际上我们关注的应该是:如何使得一个特定的IPv4地址,即目标地址能够快速的到达某个路由节点或者快速发现没有这样的路由节点。要达到这样的目的,就不得不掠过很多不带有路由项的“空心节点”,为此我们需要将这棵IP地址树进行压缩,从而最终在这棵树上仅仅保留最少数量的“空心节点”,它们存在的目的,仅仅是快速引导我们到达某一个实心的路由节点。
重新安排压缩后的路由节点的位置
在带有路由节点的IP地址图上,我们可以发现,一个目标IP地址最终使用的路由是从根部直到叶子(即它本身)的无环路径中最后一个经过的路由节点,即越靠近叶子节点的路由项目越优先被使用,因为它更加精确。最终将IP地址树压缩后,路由节点将全部被保留,此时,我们可能会有下面的子树:


我们是如何将此子树进行变换以更方便的进行上述的那个从根到叶子的游历过程呢?在此,我给出两种方式:
方式1:合并相同key路由节点为一个携带掩码链表的新节点


后面我们会谈到,这种方式更加有效。BSD和Linux都采用了这种方式。使用这种方式,所有的路由节点都被安排到了叶子,非常方便于Level压缩。
方式2:路由节点新增元信息,指示路径状况


可见,每个路由节点携带了几个不同的路径元信息:
Left more :左边子树是否还有新的路由项,如果有,且当前结果完全匹配并下一步要步入左边,那么继续匹配更精确的左子树。
Right more :同上,左换成右
Pre node idx :路径中上一个路由节点的索引(为此需要将所有路由项进行索引),如果当前节点不匹配,则直接取出上一个节点来使用,如果当前节点匹配且Left/Right more为0,则使用当前节点。
由于IP地址树存在压缩(马上就会讲到),因此这种方式不如方式1应用广泛,因为存在压缩的情况下,即便当前路由节点不匹配,上一个也不一定匹配,再者,由于存在动态的路径压缩和Level压缩,节点间的关系也会动态变化,叶子和非叶子节点也会变化,而方式2需要不断追踪节点之间的关系,开销比较高。
IPv4地址树的压缩
前面屡次提到地址树的压缩,现在终于可以单独描述它了。地址树的压缩不仅仅节省了空间,还优化了时间开销(但不经常,也不绝对,如果考虑恶性回溯,可能会恶化时间开销)。但是压缩也是有代价的,那就是在查找的时候可能需要回溯,而我们知道,在标准的完整IP地址树上查找是不需要回溯的。但是有时回溯即便在压缩的情况下也可以避免,这个不取决于算法,而是取决于你的输入IP地址,它是算法无关的。因此权衡来看,冒险是值得的。
       在我描述地址树压缩的时候,有个前提,那就是一切都是基于前面一小节的方式1来变换的。关于地址树的压缩,事实上它就是标准二叉树的压缩,方案有两类,路径压缩和Level压缩。
1.路径压缩
路径压缩很简单,看一个图自然就会明白。不过我不准备用那个及其庞大的IP地址图来开刀,它实在太大了,我只是用它的上面一部分来说明原理:


可以看出,路径压缩做的事比较简单,操作也比较简单,它的目的就是忽略和路由项无关的节点,仅仅保留路由项以及路由项引导项节点。所有的输入IP地址在查找路由的时候,不必再逐位比较,而仅仅和路由项的检查位比较即可,最终顺着一条路径到达一个叶子节点,该节点即最终找到的路由项。

      由于采用了方式1进行了路由项合并,因此需要用输入IP逐个检查叶子路由项的掩码链表,最终选出掩码最长的那个路由项。然而由于存在路径压缩,可能这一步并不会成功。我来解释一下这是为什么。请看上图的路由节点路径上的X位,对于路由项的key而言(比如192.168.1.0/24,其key就是192.168.1.0),它将所有的被压缩的bit都假设为0或者模式相同的若干位,如都是1,都是11010等,即不检查,也就是它们被忽略了,造成的结果就是,输入IP地址的这些bit可能有不是0的,这就会造成最终的叶子节点的精确匹配不会通过。而此时,就需要回溯了。这是下一节的主题。
       对于如何游历而言,很简单,每一个节点都是类似下面的结构体:
[plain]  view plain  copy
小讯
上一篇 2025-03-29 21:34
下一篇 2025-02-09 15:21

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/37083.html