3、缓存的查找当数据包进入网络层后,第一个被调用的函数是ip_rcv函数:
/* Main IP Receive routine. */
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函数进行缓存查找:
static inline int ip_rcv_finish(struct sk_buff *skb)
{
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是如何进行缓存查找的。
int ip_route_input(struct sk_buff *skb, //数据包
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值
hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos);
这里用到了rcu锁,关于这个锁的更多内容,可以参考其它相关资料。宏rcu_dereference在RCU读临界部份中取出一个RCU保护的指针。在需要内存屏障的体系中进行内存屏障:
#define rcu_dereference(p) ({ \
typeof(p) _________p1 = p; \
smp_read_barrier_depends(); \
(_________p1); \
})
于是,我们有了hash值后,就可以在hash桶中直接找到链表入口:
struct rtable * rth;
rth = rcu_dereference(rt_hash_table[hash].chain);
如果要遍历该链表中的所有路由缓存项,就可以使用如下循环:
for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
rth = rcu_dereference(rth->u.rt_next)) {
……
}
遍历每一个缓存项就简单,重要的是如何将缓存中的路由特征同数据包的特征值进行匹配。
struct rtable中的fl成员,用于存储相关的路由特征值,也就是路由缓存查找匹配的关键字,它是一个struct flowi结构类型:
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协议关心的东东了:
struct {
__u32 daddr;
__u32 saddr;
__u32 fwmark;
__u8 tos;
__u8 scope;
} ip4_u;
于是,在遍历路由缓存项时,就可以使用如下语句来匹配路由缓存:
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)
分别对来源/目的地址,输入/输出设备,Netfilter防火墙的标记值和TOS值进行匹配。如果手气好,查找命中了:
rth->u.dst.lastuse = jiffies; //更新最近使用时间标记
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;
如果没有查到,怎么办?
当然,不是次次都能糊清一色的,如果没有命中的话,就要去查到路由表了。可以推想,网络栈在缓存查找没有命中后,会去搜索路由表,如果路由表匹配,会将由于产生的路由缓存项插入缓存表,以待下一次使用。关于路由表查找的相关内容,我会在下一章中分析。