路由表
在内核中存在路由表fib_table_hash和路由缓存表rt_hash_table。路由缓存表主要是为了加速路由的查找,每次路由查询都会先查找路由缓存,再查找路由表。这和cache是一个道理,缓存存储最近使用过的路由项,容量小,查找快速;路由表存储所有路由项,容量大,查找慢。
首先,应该先了解路由表的意义,下面是route命令查看到的路由表:
Destination | Netmask | Gateway | Flags | Interface | Metric |
169.254.0.0 | 255.255.0.0 | * | U | eth0 | 1 |
192.168.123.0 | 255.255.255.0 | * | U | eth0 | 1 |
default | 0.0.0.0 | 192.168.123.254 | UG | eth0 | 1 |
一条路由其实就是告知主机要到达一个目的地址,下一跳应该走哪里。比如发往192.168.22.3报文通过查路由表,会得到下一跳为192.168.123.254,再将其发送出去。在路由表项中,还有一个很重要的属性-scope,它代表了到目的网络的距离。
路由scope可取值:RT_SCOPE_UNIVERSE, RT_SCOPE_LINK, RT_SCOPE_HOST
在报文的转发过程中,显然是每次转发都要使到达目的网络的距离要越来越小或不变,否则根本到达不了目的网络。上面提到的scope很好的实现这个功能,在查找路由表中,表项的scope一定是更小或相等的scope(比如RT_SCOPE_LINK,则表项scope只能为RT_SCOPE_LINK或RT_SCOPE_HOST)。
路由缓存
路由缓存用于加速路由的查找,当收到报文或发送报文时,首先会查询路由缓存,在内核中被组织成hash表,就是rt_hash_table。
static struct rt_hash_bucket *rt_hash_table __read_mostly; [net/ipv4/route.c]
通过ip_route_input()进行查询,首先是缓存操作时,通过[src_ip, dst_ip, iif,rt_genid]计算出hash值
hash = rt_hash(daddr, saddr, iif, rt_genid(net));
此时rt_hash_table[hash].chain就是要操作的缓存表项的链表,比如遍历该链表
for (rth = rt_hash_table[hash].chain; rth; rth = rth->u.dst.rt_next)
因此,在缓存中查找一个表项,首先计算出hash值,取出这组表项,然后遍历链表,找出指定的表项,这里需要完全匹配[src_ip, dst_ip, iif, tos, mark, net],实际上struct rtable中有专门的属性用于缓存的查找键值 – struct flowi。
/* Cache lookup keys */
struct flowi fl;
当找到表项后会更新表项的最后访问时间,并取出dst
dst_use(&rth->u.dst, jiffies);
skb_dst_set(skb, &rth->u.dst);
路由缓存的创建
inet_init() -> ip_init() -> ip_rt_init()
rt_hash_table = (struct rt_hash_bucket *)
alloc_large_system_hash("IP route cache",
sizeof(struct rt_hash_bucket),
rhash_entries,
(totalram_pages >= 128 * 1024) ?
15 : 17,
0,
&rt_hash_log,
&rt_hash_mask,
rhash_entries ? 0 : 512 * 1024);
其中rt_hash_mask表示表的大小,rt_hash_log = log(rt_hash_mask),创建后的结构如图所示:
路由缓存插入条目
函数rt_intern_hash()
要插入的条目是rt,相应散列值是hash,首先通过hash值找到对应的bucket
rthp = &rt_hash_table[hash].chain;
然后对bucket进行一遍查询,这次查询的目的有两个:如果是超时的条目,则直接删除;如果是与rt相同键值的条目,则删除并将rt插入头部返回。
while ((rth = *rthp) != NULL) {
if (rt_is_expired(rth)) { // 超时的条目
*rthp = rth->u.dst.rt_next;
rt_free(rth);
continue;
}
if (compare_keys(&rth->fl, &rt->fl) && compare_netns(rth, rt)) { //重复的条目
*rthp = rth->u.dst.rt_next;
rcu_assign_pointer(rth->u.dst.rt_next, rt_hash_table[hash].chain);
rcu_assign_pointer(rt_hash_table[hash].chain, rth);
……
}
……
rthp = &rth->u.dst.rt_next;
}
在扫描一遍后,如rt还未存在,则将其插入头部
rt->u.dst.rt_next = rt_hash_table[hash].chain;
rcu_assign_pointer(rt_hash_table[hash].chain, rt);
如果新插入rt满足一定条件,还要与ARP邻居表进行绑定
Hint:缓存的每个bucket是没有头结点的,单向链表,它所使用的插入和删除操作是值得学习的,简单实用。
路由缓存删除条目
rt_del()
要删除的条目是rt,相应散列值是hash,首先通过hash值找到对应的bucket,然后遍历,如果条目超时,或找到rt,则删除它。
rthp = &rt_hash_table[hash].chain;
spin_lock_bh(rt_hash_lock_addr(hash));
ip_rt_put(rt);
while ((aux = *rthp) != NULL) {
if (aux == rt || rt_is_expired(aux)) {
*rthp = aux->u.dst.rt_next;
rt_free(aux);
continue;
}
rthp = &aux->u.dst.rt_next;
}
spin_unlock_bh(rt_hash_lock_addr(hash));
路由表的创建
inet_init() -> ip_init() -> ip_fib_init() -> fib_net_init() -> ip_fib_net_init()[net/ipv4/fib_frontend.c]
首先为路由表分配空间,这里的每个表项hlist_head实际都会链接一个单独的路由表,FIB_TABLE_HASHSZ表示了分配多少个路由表,一般情况下至少有两个 –LOCAL和MAIN。注意这里仅仅是表头的空间分配,还没有真正分配路由表空间。
net->ipv4.fib_table_hash = kzalloc(
sizeof(struct hlist_head)*FIB_TABLE_HASHSZ, GFP_KERNEL);
ip_fib_net_init() -> fib4_rules_init(),这里真正分配了路由表空间
local_table = fib_hash_table(RT_TABLE_LOCAL);
main_table = fib_hash_table(RT_TABLE_MAIN);
然后将local和main表链入之前的fib_table_hash中
hlist_add_head_rcu(&local_table->tb_hlist,
&net->ipv4.fib_table_hash[TABLE_LOCAL_INDEX]);
hlist_add_head_rcu(&main_table->tb_hlist,
&net->ipv4.fib_table_hash[TABLE_MAIN_INDEX]);
最终生成结构如图,LOCAL表位于fib_table_hash[0],MAIN表位于fib_table_hash[1];两张表通过结构tb_hlist链入链表,而tb_id则标识了功能,255是LOCAL表,254是MAIN表。
关于这里的struct fn_hash,它表示了不同子网掩码长度的hash表[即fn_zone],对于ipv4,从0~32共33个。而fn_hash的实现则是fib_table的最后一个参数unsigned char tb_data[0]。
注意到这里fn_zone还只是空指针,我们还只完成了路由表初始化的一部分。在启动阶段还会调用inet_rtm_newroute() -> fib_table_insert() -> fn_new_zone() [fib_hash.c]来创建fn_zone结构,前面已经讲过,fn_zone一共有33个,其中掩码长度为0[/0]表示为默认路由,fn_zone可以理解为相同掩码的地址集合。
首先为fn_zone分配空间
struct fn_zone *fz = kzalloc(sizeof(struct fn_zone), GFP_KERNEL);
传入参数z代表掩码长度, z = 0的掩码用于默认路由,一般只有一个,所以fz_divisor只需设为1;其它设为16;这里要提到fz_divisor的作用,fz->fz_hash并不是个单链表,而是一个哈希表,而哈希表的大小就是fz_divisor。
if (z) {
fz->fz_divisor = 16;
} else {
fz->fz_divisor = 1;
}
fz_hashmask实际是用于求余数的,当算出hash值,再hash & fz_hashmask就得出了在哈希表的位置;而fz_hash就是下一层的哈希表了,前面已经提过路由表被多组分层了,这里fz_hash就是根据fz_divisor大小来创建的;fz_order就是子网掩码长度;fz_mask就是子网掩码。
fz->fz_hashmask = (fz->fz_divisor - 1);
fz->fz_hash = fz_hash_alloc(fz->fz_divisor);
fz->fz_order = z;
fz->fz_mask = inet_make_mask(z);
从子网长度大于新添加fz的fn_zone中挑选一个不为空的fn_zones[i],将新创建的fz设成fn_zones[i].next;然后将fz根据掩码长度添加到fn_zones[]中相应位置;fn_zone_list始终指向掩码长度最长的fn_zone。
for (i=z+1; i<=32; i++)
if (table->fn_zones[i])
break;
if (i>32) {
fz->fz_next = table->fn_zone_list;
table->fn_zone_list = fz;
} else {
fz->fz_next = table->fn_zones[i]->fz_next;
table->fn_zones[i]->fz_next = fz;
}
table->fn_zones[z] = fz;
这里的fn_hash是数组与链表的结合体,看下fn_hash定义
struct fn_hash {
struct fn_zone *fn_zones[33];
struct fn_zone *fn_zone_list;
};
fn_hash包含33数组元素,每个元素存放一定掩码长度的fn_zone,其中fn_zone[i]存储掩码长度为i。而fn_zone通过内部属性fz_next又彼此串连起来,形成单向链表,其中fn_zone_list可以看作链表头,而这里链表的组织顺序是倒序的,即从掩码长到短。
到这里,fz_hash所分配的哈希表还没有插入内容,这部分为fib_insert_node()完成。
inet_rtm_newroute() -> fib_table_insert() -> fib_insert_node() [net/ipv4/fib_hash.c]
这里f是fib_node,可以理解为具有相同网络地址的路由项集合。根据fn_key(网络地址)和fz(掩码长度)来计算hash值,决定将f插入fz_hash的哪个项。
struct hlist_head *head = &fz->fz_hash[fn_hash(f->fn_key, fz)];
hlist_add_head(&f->fn_hash, head);
}
如何fib_node还不存在,则会创建它,这里的kmem_cache_zalloc()其实就是内存分配
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_info,具体的路由信息都存储在此,它由fib_create_info()创建。
首先为fib_info分配空间,由于fib_info的最后一个属性是struct fib_nh fib_nh[0],因此大小是fib_info + nhs * fib_nh,这里的fib_nh代表了下一跳(next hop)的信息,nhs代表了下一跳的数目,一般情况下nhs=1,除非配置了支持多路径。
fi = kzalloc(sizeof(*fi)+nhs*sizeof(struct fib_nh), GFP_KERNEL);
设置fi的相关属性
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;
使fi后面所有的nh->nh_parent指向fi,设置后如图所示
change_nexthops(fi) {
nexthop_nh->nh_parent = fi;
} endfor_nexthops(fi)
设置fib_nh的属性,这里仅展示了单一路径的情况:
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;
然后,再根据cfg->fc_scope值来设置nh的其余属性。如果scope是RT_SCOPE_HOST,则设置下一跳scope为RT_SCOPE_NOWHERE
if (cfg->fc_scope == RT_SCOPE_HOST) {
struct fib_nh *nh = fi->fib_nh;
nh->nh_scope = RT_SCOPE_NOWHERE;
nh->nh_dev = dev_get_by_index(net, fi->fib_nh->nh_oif);
}
如果scope是RT_SCOPE_LINK或RT_SCOPE_UNIVERSE,则设置下跳
change_nexthops(fi) {
if ((err = fib_check_nh(cfg, fi, nexthop_nh)) != 0)
goto failure;
} endfor_nexthops(fi)
最后,将fi链入链表中,这里要注意的是所有的fib_info(只要创建了的)都会加入fib_info_hash中,如果路由项使用了优先地址属性,还会加入fib_info_laddrhash中。
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_info在路由表中位于哪个掩码、哪个网段结构下,都与fib_info_hash和fib_info_laddrhash无关,这两个哈希表与路由表独立,主要是用于加速路由信息fib_info的查找。哈希表的大小为fib_hash_size,当超过这个限制时,fib_hash_size * 2(如果哈希函数够好,每个bucket都有一个fib_info)。fib_info在哈希表的图示如下:
由于路由表信息也可能要以设备dev为键值搜索,因此还存在fib_info_devhash哈希表,用于存储nh的设置dev->ifindex。
change_nexthops(fi) {
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)
上面讲过了路由表各个部分的创建,现在来看下它们是如何一起工作的,在fib_table_insert()[net/ipv4/fib_hash.c]完成整个的路由表创建过程。下面来看下fib_table_insert()函数:
从fn_zones中取出掩码长度为fc_dst_len的项,如果该项不存在,则创建它[fn_zone的创建前面已经讲过]。
fz = table->fn_zones[cfg->fc_dst_len];
if (!fz && !(fz = fn_new_zone(table, cfg->fc_dst_len)))
return -ENOBUFS;
然后创建fib_info结构,[前面已经讲过]
fi = fib_create_info(cfg);
然后在掩码长度相同项里查找指定网络地址key(如145.222.33.0/24),查找的结果如图所示
f = fib_find_node(fz, key);
如果不存在该网络地址项,则创建相应的fib_node,并加入到链表fz_hash中
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_insert_node(fz, new_f);
如果存在该网络地址项,则在fib_node的属性fn_alias中以tos和fi->fib_priority作为键值查找。一个fib_node可以有多个fib_alias相对应,这些fib_alias以链表形式存在,并按tos并从大到小的顺序排列。因此,fib_find_alias查找到的是第一个fib_alias->tos不大于tos的fib_alias项。
fa = fib_find_alias(&f->fn_alias, tos, fi->fib_priority);
如果查找到的fa与与要插入的路由项完全相同,则按照设置的标置位进行操作,NLM_F_REPLACE则替换掉旧的,NLM_F_APPEND添加在后面。
设置要插入的fib_alias的属性,包括最重要的fib_alias->fa_info设置为fi
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;
如果没有要插入路由的网络地址项fib_node,则之前已经创建了新的,现在将它插入到路由表中fib_insert_node();然后将new_fa链入到fib_node->fn_alias中
if (new_f)
fib_insert_node(fz, new_f);
list_add_tail(&new_fa->fa_list,
(fa ? &fa->fa_list : &f->fn_alias));
最后,由于新插入的路由表项,会发出通告,告知所以加入RTNLGRP_IPV4_ROUTE组的成员,这个功能可以在linux中使用”ip route monitor”来测试。最终的路由表如图所示:
rtmsg_fib(RTM_NEWROUTE, key, new_fa, cfg->fc_dst_len, tb->tb_id, &cfg->fc_nlinfo, 0);
至此,就完成了路由表项的插入,加上之前的路由表的初始化,整个路由表的创建过程就讲解完了,小小总结一下:
路由表的查找效率是第一位的,因此内核在实现时使用了多级索引来进行加速
第一级:fn_zone 按不同掩码长度分类(如/5和/24)
第二级:fib_node 按不同网络地址分类(如124.44.33.0/24)
第三级:fib_info 下一跳路由信息
路由子系统的核心是转发信息库(Forwarding Information Base,FIB),即路由表。路由表是用来存储这样一些信息的:一是用以确定输入数据是应该上传给本机的上层协议还是继续转发的信息;二是如果需要转发,为转发数据报提供所需要信息;三是输出数据报应该从哪个具体的网络设备输出的信息
路由表项的维护以及查找涉及以下文件:
include/net/ip_fib.h 定义路由表等结构、宏和函数原型
net/ipv4/fib_lookup.h 定义路由查找的相关函数原型
net/ipv4/fib_hash.c 实现路由表的查找和维护
net/ipv4/fib_frontend.c 实现操作路由表的接口函数和通知
net/ipv4/route.c 实现路由缓存项的操作函数
路由要素:
(1)路由表
路由表是一个由路由表项组成的数据库,并为诸如IPv4等其他子系统提供了多种接口,其中最重要的接口就是路由查找
(2)作用范围
IP地址和路由都有作用范围,用于说明它们在哪些情况下是有意义并可以被使用的。IP地址的作用范围表示该IP地址距离本地主机由多远,而路由的作用范围表示到目的网络的距离。
IP地址的作用范围 描述
Host 当一个地址只用于主机自身内部通信时,作用范围为Host,该地址在主机以外不可知 并且不能被使用。例如环回地址127.0.0.1
Link 当一个地址只在一个局域网(即每台计算机通过链路层互联的一个网络)内有意义且只 在局域网内使用时,该地址的作用范围为Link。例如子网的广播地址。子网内一台主 机发送到子网广播地址的数据报被送给同一子网内的其他主机
Universe 当一个地址可以在任何地方使用时作用范围为universe,这是大多数地址的默认 scope
路由的作用范围 描述
Host 当一条路由使目的地址为本地主机时,作用范围为host
Link 当一条路由使目的地址为本地网络是,作用范围为Link
Universe 当一条路由使目的地址超过一跳时,作用范围为universe
(3)默认网关
默认网关通常是指0.0.0.0/0路由,当到一个目的地址不存在明确的路由项时使用该路由。
(4)特殊路由
当主机收到一个数据报后,路由子系统需要决定将它上传给本地上层协议还是继续转发出去。因此在路由子系统中,有两张特殊的路由表:
一张表用于本地地址,存储了所有的本地地址,如果在该表中能查到匹配表项,则表明数据报是发给本机的
一张表用于所有其他的路由,路由表项由用户手工静态配置或由路由协议动态配置
在路由查找时先扫描本地路由表,只有当查找该表未果的情况下,才会去查找另一路由表,以确定是否可以转发。
路由缓存
一个路由表中的路由项数量,在一般的主机中可能只是几条而已,而在路由器中,这个数目可以达到数十万条。因此很显然,在这种情况下维护一张更小的表来缓存路由查找结果是非常有必要的
路由缓存分为两部分:一部分是与协议(如IPv4等三层协议)相关的缓存,这就是缓存框架部分,每个元素被定义为一个由具体协议字段组成的集合;另一部分是与协议无关的缓存,通常被称为DST,嵌套在缓存框架中,只存储与协议无关的信息
路由表和路由缓存除了容量和结构不同之外,对象粒度也不同。路由表使用连续地址的集合,即子网,而缓存项与单个IP地址相关联。因此,路由表和路由缓存使用的查找算法也不同。
路由表结构
fib_table结构
对每个路由表实例创建一个fib_table结构,这个结构主要由一个路由表标识和管理该路由表的一组函数指针组成
- struct fib_table {
- struct hlist_node tb_hlist;
- u32 tb_id;
- int tb_default;
- int (*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res);
- int (*tb_insert)(struct fib_table *, struct fib_config *);
- int (*tb_delete)(struct fib_table *, struct fib_config *);
- int (*tb_dump)(struct fib_table *table, struct sk_buff *skb,
- struct netlink_callback *cb);
- int (*tb_flush)(struct fib_table *table);
- void (*tb_select_default)(struct fib_table *table,
- const struct flowi *flp, struct fib_result *res);
- unsigned char tb_data[0];
- };
用来将各个路由表链接成一个双向链表
u32 tb_id;
路由表标识。在支持策略路由的情况下,系统中最多可以有256个路由表,枚举类型rt_class_t定义了保留的路由路由表ID
unsigned char tb_data[0];
路由表项的散列表起始地址。在FIB_HASH算法中指向fn_hash结构,而在FIB_TRIE算法中则指向trie结构
fn_zone结构
一个zone是一组有着相同目的地址掩码长度的路由表项的散列表
- 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 */
- __be32 fz_mask;
- #define FZ_MASK(fz) ((fz)->fz_mask)
- };
将活动的(路由表项不为空)zone链接在一起的指针,该链表的头部存储在fn_hash数据结构的fn_zone_list字段中。
struct hlist_head *fz_hash;
指向存储该zone中路由项的散列表。
int fz_nent;
在该zone的散列表中fib_node实例的数目,用于检查是否需要改变散列表的容量。
int fz_divisor;
表示散列表fz_hash的容量,以及散列表桶的数目。
u32 fz_hashmask;
其值为fz_divisor - 1,用来计算散列表的关键值
int fz_order;
网络掩码的长度,255.255.255.0的网络掩码长度为24
__be32fz_mask;
网络掩码
fib_node结构
fib_node实例代表每一个唯一的目的网络的路由表项,即同一个子网中所有路由表项所共享的信息。目的网络相同但其他配置参数不同的路由表项共享同一个fib_node实例,因此一个fib_node实例上存在着一个或多个路由表项。
- struct fib_node {
- struct hlist_node fn_hash;
- struct list_head fn_alias;
- __be32 fn_key;
- struct fib_alias fn_embedded_alias;
- };
struct list_head fn_alias;
fn_alias指向一个或多个fib_alias结构实例构成的链表
__be32fn_key;
由IP地址和路由项的netmask与操作后得到,被用作查找路由表时的搜索条件
fib_alias结构
fib_alias实例代表一条路由表项,目的地址相同但其他配置参数不同的表项共享fib_node实例
- struct fib_alias {
- struct list_head fa_list;
- struct fib_info *fa_info;
- u8 fa_tos;
- u8 fa_type;
- u8 fa_scope;
- u8 fa_state;
- #ifdef CONFIG_IP_FIB_TRIE
- struct rcu_head rcu;
- #endif
- };
将共享同一个fib_node实例的所有fib_alias实例链接在一起
struct fib_info*fa_info;
指针指向一个fib_info实例,该实例存储着如何处理与该路由相匹配数据报的信息
u8 fa_tos;
路由的服务类型比特位字段
u8 fa_type;
路由表项的类型,如RTN_UNICAST、RTN_LOCAL等
u8 fa_scope;
路由表项的作用范围
u8 fa_state;
一些标志的位图
fib_info结构
fib_node结构和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]
- int fib_nhs;
- #ifdef CONFIG_IP_ROUTE_MULTIPATH
- int fib_power;
- #endif
- struct fib_nh fib_nh[0];
- #define fib_dev fib_nh[0].nh_dev
- };