转载:http://blog.163.com/s_xli1/blog/static/783579622008416091770/
内核版本:2.6.12
1、什么是路由缓存
当数据包进入网络层后,需要做的第一件事情,就是进行路由查找,也即根据数据包的目标地址,查找需要转发的目的网关,当然,实际情况比这个要复杂,查找条 件并不仅限于目的地址。
为了提高路由查找的效率,路由子系统引入了路由缓存的概念,用于某个目的地址在路由表中查找到匹配路由项后,就把目的地址对应的路由项缓存起来,可以通过 route –C看到路由缓存表:
这样,下一次路由查找时,就先查缓存,没有在缓存中,再去搜索路由表。
不论进入或者是发出的数据,都要在网络层进行路由查找,查找首先在路由缓存中进行,如果命中,则查找完成,如果没有命中,则进入路由表中的查找,如果在表 中查找命中,则创建相应的路由缓存项。
2、路由缓存表的组织
如前所述,整个缓存表由一条条地缓存项组成,整个表采用了hash表来组织,如下图。
从这张图中,我们可以看到许多重要的信息:
- hash桶的大小由变量rt_hash_mask决定,它是决定路由缓存大小的重要因素;
- 每一个hash桶的项,是由结构struct rt_hash_bucket;
- 每一个路由缓存项,是由结构struct rtable结构描述的,它的rt_next/next成员来组织链表的;为什么是两个成员指针而不仅仅是一个next,这在后面来分析;
- struct rtable缓存项中,还包含了一个struct dst_entry结构;
哈希桶的类型是rt_hash_bucket,该结构只包含指向缓存元素链表的一个指针和一个锁:
struct rtable *chain;
spinlock_t lock;
} __attribute__((__aligned__(8)));
chain成员指针自然指向下一个路由缓存项, 用于链表的维护,另外,它还包含了一个自旋锁。关于锁的使用,这在后面代码分析中,可以看到。
Chain是一个struct rtable类型的指针,struct rtable用于描述一条完整的路由缓存项,它是路由子系统中,最重要的数据结构之一:
{
/*
* 第一个成员u,被定义为一个联合体,这非常的重要,因为struct rtable被定义为每一项路由缓存,所以不可避免地在hash表中,存在冲撞的情况,rt_next指针就用来组织冲撞的链表。而另一方面, struct dst_entry结构的第一个成员struct dst_entry *next;也指向了下一个冲撞节点struct rtable中的struct dst_entry,虽然这两个指针rt_next和dst.next的类型不同,但是它们却指向了同一内存位置(因为dst是struct rtable的第一个成员),这样,巧妙的设计,使得其很容易其享一些数据。一个指向rtable结构的指针可以安全地映射(typecast)为一个指 向dst_entry的指针,反之亦然。前面分析哈希表时,说同时用到rt_next和next两个成员指针来维护链表,就是这个道理。
*/
union
{
struct dst_entry dst;
struct rtable *rt_next;
} u;
/*该指针指向egress设备的IP配置块。注意对送往本地的ingress报文的路由,设置的egress设备为loopback设备。*/
struct in_device *idev;
unsigned rt_flags; /*路由标志*/
__u16 rt_type; /*路由类型*/
__u16 rt_multipath_alg; /*多路径缓存算法。该字段是根据相关路由项上配置的算法来初始化*/
__u32 rt_dst; /* 目的IP */
__u32 rt_src; /* 源IP */
/*
Ingress设备标识。这个值是从ingress设备的net_device数据结构中得到。对本地生
成的流量(因此不是从任何接口上接收到的),该字段被设置为出设备的ifindex
字段。不要将该字段与本章后面描述的flowi数据结构fl中的iif字段混淆,对本地生
成的流量,fl中的iif字段被设置为零(loopback_dev)。
*/
int rt_iif;
/*
* 当目的主机为直连时(即在同一链路上),rt_gateway表示目的地址。当需要通过一个网关到达目的地时,
* rt_gateway被设置为由路由项中的下一跳网关。
*/
__u32 rt_gateway;
/* 用于缓存查找的搜索key */
struct flowi fl;
/* Miscellaneous cached information */
__u32 rt_spec_dst; /* RFC1122 specific destination */
/*
inet_peer结构存储有关IP peer的long-living信息,IP peer是该缓存
路由项的目的IP地址对应的主机。与本地主机在最近一段时间通信的每个远端IP
地址都有一个inet_peer结构。
*/
struct inet_peer *peer; /* long-living peer info */
};
第 一个成员dst是struct dst_entry类型,它用于存储缓存路由项中独立于协议的信息,因为协议栈中除了IPV4,还有其它网络层协议,如IPV6,还要使用路由子系统。一 般是在网络层协议的缓存结构中,包含此结构,以用于存储私有信息,这里我们看到的在struct rtable中包含struct dst_entry就是一个典型的例子:
{
/*用于将分布在同一个哈希桶内的dst_entry实例链接在一起*/
struct dst_entry *next;
/*引用计数*/
atomic_t __refcnt; /* client references */
int __use; /*该表项已经被使用的次数(即缓存查找返回该表项的次数)*/
struct dst_entry *child;
struct net_device *dev; /*Egress设备(即将报文送达目的地的发送设备)*/
/*
当fib_lookup API(只被IPv4使用)失败时,错误值被保存在error(用一个正值)
中,在后面的ip_error中使用该值来决定如何处理本次路由查找失败(即决定生成
哪一类ICMP消息)。
*/
short error;
/*
* 用于定义该dst_entry实例的可用状态:0(缺省值)表示该结构有效而且可以被使
用,2表示该结构将被删除因而不能被使用,-1被IPsec和IPv6使用但不被IPv4使
用。
*/
short obsolete;
/*
* 标志集合(Set of flags)。DST_HOST被TCP使用,表示主机路由(即它不是到网
络或到一个广播/多播地址的路由)。DST_NOXFRM,DST_NOPOLICY和
DST_NOHASH只用于IPsec。
*/
int flags;
#define DST_HOST 1
#define DST_NOXFRM 2
#define DST_NOPOLICY 4
#define DST_NOHASH 8
#define DST_BALANCED 0x10
/*
用于记录该表项上次被使用的时间戳。当缓存查找成功时更新该时间戳,垃圾回
收程序使用该时间戳来选择最合适的应当被释放的结构。
*/
unsigned long lastuse;
unsigned long expires; /*表示该表项将过期的时间戳。*/
unsigned short header_len; /* more space at head required */
unsigned short trailer_len; /* space to reserve at tail */
u32 metrics[RTAX_MAX];
struct dst_entry *path;
unsigned long rate_last; /* 这两个字段被用于对两种类型的ICMP消息限速。 */
unsigned long rate_tokens;
struct neighbour *neighbour; /*neighbour是包含下一跳三层地址到二层地址映射的结构,hh是缓存的二层头。*/
struct hh_cache *hh;
struct xfrm_state *xfrm;
/*分别表示处理ingress报文和处理egress报文的函数。参见第33章“缓存查找”小节。*/
int (*input)(struct sk_buff*);
int (*output)(struct sk_buff*);
#ifdef CONFIG_NET_CLS_ROUTE
__u32 tclassid; /*基于路由表的classifier的标签。*/
#endif
struct dst_ops *ops; /*该结构内的虚函数表(VFT)用于处理dst_entry结构。*/
struct rcu_head rcu_head;
char info[0];
};
当然,结构是复杂的,可以暂时跳过其成员的含义,大约知道这个结构是拿来做什么的就可以了,我们在代码分析中,再回 过头来分析其成员变量的含义。
明白了缓存hash表,现在来看这个表是如何被组织起来的:
net/ipv4/route.c
int __init ip_rt_init(void)
{
……
ipv4_dst_ops.kmem_cachep = kmem_cache_create("ip_dst_cache",
sizeof(struct rtable),
0, SLAB_HWCACHE_ALIGN,
NULL, NULL);
if (!ipv4_dst_ops.kmem_cachep)
panic("IP: failed to allocate ip_dst_cache/n");
//计算出最大可需要的内存空间
goal = num_physpages >> (26 - PAGE_SHIFT);
if (rhash_entries)
goal = (rhash_entries * sizeof(struct rt_hash_bucket)) >> PAGE_SHIFT;
//该循环计算出此内存空间,需要的最大的order数
for (order = 0; (1UL << order) < goal; order++)
/* NOTHING */;
do {
//1UL << order,计算出分配的页面,再乘上页面大小,除以桶大小,计算出共可以有多少个hash桶
rt_hash_mask = (1UL << order) * PAGE_SIZE /
sizeof(struct rt_hash_bucket);
while (rt_hash_mask & (rt_hash_mask - 1))
rt_hash_mask--;
//分配hash表空间,它共有rt_hash_mask个桶
rt_hash_table = (struct rt_hash_bucket *)
__get_free_pages(GFP_ATOMIC, order);
} while (rt_hash_table == NULL && --order > 0);
//上面这个while循环在分配失败后,一直尝试递减order,再尝试分配,直至分配分功或者order为0
if (!rt_hash_table)
panic("Failed to allocate IP route cache hash table/n");
……
}
初始化工作中,主要完成内存的计算,hash桶的空间的分配工作,这样,所有链表的链表首部就被建立起来了。整个hash表的框架就被建立起来了。当一个 缓存项需要被加入至这个表中,就根据相应的hash算法计算出hash值,然后使用rt_hash_table[hash]定位到链表的入口,利用前文所 述的struct rt_hash_bucket结构的chain成员组织链表,将其加入即可。因为我这里主要分析数据流的转发,重点是查找工作,缓存表的插入/删除/垃圾 回收等工作,就不在这里一一详细分析了。
3、缓存的查找
当数据包进入网络层后,第一个被调用的函数是ip_rcv函数:
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt)
{
struct iphdr *iph;
/* 混杂模式下,数据将被丢弃 */
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;
/*更新SNMP统计修筑*/
IP_INC_STATS_BH(IPSTATS_MIB_INRECEIVES);
/*skb_share_check用于skb的共享检查,如果有别人已经在使用了,则克隆一份给自己使用*/
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto out;
}
/*一个正确的IP包,包长度应该大于或等于包首部长度*/
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;
/*取得IP首部*/
iph = skb->nh.iph;
/*
* RFC1122: 3.1.2.2 MUST silently discard any IP frame that fails the checksum.
*
* Is the datagram acceptable?
*
* 1. Length at least the size of an ip header
* 2. Version of 4
* 3. Checksums correctly. [Speed optimisation for later, skip loopback checksums]
* 4. Doesn't have a bogus length
*/
/*长度和版本检查*/
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;
if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;
/*因为如果运行不好,上边pskb_may_pull函数会进一步去调用__pskb_pull_tail函数,去以完成补全数据包的页外数据的工作, 把碎片部分
的数据线性重组,所以,有必要重置iph指针,以指向正确的ip 首部*/
iph = skb->nh.iph;
/*校验和检查*/
if (ip_fast_csum((u8 *)iph, iph->ihl) != 0)
goto inhdr_error;
{
__u32 len = ntohs(iph->tot_len);
if (skb->len < len || len < (iph->ihl<<2))
goto inhdr_error;
/* Our transport medium may have padded the buffer out. Now we know it
* is IP we can trim to the true length of the frame.
* Note this now means skb->len holds ntohs(iph->tot_len).
*/
if (pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto drop;
}
}
/*进入Netfilter钩子,处理完后,继续执行ip_rcv_finish */
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
inhdr_error:
IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
drop:
kfree_skb(skb);
out:
return NET_RX_DROP;
}
这一部份代码,简而言之,就是取得IP首部,进行合法性检查,然后调用ip_rcv_finish函数,关于 Netfilter的更多内容,请参考九贱的《Linux防火墙设计与Nefilter源码分析》。
ip_rcv_finish 要做的第一件事情,就是调用ip_route_input函数进行缓存查找:
{
struct net_device *dev = skb->dev;
struct iphdr *iph = skb->nh.iph;
/*
* Initialise the virtual path cache for the packet. It describes
* how the packet travels inside Linux networking.
*/
if (skb->dst == NULL) {
if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
goto drop;
}
……
这就进入我们本章的主题了,接下来看看ip_route_input是如何进行缓存查找的。
u32 daddr, u32 saddr, //目的地址和源地址
u8 tos, //TOS
struct net_device *dev) //输入设备
{
struct rtable * rth;
unsigned hash;
int iif = dev->ifindex;
tos &= IPTOS_RT_MASK;
hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos);
rcu_read_lock();
for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
rth = rcu_dereference(rth->u.rt_next)) {
if (rth->fl.fl4_dst == daddr &&
rth->fl.fl4_src == saddr &&
rth->fl.iif == iif &&
rth->fl.oif == 0 &&
#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark == skb->nfmark &&
#endif
rth->fl.fl4_tos == tos) {
rth->u.dst.lastuse = jiffies;
dst_hold(&rth->u.dst);
rth->u.dst.__use++;
RT_CACHE_STAT_INC(in_hit);
rcu_read_unlock();
skb->dst = (struct dst_entry*)rth;
return 0;
}
RT_CACHE_STAT_INC(in_hlist_search);
}
rcu_read_unlock();
if (MULTICAST(daddr)) {
struct in_device *in_dev;
rcu_read_lock();
if ((in_dev = __in_dev_get(dev)) != NULL) {
int our = ip_check_mc(in_dev, daddr, saddr,
skb->nh.iph->protocol);
if (our
#ifdef CONFIG_IP_MROUTE
|| (!LOCAL_MCAST(daddr) && IN_DEV_MFORWARD(in_dev))
#endif
) {
rcu_read_unlock();
return ip_route_input_mc(skb, daddr, saddr,
tos, dev, our);
}
}
rcu_read_unlock();
return -EINVAL;
}
return ip_route_input_slow(skb, daddr, saddr, tos, dev);
}
函数的第一个工作,就是根据目的地址、源地址、接口索引和TOS值计算hash值
这里用到了 rcu锁,关于这个锁的更多内容,可以参考其它相关资料。宏rcu_dereference在RCU读临界部份中取出一个RCU保护的指针。在需要内存屏 障的体系中进行内存屏障:
typeof(p) _________p1 = p; /
smp_read_barrier_depends(); /
(_________p1); /
})
于是,我们有了hash值后,就可以在hash 桶中直接找到链表入口:
rth = rcu_dereference(rt_hash_table[hash].chain);
如果要遍历该链 表中的所有路由缓存项,就可以使用如下循环:
rth = rcu_dereference(rth->u.rt_next)) {
……
}
遍历每一个缓存项就简单,重要的是如何将缓存中的路由特征同数据包的特征值进行匹配。
struct rtable中的fl成员,用于存储相关的路由特征值,也就是路由缓存查找匹配的关键字,它是一个struct flowi结构类型:
/*Egress设备ID和ingress设备ID*/
int oif;
int iif;
/*该联合的各个字段是可用于指定L3参数取值的结构。目前支持的协议为IPv4,IPv6和DECnet。*/
union {
struct {
__u32 daddr;
__u32 saddr;
__u32 fwmark;
__u8 tos;
__u8 scope;
} ip4_u;
struct {
struct in6_addr daddr;
struct in6_addr saddr;
__u32 flowlabel;
} ip6_u;
struct {
__u16 daddr;
__u16 saddr;
__u32 fwmark;
__u8 scope;
} dn_u;
} nl_u;
#define fld_dst nl_u.dn_u.daddr
#define fld_src nl_u.dn_u.saddr
#define fld_fwmark nl_u.dn_u.fwmark
#define fld_scope nl_u.dn_u.scope
#define fl6_dst nl_u.ip6_u.daddr
#define fl6_src nl_u.ip6_u.saddr
#define fl6_flowlabel nl_u.ip6_u.flowlabel
#define fl4_dst nl_u.ip4_u.daddr
#define fl4_src nl_u.ip4_u.saddr
#define fl4_fwmark nl_u.ip4_u.fwmark
#define fl4_tos nl_u.ip4_u.tos
#define fl4_scope nl_u.ip4_u.scope
/*L4协议*/
__u8 proto;
/*该变量只定义了一个标志,FLOWI_FLAG_MULTIPATHOLDROUTE,它最初用于多路径代码,但已不再被使用。*/
__u8 flags;
#define FLOWI_FLAG_MULTIPATHOLDROUTE 0x01
/*该联合的各个字段是可用于指定L4参数取值的主要结构。目前支持的协议为TCP,UDP,ICMP,DECnet和IPsec协议套件 (suite)*/
union {
struct {
__u16 sport;
__u16 dport;
} ports;
struct {
__u8 type;
__u8 code;
} icmpt;
struct {
__u16 sport;
__u16 dport;
__u8 objnum;
__u8 objnamel; /* Not 16 bits since max val is 16 */
__u8 objname[16]; /* Not zero terminated */
} dnports;
__u32 spi;
} uli_u;
#define fl_ip_sport uli_u.ports.sport
#define fl_ip_dport uli_u.ports.dport
#define fl_icmp_type uli_u.icmpt.type
#define fl_icmp_code uli_u.icmpt.code
#define fl_ipsec_spi uli_u.spi
} __attribute__((__aligned__(BITS_PER_LONG/8)));
抛开其它协议和成 员,联合体成员ip4_u就是IPV4协议关心的东东了:
__u32 daddr;
__u32 saddr;
__u32 fwmark;
__u8 tos;
__u8 scope;
} ip4_u;
于是,在遍历路由缓存项时,就可以使用如下语句来匹配路由缓存:
rth->fl.fl4_src == saddr &&
rth->fl.iif == iif &&
rth->fl.oif == 0 &&
#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark == skb->nfmark &&
#endif
rth->fl.fl4_tos == tos)
分别对来源/目的地 址,输入/输出设备,Netfilter防火墙的标记值和TOS值进行匹配。如果手气好,查找命中了:
dst_hold(&rth->u.dst);
rth->u.dst.__use++; //更新缓存使用记数器
RT_CACHE_STAT_INC(in_hit);
rcu_read_unlock();
[b]skb->dst = (struct dst_entry*)rth; //设置skb的dst指针指向路由缓存项[/b]
return 0;
如果没有查到,怎么办?
当然,不是次次都能糊清一色的,如果没有命中的话,就要去查到路由表了。可以推想,网络栈在缓存查找没有命中后,会去搜索路由表,如果路由表匹配,会将由 于产生的路由缓存项插入缓存表,以待下一次使用。