Linux路由表的结构与算法分析

路 由是网络栈的核心部分。路由表本身的设计很大情度上影响着路由的性能,并且好的设计 能减少系统资源的消耗,这两方面尤其体现在路由表的查找上。目前的内核路由存在两种查找算法,一种为HASH算法,另一种为LC-trie算法,前者是目 前内核使用的缺省算法,而后者更适用在超大路由表的情况,它在这种情况提高查找效率的同时,大大地增加了算法本身的复杂性和内存的消耗。综上,这两种算法 各有其适用的场合,本文分析了基于2.6.18内核路由部分的代码在HASH算法上路由表结构的实现,并且在文章最后给出了一个简单的策略路由的应用。
一、路由表的结构
       为 了支持策略路由,Linux使用了多个路由表而不是一个,即使不使用策略路由,Linux也使用了 两个路由表,一个用于上传给本地上层协议,另一个则用于转发。Linux使用多个路由表而不是一个,使不同策略的路由存放在不同的表中,有效地被免了查找 庞大的路由表,在一定情度上提高了查找了效率。
       路 由表本身不是由一个结构表示,而是由多个结构组合而成。路由表可以说是一个分层的结构组合。在第一 层,它先将所有的路由根据子网掩码(netmask)的长度(0~32)分成33个部分(struct fn_zone),然后在同一子网掩码(同一层)中,再根据子网的不同(如10.1.1.0/24和10.1.2.0/24),划分为第二层 (struct fib_node),在同一子网中,有可能由于TOS等属性的不同而使用不同的路由,这就是第三层(struct fib_alias),第三层结构表示一个路由表项,而每个路由表项又包括一个相应的参数,如协议,下一跳路由地址等等,这就是第四层(struct fib_info)。分层的好处是显而易见的,它使路由表的更加优化,逻辑上也更加清淅,并且使数据可以共享(如struct fib_info),从而减少了数据的冗余。
struct fib_table *fib_tables[RT_TABLE_MAX+1]; // RT_TABLE_MAX 为255
       图1为一个路由表的总体结构。自上而下由左向右看,它首先为一个fib_table结构指针的数组,它被定义为:
struct fib_table {
       unsigned char tb_id;
       unsigned tb_stamp;
       int           (*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res);
       int           (*tb_insert)(struct fib_table *table, struct rtmsg *r,
                     ……
       void        (*tb_select_default)(struct fib_table *table,
                                        const struct flowi *flp, struct fib_result *res);
       unsigned char tb_data[0];
};
       每个fib_table结构在内核中表示一个路由表:
       +
图1(引自[1])
这个结构中包括这个表的ID,以及主要的一些用于操作路由表的函数指针,这里我们只关心最后一个域——tb_data[0],这是一个零长的数组,它在内核中也较为常见,它表示
struct fn_hash {
struct fn_zone *fn_zones[33];
struct fn_zone *fn_zone_list;
};
指向这个结构的末尾。由图1可以看到,这个结构的末尾接着便是一个struct fn_hash结构,这个结构是随着fib_table结构一起分配的,所以fib_table->tb_data就是fn_hash。
struct fn_zone {
       struct fn_zone          *fz_next; /* Next not empty zone */
       struct hlist_head     *fz_hash;       /* Hash table pointer      */
       int                                fz_nent;   /* Number of entries      */
       int                                fz_divisor;      /* Hash divisor              */
       u32                             fz_hashmask; /* (fz_divisor - 1)   */
#define FZ_HASHMASK(fz)         ((fz)->fz_hashmask)
       int                              fz_order; /* Zone order         */
       u32                             fz_mask;
#define FZ_MASK(fz)          ((fz)->fz_mask)
};

       这个fn_zone域就是我们上面提前的结构,用于将路由根据子网掩码的长度分开成33个部分,其中fn_zones[0]用于默认网关。而fn_zone_list域就是将正在使用的fn_zone链成一个链表。接着再深入到struct fn_zone结构中:

这个结构中有两个域比较重要,一个为fz_hash域,它指向一个HASH表的表头,这个HASH的长度是fz_divisor。并且这个HASH表的长度是可变的,当表长达到一个限定值时,将重建这个HASH表,被免出现HASH冲突表过长造成查找效率降低。
为了提高查找的效率,内核使用了大量的HASH表,而路由表就是一个例子。在图1中可以看到,等长子网掩码的路由存放在同一个fn_zone中,而根据到不同子网(fib_node)的路由键值(fn_key),将它HASH到相应的链表中。
struct fib_node {
       struct hlist_node     fn_hash;
       struct list_head       fn_alias;
       u32                fn_key;
};
这 个键值其实就是这个子网值了(如10.1.1.0/24,则子网值为 10.1.1),得到这个键值通过n = fn_hash()函数HASH之后就是这个子网对应的HASH值,然后就可以插入到相应的fz_hash[n]链表中了。冲突的fib_node由 fn_hash域相链,而fn_alias则是指向到达这个子网的路由了。
struct fib_alias {
       struct list_head       fa_list;
       struct rcu_head      rcu;
       struct fib_info        *fa_info;
       u8                  fa_tos;
       u8                  fa_type;
       u8                  fa_scope;
       u8                  fa_state;
};
当到达这个子网的路由由于TOS等属性的不同可存在着多个路由时,它们就通过fib_alias中fa_list域将这些路由表项链成一个链表。这个结构中的另一个域fa_info指向一个fib_info结构,这个才是存放真正重要路由信息的结构。
struct fib_info {
       struct hlist_node     fib_hash;
       struct hlist_node     fib_lhash;
       ……
       int                  fib_dead;
       unsigned         fib_flags;
       int                  fib_protocol;
       u32                fib_prefsrc;
       u32                fib_priority;
       ……
int                         fib_nhs;
       struct fib_nh          fib_nh[0];
#define fib_dev             fib_nh[0].nh_dev
};
这 个结构里面是一个用于路由的标志和属性,其中最重要的一个域是fib_nh[0], 在这里,我们再次看到了零长数组的应用,它是通过零长来实现变长结构的功能的。因为,我们需要一个定长的fib_info结构,但是在这个结构末尾,我们 需要的fib_nh结构的个数是不确定的,它在运行时确定。这样,我们就可以通过这种结构组成,在运行时为fib_info分配空间的时候,同时在其末尾 分配所需的若干个fib_nh结构数组,并且这个结构数组可以通过fib_info->fib_nh[n]来访问,在完成fib_info的分配后 将fib_nhs域置为这个数组的长度。
另 一方面,fib_info也是HASH表的一个应用,结构中存在着两个域,分别是 fib_hash 和fib_lhash,它们都用于HASH链表。这个结构在完成分配后,将被用fib_hash域链入fib_info_hash表中,如果这个路由存在 首选源地址,这个fib_info将同时被用fib_lhash链入fib_info_laddrhash表中。这样,就可以根据不同目的实现快速查找 了。
Struct fib_nh也是一个重要的结构。它存放着下一跳路由的地址(nh_gw)。刚刚已经提到,一个路由(fib_alias)可能有多个fib_nh结构, 它表示这个路由有多个下一跳地址,即它是多路径(multipath)的。下一跳地址的选择也有多种算法,这些算法都是基于nh_weight, nh_power域的。nh_hash域则是用于将nh_hash链入HASH表的。
struct fib_nh {
       struct net_device    *nh_dev;
       struct hlist_node     nh_hash;
       struct fib_info        *nh_parent;
       unsigned                nh_flags;
       unsigned char        nh_scope;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
       int                  nh_weight;
       int                  nh_power;
#endif
#ifdef CONFIG_NET_CLS_ROUTE
       __u32                   nh_tclassid;
#endif
       int                  nh_oif;
       u32                nh_gw;
};
二、路由的查找
       路由的查找速度直接影响着路由及整个网络栈的性能。路由的查找当然首先发生在路由缓存中,当在缓存中查找失败时,它再转去路由表中查找,这是本文所关注的地方。
       上一节已经详细地描述了路由表的组成。当一个主要的IP层将要发送或接收到一个IP数据包时,它就要调用路由子系统完成路由的查找工作。路由表查找就是根据给定的参数,在某一个路由表中找到合适的下一跳路由的地址。
       上 面已提到过,当一个主机不支持策略路由时,它只使用了两个路由表,一个是 ip_fib_local_table,用于本地,另一个是ip_fib_main_table,用于接发。只有在查找 ip_fib_local_table表时没有找到匹配的路由(不是发给本地的)它才会去查找ip_fib_main_table。当一个主机支持策略路 由时,它就有可能存在着多个路由表,因而路由表的选择也就是查找的一部分。路由表的选择是由策略来确定的,而策略则是由应用(用户)来指定的,如能过ip rule命令:
ip rule add from 10.1.1.0/24 table TR1
ip rule add iff eth0 table RT2
       如上,第一条命令创建了基于源地址路由的一条策略,这个策略使用了RT1这个路由表,第二条命令创建了基于数据包入口的一个策略,这个策略使用了RT2这个路由表。当被指定的路由表不存在时,相应的路由表将被创建。
       第二步就是遍历这个路由表的fn_zone,遍历是从最长前缀(子网掩码最长)的fn_zone开始的,直到找到或出错为止。因为最长前缀才是最匹配的。假设有如下一个路由表:
dst                 nexthop               dev
       10.1.0.0/16       10.1.1.1                     eth0
       10.1.0.0/24          10.1.0.1               eth1
它会先找到第二条路由,然后选择10.1.0.1作为下一跳地址。但是,如果由第二步定位到的子网(fib_node)有多个路由,如下:
dst                 nexthop               dev
       10.1.0.0/24       10.1.0.1                     eth1
       10.1.0.0/24          10.1.0.2               eth1
到 达同一个子网有两个可选的路由,仅凭目的子网无法确定,这时,它就需要更多的信息来 确定路由的选择了,这就是用于查找路由的键值(struct flowi)还包括其它信息(如TOS)的原因。这样,它才能定位到对应一个路由的一个fib_alias实例。而它指向的fib_info就是路由所需 的信息了。
最后一步,如果内核被编译成支持多路径(multipath)路由,则fib_info中有多个fin_nh,这样,它还要从这个fib_nh数组中选出最合适的一个fib_nh,作为下一跳路由。
三、路由的插入与删除
       路由表的插入与删除可以看看是路由查找的一个应用,插入与删除的过程本身也包含一个查找的过程,这两个操作都需要检查被插入或被删除的路由表项是否存在,插入一个已经存在的路由表项要做特殊的处理,而删除一个不存在的路由表项当然会出错。
       下面看一个路由表插入的例子:
ip route add 10.0.1.0/24 nexthop via 10.0.1.1 weight 1
                                nexthop via 10.0.1.2 weight 2
                                   table RT3
这 个命令在内核中建立一条新的路由。它首先查找路由表RT3中的子网掩码长为24的 fn_zone,如果找不到,则创建一个fn_zone。接着,继续查找子网为10.0.1的fib_node,同样,如果不存在,创建一个 fib_node。然后它会在新建一个fib_info结构,这个结构包含2个fib_nh结构的数组(因为有两个nexthop),并根据用户空间传递 过来的信息初始化这个结构,最后内核再创建一个fib_alias结构(如果先前已经存在,则出错),并用fib_nh来创始化相应的域,最后将自己链入 fib_node的链中,这样就完成了路由的插入操作。
路由的删除操作是插入操作的逆过程,它包含一系列的查找与内存的释放操作,过程比较简单,这里就不再赘述了。
四、策略路由的一个简单应用
       Linux系 统在策略路由开启的时候将使用多个路由表,它不同于其它某些系统,在所有情况下都只使用 单个路由表。虽然使用单个路由表也可以实现策略路由,但是如本文之前所提到的,使用多个路由表可以得到更好的性能,特别在一个大型的路由系统中。下面只通 过简单的情况说明Linux下策略路由的应用。
如 图2,有如下一个应用需求,其中网关服务器上有三个网络接口。接口1的IP为 172.16.100.1,子网掩码为255.255.255.0,网关gw1为a.b.c.d,172.16.100.0/24这个网段的主机可以通过 这个网关上网;接口2的IP是172.16.10.1,子网掩码同接口一,网关gw2为e.f.g.h,172.16.10.0/24这个网段的主机可以 通过这个网关上网;接口0的IP为192.168.1.1,这个网段的主机由于网络带宽的需求需要通过e.f.g.h这个更快的网关路由出去。
图 2
步骤一:设置各个网络接口的IP,和默认网关:
ip addr add 172.16.100.1/24 dev eth1
ip route add default via a.b.c.d dev eth1
       其它接口IP的设置和第一个接口一样,这时,如果没有其它设置,则所有的数据通过这个默认网关路由出去。
步骤二:使子网172.16.10.0/24可以通过gw2路由出去
       ip route add 172.16.10.0/24 via e.f.g.h dev eth2
      
步骤三:添加一个路由表
       echo   “250 HS_RT” >> /etc/iproute2/rt_tables
步骤四:使用策略路由使192.168.1.0/24网段的主机可以通过e.f.g.h这个网关上网
       ip rule add from 192.168.1.0/24 dev eth0 table HS_RT pref 32765
       ip route add default via e.f.g.h dev eth2
       iptables –t nat –A POSTROUTING –s 192.168.1.0/24 –j MASQUERADE
     
步骤五:刷新路由cache,使新的路由表生效
ip route flush cache
这样就可以实现了以上要求的策略路由了,并且可以通过traceroute工具来检测上面的设置是否能正常工作。
参考资料:
[1] Understanding Linux Network Internals ,Christian Benvenuti
[2] http://www.kernel.org
[3]http://heuristic.kaist.ac.kr/paper/full-paper/24.%20IP%20Lookup%20Table%20Design%20Using%20LC-Trie%20with%20Memory%20Constraint.pdf
[4] iproute2 man page
[5] iptables howto
[6] http://lartc.org,Linux Advanced Routing & Traffic Control


浅析Linux Kernel 哈希路由表实现


1. 路由表

目前Linux内核中支持两种路由表,一种是Hash路由表,另一种是Trie路由表,Trie算法查找效率很高,但它也因极其消毫内存资源而闻 名,因此一般用在大型机上,否则在路由项过多时很可能造成内存的耗尽。在一般的机器上最好还是使用Hash路由表,之前有争论说Linux使用Hash路 由表相比于二叉树效率太低,不适合商用,其实只要设计良好的哈希算法,尽量减少哈希碰撞,Hash路由表的查找效率也是很高的,在最好的情况下算法复杂算 可以达到O(1),当然,最差也不过是O(n),我们有理由相信Linux中存在各种优秀的哈希算法,这些都是值得我们学习的。

Linux内核中IPv4路由表采用了分层的结构,第一层是是fib_table,表示路由表,最多可以支持256张路由表,用户可以根据策略路由 的需求来创建自己的路由表,系统默认的两张路由表为RT_TABLE_LOCAL和RT_TABLE_MAIN。下面是系统保留的路由表标识符:

enum rt_class_t {  RT_TABLE_UNSPEC=0, /* User defined values */  RT_TABLE_COMPAT=252,  RT_TABLE_DEFAULT=253,  RT_TABLE_MAIN=254,  RT_TABLE_LOCAL=255,  RT_TABLE_MAX=0xFFFFFFFF };

下面是路由表的定义:

struct fib_table {  struct hlist_node tb_hlist;  /* 路由表的标识,即为上面提到的类型 */  u32  tb_id;  /* 该路由中默认路由条目在哈希路由表中的索引,   在fib_table_select_default()函数中计算得出 */  int  tb_default;  /* Linux保留内存空间一惯的做法,这块空间留给了struct fn_hash */  unsigned char tb_data[0]; };

1.1 根据路由表ID来查找路由表。

struct fib_table *fib_get_table(struct net *net, u32 id) {  struct fib_table *tb;  struct hlist_node *node;  struct hlist_head *head;  unsigned int h;    if (id == 0)   id = RT_TABLE_MAIN;  h = id & (FIB_TABLE_HASHSZ - 1);    rcu_read_lock();  head = &net->ipv4.fib_table_hash[h];  hlist_for_each_entry_rcu(tb, node, head, tb_hlist) {   if (tb->tb_id == id) {    rcu_read_unlock();    return tb;   }  }  rcu_read_unlock();  return NULL; }

当路由表标识为0时默认返回主路由表,这里的哈希算法也很简单,当支持多径路由时FIB_TABLE_HASHSZ定义为256,否则FIB_TABLE_HASHSZ定义为2。也就是说在不支持多径路由的情况下,fib_table_hash中只有两个元素。

1.2 路由表的创建。

  struct fib_table *fib_new_table(struct net *net, u32 id) {  struct fib_table *tb;  unsigned int h;    if (id == 0)   id = RT_TABLE_MAIN;  tb = fib_get_table(net, id);  if (tb)   return tb;    tb = fib_hash_table(id);  if (!tb)   return NULL;  h = id & (FIB_TABLE_HASHSZ - 1);  hlist_add_head_rcu(&tb->tb_hlist, &net->ipv4.fib_table_hash[h]);  return tb; }

在这个函数里面首先在已有的哈希表中根据给出的路由表标识查找已存在的路由表,若找到对应的路由表则直接返回,否则调用 fib_hash_table()函数创建一个新的路由表,然后将其添加到fib_table_hash中去。接下来看一下 fib_hash_table()这个函数:

struct fib_table *fib_hash_table(u32 id) {  struct fib_table *tb;    tb = kmalloc(sizeof(struct fib_table) + sizeof(struct fn_hash),        GFP_KERNEL);  if (tb == NULL)   return NULL;    tb->tb_id = id;  tb->tb_default = -1;    memset(tb->tb_data, 0, sizeof(struct fn_hash));  return tb; }

这个函数很简单,分配了一块内存空间,大小为sizeof(struct fib_table) + sizeof(struct fn_hash),于是fib_table的最后一个字段tb_data便指向了这个struct fn_hash。接下来该说路由表的第二层了。

2. 路由域struct fn_zone

第二层是fn_zone,表示路由域,Linux根据路由掩码的长度将所有的路由分为32个域。我们先来看下位于fib_table末尾的struct fn_hash的定义:

struct fn_hash {  struct fn_zone  *fn_zones[33];  struct fn_zone __rcu *fn_zone_list; };

如上路由表分为32个路由域,这32个跌幅域又连成了链表,其中fn_zone_list这个字段指向链表的头。

  struct fn_zone {  struct fn_zone __rcu *fz_next; /* Next not empty zone */  struct hlist_head __rcu *fz_hash; /* Hash table pointer */  seqlock_t  fz_lock;  u32   fz_hashmask; /* (fz_divisor - 1) */    u8   fz_order; /* Zone order (0..32) */  u8   fz_revorder; /* 32 - fz_order */  __be32   fz_mask; /* inet_make_mask(order) */ #define FZ_MASK(fz)  ((fz)->fz_mask)    struct hlist_head fz_embedded_hash[EMBEDDED_HASH_SIZE];    int   fz_nent; /* Number of entries */  int   fz_divisor; /* Hash size (mask+1) */ };

2.1 fn_zone的创建

  static struct fn_zone * fn_new_zone(struct fn_hash *table, int z) {  int i;  struct fn_zone *fz = kzalloc(sizeof(struct fn_zone), GFP_KERNEL);  if (!fz)   return NULL;    seqlock_init(&fz->fz_lock);  fz->fz_divisor = z ? EMBEDDED_HASH_SIZE : 1;  fz->fz_hashmask = fz->fz_divisor - 1;  RCU_INIT_POINTER(fz->fz_hash, fz->fz_embedded_hash);  fz->fz_order = z;  fz->fz_revorder = 32 - z;  fz->fz_mask = inet_make_mask(z);    /* Find the first not empty zone with more specific mask */  for (i = z + 1; i <= 32; i++)   if (table->fn_zones[i])    break;  if (i > 32) {   /* No more specific masks, we are the first. */   rcu_assign_pointer(fz->fz_next,        rtnl_dereference(table->fn_zone_list));   rcu_assign_pointer(table->fn_zone_list, fz);  } else {   rcu_assign_pointer(fz->fz_next,        rtnl_dereference(table->fn_zones[i]->fz_next));   rcu_assign_pointer(table->fn_zones[i]->fz_next, fz);  }  table->fn_zones[z] = fz;  fib_hash_genid++;  return fz; }

首先在内存中给struct fn_zone分配内存空间,然后按规则对一些基本的变量进行初始化,之后就是安排各个fn_zone在fn_zone_list的位置 了,fn_zone_list中的节点都是按照fz_order从大到小排列的,所以首先一个for循环找出比当前fn_zone的fz_order大的 最小fn_zone,如果存在,就将当前节点插到该节点之后;如果不存在,则表示当前节点是目前zn_order最大的节点,则它会成为链表的头。

3. 路由节点fib_node

第三层为路由节点fib_node,在同一个路由域中的路由项有相同的掩码长度,但它们的网络号可以不同,则根据不同路由的网络号将同一个域划分为 不同的路由节点,如到地址10.10.65.0和10.10.64.0的路由便是属于同一个路由域里面两个不同路由节点的两条路由。在路由节点中又有根据 路由的服务类型,路由类型,路由寻址范围以前路由状态的不同将同一个路由节点中的路由划分成不同的路由

struct fib_node {  /* 哈希链表指针,针向同一个Hash槽中的相临节点 */  struct hlist_node fn_hash;  /* 属于该节点的alias链表的头 */  struct list_head fn_alias;  /* 该节点对应的网络key */  __be32   fn_key;  /* 预先分配了空间的别名,在fib_fast_alloc()中使用 */  struct fib_alias        fn_embedded_alias; };

3.1 fib_alias结构

struct fib_alias {  /* 指向链表中的相邻节点 */  struct list_head fa_list;  /* fa_info是最终的路由信息 */  struct fib_info  *fa_info;  /* 服务类型,对于一般服务取值为0 */  u8   fa_tos;  /* 路由类型 */  u8   fa_type;  /* 路由范围 */  u8   fa_scope;  /* 路由状态 */  u8   fa_state;  struct rcu_head  rcu; };

路由类型和路由范围的对应关系如下结构:

  static const struct {  int error;  u8 scope; } fib_props[RTN_MAX + 1] = {  [RTN_UNSPEC] = {   .error = 0,   .scope = RT_SCOPE_NOWHERE,  },  [RTN_UNICAST] = {   .error = 0,   .scope = RT_SCOPE_UNIVERSE,  },  [RTN_LOCAL] = {   .error = 0,   .scope = RT_SCOPE_HOST,  },  [RTN_BROADCAST] = {   .error = 0,   .scope = RT_SCOPE_LINK,  },  [RTN_ANYCAST] = {   .error = 0,   .scope = RT_SCOPE_LINK,  },  [RTN_MULTICAST] = {   .error = 0,   .scope = RT_SCOPE_UNIVERSE,  },  [RTN_BLACKHOLE] = {   .error = -EINVAL,   .scope = RT_SCOPE_UNIVERSE,  },  [RTN_UNREACHABLE] = {   .error = -EHOSTUNREACH,   .scope = RT_SCOPE_UNIVERSE,  },  [RTN_PROHIBIT] = {   .error = -EACCES,   .scope = RT_SCOPE_UNIVERSE,  },  [RTN_THROW] = {   .error = -EAGAIN,   .scope = RT_SCOPE_UNIVERSE,  },  [RTN_NAT] = {   .error = -EINVAL,   .scope = RT_SCOPE_NOWHERE,  },  [RTN_XRESOLVE] = {   .error = -EINVAL,   .scope = RT_SCOPE_NOWHERE,  }, };

3.2 fib_info结构

fib_alias中的fib_info为最终的路由信息,来看一下它的定义:

  struct fib_info {  struct hlist_node fib_hash;  struct hlist_node fib_lhash;  struct net  *fib_net;  int   fib_treeref;  atomic_t  fib_clntref;  int   fib_dead;  unsigned  fib_flags;  int   fib_protocol;  __be32   fib_prefsrc;  u32   fib_priority;  u32   fib_metrics[RTAX_MAX]; #define fib_mtu fib_metrics[RTAX_MTU-1] #define fib_window fib_metrics[RTAX_WINDOW-1] #define fib_rtt fib_metrics[RTAX_RTT-1] #define fib_advmss fib_metrics[RTAX_ADVMSS-1]  /* 路由的跃点数,当支持多径路由时,fib_nhs为跃点数目,  当不支持多径路由时,到达目前地址都只有一跳,则该值为1 */  int   fib_nhs;  struct rcu_head  rcu;  /* 下一跳信息 */  struct fib_nh  fib_nh[0]; #define fib_dev  fib_nh[0].nh_dev };

3.3 fib_info的创建

struct fib_config中包含了创建一条路由条目所需要的信息。本文中不考虑路由多径的情况,即假设宏CONFIG_IP_ROUTE_MULTIPATH未定义。

struct fib_info *fib_create_info(struct fib_config *cfg) {  int err;  struct fib_info *fi = NULL;  struct fib_info *ofi;  int nhs = 1;  struct net *net = cfg->fc_nlinfo.nl_net;    /* 检测该请求创建的路由信息范围与类型是否对应 */  if (fib_props[cfg->fc_type].scope > cfg->fc_scope)   goto err_inval;    /* 路由信息除依附于上述所提到的几种数据结构外,同时系统会维护   两个全局的哈希链表,一个用于保存路由信息,另一个用于保存本地地址,   fib_info_cnt全局变量表示路由表项的数目,fib_hash_size表示哈希表   的大小,当路由表项数目大于路由哈希表大小时,为了防止哈希碰撞导致的   查找效率降低,需要扩大哈希表大小为原来的两倍 */  err = -ENOBUFS;  if (fib_info_cnt >= fib_hash_size) {   unsigned int new_size = fib_hash_size << 1;   struct hlist_head *new_info_hash;   struct hlist_head *new_laddrhash;   unsigned int bytes;     if (!new_size)    new_size = 1;   /* 按新的哈希表尺寸为哈希表分配内存空间 */   bytes = new_size * sizeof(struct hlist_head *);   new_info_hash = fib_hash_alloc(bytes);   new_laddrhash = fib_hash_alloc(bytes);   if (!new_info_hash || !new_laddrhash) {    fib_hash_free(new_info_hash, bytes);    fib_hash_free(new_laddrhash, bytes);   } else    /* 如果内存空间分配成功,则将旧的列表中的内容移动到新的链表中,      并释放旧列表的内存空间 */    fib_hash_move(new_info_hash, new_laddrhash, new_size);     /* 列表大小溢出,则出错返回 */   if (!fib_hash_size)    goto failure;  }    /* 为路由表项分配内存空间,大小为struct fib_info的大小加上nhs个下一跳信息  struct fib_nh的大小,不支持多径路由的路由时nhs始终为1 */  fi = kzalloc(sizeof(*fi)+nhs*sizeof(struct fib_nh), GFP_KERNEL);  if (fi == NULL)   goto failure;  fib_info_cnt++;    fi->fib_net = hold_net(net);  fi->fib_protocol = cfg->fc_protocol;  fi->fib_flags = cfg->fc_flags;  fi->fib_priority = cfg->fc_priority;  fi->fib_prefsrc = cfg->fc_prefsrc;    fi->fib_nhs = nhs;  /* 遍历fib_nh信息,此处仅执行一次,设置fib_nh的nh_parent */  change_nexthops(fi) {   nexthop_nh->nh_parent = fi;  } endfor_nexthops(fi)    /* 如果给出了路由属性信信,则通过遍历路由属性信息来确定fib_metrics的值 */  if (cfg->fc_mx) {   struct nlattr *nla;   int remaining;     nla_for_each_attr(nla, cfg->fc_mx, cfg->fc_mx_len, remaining) {    int type = nla_type(nla);      if (type) {     if (type > RTAX_MAX)      goto err_inval;     fi->fib_metrics[type - 1] = nla_get_u32(nla);    }   }  }    struct fib_nh *nh = fi->fib_nh;    nh->nh_oif = cfg->fc_oif;  nh->nh_gw = cfg->fc_gw;  nh->nh_flags = cfg->fc_flags;    if (fib_props[cfg->fc_type].error) {   if (cfg->fc_gw || cfg->fc_oif || cfg->fc_mp)    goto err_inval;   goto link_it;  }    if (cfg->fc_scope > RT_SCOPE_HOST)   goto err_inval;    if (cfg->fc_scope == RT_SCOPE_HOST) {   struct fib_nh *nh = fi->fib_nh;   /* 当前添加的是本地路由信息,只可能有一跳,即便是开启了    多径路由,下一跳数目不为1则报错,同时本地路由也不需要    指定网关,如果指定则报错 */     if (nhs != 1 || nh->nh_gw)    goto err_inval;   nh->nh_scope = RT_SCOPE_NOWHERE;   nh->nh_dev = dev_get_by_index(net, fi->fib_nh->nh_oif);   err = -ENODEV;   if (nh->nh_dev == NULL)    goto failure;  } else {   /* 如果添加的不是本地路由信息,则检查下一跳信息 */   change_nexthops(fi) {    err = fib_check_nh(cfg, fi, nexthop_nh);    if (err != 0)     goto failure;   } endfor_nexthops(fi)  }    if (fi->fib_prefsrc) {   if (cfg->fc_type != RTN_LOCAL || !cfg->fc_dst ||       fi->fib_prefsrc != cfg->fc_dst)    if (inet_addr_type(net, fi->fib_prefsrc) != RTN_LOCAL)     goto err_inval;  }   link_it:  /* 查找路由条目,返回与当前路由条目精确匹配的条目,   若存在,则释放当前创建的新条目,增加已找到的路由条目   的引用计数,并返回已找到的旧路由条目 */  ofi = fib_find_info(fi);  if (ofi) {   fi->fib_dead = 1;   free_fib_info(fi);   ofi->fib_treeref++;   return ofi;  }    /* 当前路由表中未找到已存在的符合要求的路由条目, 则增加   新建路由条目的引用计数 */  fi->fib_treeref++;  atomic_inc(&fi->fib_clntref);  spin_lock_bh(&fib_info_lock);  /* 将新建的路由插入到全局路由列表中,其中fib_info_hashfh   为散列函数 */  hlist_add_head(&fi->fib_hash,          &fib_info_hash[fib_info_hashfn(fi)]);    /* 如果指定了源地址,则将源地址插入到全局本地地址列表中 */  if (fi->fib_prefsrc) {   struct hlist_head *head;   head = &fib_info_laddrhash[fib_laddr_hashfn(fi->fib_prefsrc)];   hlist_add_head(&fi->fib_lhash, head);  }  /* 将下一跳信息写入全局列表中,由上述知本迭代只进行一次,   散列函数为fib_devindex_hashfn() */  change_nexthops(fi) {   struct hlist_head *head;   unsigned int hash;     if (!nexthop_nh->nh_dev)    continue;   hash = fib_devindex_hashfn(nexthop_nh->nh_dev->ifindex);   head = &fib_info_devhash[hash];   hlist_add_head(&nexthop_nh->nh_hash, head);  } endfor_nexthops(fi)  spin_unlock_bh(&fib_info_lock);  return fi;   err_inval:  err = -EINVAL;   failure:  if (fi) {   fi->fib_dead = 1;   free_fib_info(fi);  }    return ERR_PTR(err); }

3.4 向路由表中插入路由信息。

  int fib_table_insert(struct fib_table *tb, struct fib_config *cfg) {  struct fn_hash *table = (struct fn_hash *) tb->tb_data;  struct fib_node *new_f = NULL;  struct fib_node *f;  struct fib_alias *fa, *new_fa;  struct fn_zone *fz;  struct fib_info *fi;  u8 tos = cfg->fc_tos;  __be32 key;  int err;    if (cfg->fc_dst_len > 32)   return -EINVAL;    /* 根据目的地址长度找出对应的路由域 */  fz = table->fn_zones[cfg->fc_dst_len];  /* 如果路由域不存在,则调用fn_new_zone()函数创建一个新的路由域 */  if (!fz && !(fz = fn_new_zone(table, cfg->fc_dst_len)))   return -ENOBUFS;    key = 0;    /* 如果指定了目的地址,如果目的地址主机位不为0,则出错返回 */  if (cfg->fc_dst) {   if (cfg->fc_dst & ~FZ_MASK(fz))    return -EINVAL;   key = fz_key(cfg->fc_dst, fz);  }    /* 创建一个新的fib_info对象 */  fi = fib_create_info(cfg);  if (IS_ERR(fi))   return PTR_ERR(fi);    /* 如果当前路由域中路由节点的数目大于散列表大小的两倍,   并且相关数据都合法的情况下,需要重构散列表以减小   哈希碰撞 */  if (fz->fz_nent > (fz->fz_divisor<<1) &&      fz->fz_divisor < FZ_MAX_DIVISOR &&      (cfg->fc_dst_len == 32 ||       (1 << cfg->fc_dst_len) > fz->fz_divisor))   fn_rehash_zone(fz);    /* 通过网络号key找出对应的路由节点fn_node */  f = fib_find_node(fz, key);    if (!f)   fa = NULL;  else   fa = fib_find_alias(&f->fn_alias, tos, fi->fib_priority);    if (fa && fa->fa_tos == tos &&      fa->fa_info->fib_priority == fi->fib_priority) {   struct fib_alias *fa_first, *fa_match;     err = -EEXIST;   /* 如果具有与新建路由项相同属性的fib_alias存在,并且添加路由项标志中    设置了NLM_F_EXCL(排它选项),则返回路由已存在 */   if (cfg->fc_nlflags & NLM_F_EXCL)    goto out;     /* We have 2 goals:    * 1. Find exact match for type, scope, fib_info to avoid    * duplicate routes    * 2. Find next 'fa' (or head), NLM_F_APPEND inserts before it    */   fa_match = NULL;   fa_first = fa;   fa = list_entry(fa->fa_list.prev, struct fib_alias, fa_list);   list_for_each_entry_continue(fa, &f->fn_alias, fa_list) {    if (fa->fa_tos != tos)     break;    if (fa->fa_info->fib_priority != fi->fib_priority)     break;    if (fa->fa_type == cfg->fc_type &&        fa->fa_scope == cfg->fc_scope &&        fa->fa_info == fi) {     fa_match = fa;     break;    }   }     if (cfg->fc_nlflags & NLM_F_REPLACE) {    u8 state;      /* 如果存在一条精确匹配的路由项fib_alias,并且在设置了NLM_F_REPLACE     标志的情况下,不做处理直接返回 */    fa = fa_first;    if (fa_match) {     if (fa == fa_match)      err = 0;     goto out;    }      /* 并没有精确匹配的路由项fib_alias,即便有匹配的fib_alias,也是     仅tos和priority两个选项匹配,因此需要新建一个路由别名fib_alias */    err = -ENOBUFS;    new_fa = fib_fast_alloc(f);    if (new_fa == NULL)     goto out;      new_fa->fa_tos = fa->fa_tos;    new_fa->fa_info = fi;    new_fa->fa_type = cfg->fc_type;    new_fa->fa_scope = cfg->fc_scope;    state = fa->fa_state;    new_fa->fa_state = state & ~FA_S_ACCESSED;    fib_hash_genid++;    /* 因为设置了NLM_F_REPLACE选项,所以用新fib_alias对象替换掉     列表中旧的fib_alias对象,并释放旧对象的内存 */    list_replace_rcu(&fa->fa_list, &new_fa->fa_list);      fn_free_alias(fa, f);    if (state & FA_S_ACCESSED)     rt_cache_flush(cfg->fc_nlinfo.nl_net, -1);    /* 这里留做以后再讨论 */    rtmsg_fib(RTM_NEWROUTE, key, new_fa, cfg->fc_dst_len,       tb->tb_id, &cfg->fc_nlinfo, NLM_F_REPLACE);    return 0;   }     /* Error if we find a perfect match which    * uses the same scope, type, and nexthop    * information.    */   if (fa_match)    goto out;     if (!(cfg->fc_nlflags & NLM_F_APPEND))    fa = fa_first;  }    /* 对应的fib_alias对象并不存在,如果没有设置NLM_F_CREATE则返回出错 */  err = -ENOENT;  if (!(cfg->fc_nlflags & NLM_F_CREATE))   goto out;    err = -ENOBUFS;    /* 新建一个路由节点项fib_node并初始化 */  if (!f) {   new_f = kmem_cache_zalloc(fn_hash_kmem, GFP_KERNEL);   if (new_f == NULL)    goto out;     INIT_HLIST_NODE(&new_f->fn_hash);   INIT_LIST_HEAD(&new_f->fn_alias);   new_f->fn_key = key;   f = new_f;  }    /* 新建一个路由别名项fib_alias并初始化 */  new_fa = fib_fast_alloc(f);  if (new_fa == NULL)   goto out;    new_fa->fa_info = fi;  new_fa->fa_tos = tos;  new_fa->fa_type = cfg->fc_type;  new_fa->fa_scope = cfg->fc_scope;  new_fa->fa_state = 0;    /* 将路由信息项,路由别名项,路由节点项按规则组织起来   后刷新路由表 */  if (new_f)   fib_insert_node(fz, new_f);  list_add_tail_rcu(&new_fa->fa_list,    (fa ? &fa->fa_list : &f->fn_alias));  fib_hash_genid++;    if (new_f)   fz->fz_nent++;  rt_cache_flush(cfg->fc_nlinfo.nl_net, -1);    rtmsg_fib(RTM_NEWROUTE, key, new_fa, cfg->fc_dst_len, tb->tb_id,     &cfg->fc_nlinfo, 0);  return 0;   out:  if (new_f)   kmem_cache_free(fn_hash_kmem, new_f);  fib_release_info(fi);  return err; }

3.5 查询路由信息

这个这个函数先在RT_TABLE_LOCAL表中查询路由信息,成功则返回,如果失败则再从RT_TABLE_MAIN表中查询路由信息,如果失败则返回网络不可达,成功则返回0。

static inline int fib_lookup(struct net *net, const struct flowi *flp,         struct fib_result *res) {  struct fib_table *table;    table = fib_get_table(net, RT_TABLE_LOCAL);  if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))   return 0;    table = fib_get_table(net, RT_TABLE_MAIN);  if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))   return 0;  return -ENETUNREACH; }

接下来再看fib_table_lookup()函数的定义:

int fib_table_lookup(struct fib_table *tb,        const struct flowi *flp, struct fib_result *res,        int fib_flags) {  int err;  struct fn_zone *fz;  struct fn_hash *t = (struct fn_hash *)tb->tb_data;    rcu_read_lock();  for (fz = rcu_dereference(t->fn_zone_list);       fz != NULL;       fz = rcu_dereference(fz->fz_next)) {   struct hlist_head *head;   struct hlist_node *node;   struct fib_node *f;   __be32 k;   unsigned int seq;     do {    seq = read_seqbegin(&fz->fz_lock);    k = fz_key(flp->fl4_dst, fz);      head = rcu_dereference(fz->fz_hash) + fn_hash(k, fz);    hlist_for_each_entry_rcu(f, node, head, fn_hash) {     if (f->fn_key != k)      continue;       err = fib_semantic_match(&f->fn_alias,        flp, res,        fz->fz_order, fib_flags);     if (err <= 0)      goto out;    }   } while (read_seqretry(&fz->fz_lock, seq));  }  err = 1; out:  rcu_read_unlock();  return err; }

该函数从fn_zone_list开始遍历fn_zone链表,上面创建fn_zone的过程中我们提到过,fn_zone在 fn_zone_list中是按掩码长度从大到小排列的,即该搜索先匹配掩码最大的zone,若不匹配则转而去匹配下一个掩码稍小的路由域,其中 fn_key是匹配的关键,如果key不匹配则该路由项肯定不匹配,key匹配之后还要再调用fib_semantic_match()函数再做进一步的 检查,并在该函数中利用查询到的路由信息给查询结果对象fib_result初始化。当没有一个掩码长度不为0的zone中有路由项与其匹配时,函数最后 一次循环会检测掩码长度为0的zone,该域便为默认路由域,在这里fn_key与k的值均为0,它们始终匹配,也就是说如果检查不到匹配的路由项,则交 由默认路由来处理(不知道这样表达合不合理,大体就是这个意思),接下来再来检验证TOS,SCOPE等信息。接下来看 fib_semantic_match()函数:

  int fib_semantic_match(struct list_head *head, const struct flowi *flp,          struct fib_result *res, int prefixlen, int fib_flags) {  struct fib_alias *fa;  int nh_sel = 0;    list_for_each_entry_rcu(fa, head, fa_list) {   int err;     if (fa->fa_tos &&       fa->fa_tos != flp->fl4_tos)    continue;     if (fa->fa_scope < flp->fl4_scope)    continue;     fib_alias_accessed(fa);     err = fib_props[fa->fa_type].error;   if (err == 0) {    struct fib_info *fi = fa->fa_info;      if (fi->fib_flags & RTNH_F_DEAD)     continue;      switch (fa->fa_type) {    case RTN_UNICAST:    case RTN_LOCAL:    case RTN_BROADCAST:    case RTN_ANYCAST:    case RTN_MULTICAST:     for_nexthops(fi) {      if (nh->nh_flags & RTNH_F_DEAD)       continue;      if (!flp->oif || flp->oif == nh->nh_oif)       break;     }     goto out_fill_res;     endfor_nexthops(fi);     continue;      default:     pr_warning("fib_semantic_match bad type %#x\n",         fa->fa_type);     return -EINVAL;    }   }   return err;  }  return 1;   out_fill_res:  res->prefixlen = prefixlen;  res->nh_sel = nh_sel;  res->type = fa->fa_type;  res->scope = fa->fa_scope;  res->fi = fa->fa_info;  if (!(fib_flags & FIB_LOOKUP_NOREF))   atomic_inc(&res->fi->fib_clntref);  return 0; }

该函数返回1时表示没有匹配的路由项,返回0时表示匹配成功,返回负值时表示管理失败,即fa->fa_type类型有问题,可查询 fib_props数组来确定对应的fa_type是否有错误,如果类型为 RTN_BLACKHOLE,RTN_UNREACHABLE,RTN_PROHIBIT,RTN_THROW,RTN_NAT,RTN_XRESOLVE 时表示存在对应的错误。

函数先检验TOS,如果搜索健值中指定了TOS,则必须对TOS进行严格匹配,否则当前路由项即不符合要求。如果未指定TOS,则可以匹配任意TOS。

接下来检验scope,路由项的范围必须比请求的范围更”窄“才能符合要求,如果请求查找的scope为RTN_UNIVERSE,则RTN_LINK即不匹配;类似的,如果请求查找的是RTN_HOST,则RTN_LINK,RTN_UNIVERSE都是可以匹配的。

scope检查完成之后即开始检查路由类型,即刚才提到的fa->fa_type,不再说了。如果类型没有问题则接下来检查下一跳信息,首先 得确保下一跳信息不是待删除的,其次如果指定了出口设备,则出口设备要和下一跳信息中的出口设备匹配。如果到这一步全都匹配,则函数跳到 out_fill_res,填充fib_result,之后返回0,表示成功。

转载于: http://wdqfirst.blog.163.com/blog/static/11334741120117585840648/



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值