3.6版本以前的路由缓存
缓存无处不在。现代计算机系统中,Cache是CPU与内存间存在一种容量较小但速度很高的存储器,用来存放CPU
刚使用过或最近使用的数据。路由缓存就是基于这种思想的软件实现。内核查询FIB前,固定先查询cache中的记录,如果cache命中(hit),那就直接用就好了,不必查询FIB。如果没有命中(miss), 就回过头来查询FIB,最终将结果保存到cache,以便下次不再需要需要查询FIB。
缓存是精确匹配的, 每一条缓存表项记录了匹配的源地址和目的地址、接收发送的dev
,以及与内核邻居系统(L2层)的联系(negghbour
) FIB
中存储的也就是路由信息,它常常是范围匹配的,比如像ip route 1.2.3.0/24 dev eth0
这样的网段路由
看上去的确可能能提高性能! 只要cache命中率足够高。要获得高的cache命中率有以下两个途径:1. 存储更多的表项; 2.存储更容易命中的表项
缓存中存放的表项越多,那么目标报文与表项匹配的可能性越大。但是cache又不能无限制地增大,cache本身占用内存是一回事,更重要的是越多的表项会导致查询cache本身变慢。使用cache的目的是为了加速,如果不能加速,那就没用了!
内核为了避免cache表项过多,内核还会在一定时机下清除过期的表项。有两个这样的时机,其一是添加新的表项时,如果冲突链的表项过多,就删除一条已有的表项;其二是内核会启动一个专门的定时器周期性地老化一些表项.
获得更高的cache命中率的第二个途径是存储更容易命中的表项,什么是更容易命中的呢? 那就是真正有效的报文。遗憾的是,内核一点也不聪明:只要输入路由系统的报文不来离谱,它就会生成新的缓存表项。坏人正好可以利用这一点,不停地向主机发送垃圾报文,内核因此会不停地刷新cache。这样每个skb都会先在cache表中进行搜索,再查询FIB表,最后再创建新的cache表项,插入到cache表。这个过程中还会涉及为每一个新创建的cache表项绑定邻居,这又要查询一次ARP
表。
要知道,一台主机上的路由表项可能有很多,特别是对于网络交换设备,由OSPF**BGP等路由协议动态下发的表项有上万条是很正常的事。而邻居节点却不可能达到这个数量。对于转发或者本机发送的skb来说,路由系统能帮它们找到下一跳邻居**就足够了。
总结起来就是,3.6版本以前的这种路由缓存在skb地址稳定时的确可能提高性能。但这种根据skb内容决定的性能却是不可预测和不稳定的。
3.6版本以后的下一跳缓存
正如前面所说,3.6版本移除了FIB查找前的路由缓存。这意味着每一个接收发送的skb现在都必须要进行FIB查找了。这样的好处是现在查找路由的代价变得稳定(consistent)了。
路由缓存完全消失了吗? 并没有!在3.6以后的版本, 你还可以在内核代码中看到dst_entry。这是因为,3.6版本实际上是将FIB查找缓存到了下一跳(fib_nh)结构上,也就是下一跳缓存
········什么需要缓存下一跳呢? 我们可以先来看下没有下一跳缓存的情况。以转发过程为例,相关的伪代码如下:
FORWARD: fib_result = fib_lookup(skb) dst_entry = alloc_dst_entry(fib_result) skb->dst = dst_entry; skb->dst.output(skb) nexthop = rt_nexthop(skb->dst, ip_hdr(skb)->daddr) neigh = ipv4_neigh_lookup(dev, nexthop) dst_neigh_output(neigh,skb) release_dst_entry(skb->dst)
内核利用FIB查询的结果申请dst_entry, 并设置到skb上,然后在发送过程中找到下一跳地址,继而查找到邻居结构(查询ARP),然后邻居系统将报文发送出去,最后释放dst_entry。
下一跳缓存的作用就是尽量减少最初和最后的申请释放dst_entry,它将dst_entry缓存在下一跳结构(fib_nh)上。这和之前的路由缓存有什么区别吗? 很大的区别!之前的路由缓存是以源IP和目的IP为KEY,有千万种可能性,而现在是和下一跳绑定在一起,一台设备没有那么多下一跳的可能。这就是下一跳缓存的意义!
early demux
early demux
是在skb接收方向的加速方案。如前面所说,在取消了FIB查询前的路由缓存后,每个skb应该都需要查询FIB。而early demux是基于一种思想:如果一个skb是本机某个应用程序的套接字需要的,那么我们可以将路由的结果缓存在内核套接字结构上,这样下次同样的报文(四元组)到达后,我们可以在FIB查询前就将报文提交给上层,也就是提前分流(early demux)。但是 对于非面向链接的socket等 就不友好了!
ip_route_input_slow 分析:
/* * NOTE. We drop all the packets that has local source * addresses, because every properly looped back packet * must have correct destination already attached by output routine. * * Such approach solves two big problems: * 1. Not simplex devices are handled properly. * 2. IP spoofing attempts are filtered with 100% of guarantee. * called with rcu_read_lock() */ static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr, u8 tos, struct net_device *dev) { struct fib_result res; struct in_device *in_dev = __in_dev_get_rcu(dev);//这里要使用输入网络设备dev,增加引用计数 struct ip_tunnel_info *tun_info; struct flowi4 fl4; unsigned int flags = 0; u32 itag = 0; struct rtable *rth; int err = -EINVAL; struct net *net = dev_net(dev); bool do_cache; /* IP on this device is disabled. */ if (!in_dev) goto out; /* Check for the most weird martians, which can be not detected by fib_lookup. */ tun_info = skb_tunnel_info(skb); if (tun_info && !(tun_info->mode & IP_TUNNEL_INFO_TX)) fl4.flowi4_tun_key.tun_id = tun_info->key.tun_id; else fl4.flowi4_tun_key.tun_id = 0; skb_dst_drop(skb); if (ipv4_is_multicast(saddr) || ipv4_is_lbcast(saddr)) goto martian_source; debug_v4route("%s-->begin to slow looking\n",__FUNCTION__); res.fi = NULL; res.table = NULL; if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0)) goto brd_input; /* Accept zero addresses only to limited broadcast; * I even do not know to fix it or not. Waiting for complains :-) */ if (ipv4_is_zeronet(saddr)) goto martian_source; if (ipv4_is_zeronet(daddr)) goto martian_destination; /* Following code try to avoid calling IN_DEV_NET_ROUTE_LOCALNET(), * and call it once if daddr or/and saddr are loopback addresses */ if (ipv4_is_loopback(daddr)) { if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net)) goto martian_destination; } else if (ipv4_is_loopback(saddr)) { if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net)) goto martian_source; } /* * Now we are ready to route packet. */ fl4.flowi4_oif = 0; fl4.flowi4_iif = l3mdev_fib_oif_rcu(dev); fl4.flowi4_mark = skb->mark; fl4.flowi4_tos = tos; fl4.flowi4_scope = RT_SCOPE_UNIVERSE; fl4.flowi4_flags = 0; fl4.daddr = daddr; fl4.saddr = saddr; err = fib_lookup(net, &fl4, &res, 0); pr_err("dev_name:%s src:%pI4-->dst:%pI4 mark:%d fib_loopup[err%d res.type:%d]\n", in_dev->dev->name,&saddr, &daddr, skb->mark, err, res.type); if (err != 0) { if (!IN_DEV_FORWARD(in_dev)) err = -EHOSTUNREACH; goto no_route;pr_err } // /*根据查找到的路由类型,分类处理 广播处理*/ if (res.type == RTN_BROADCAST) goto brd_input; if (res.type == RTN_LOCAL) { /*如果是发给本机的包,则验证原地址是否合法*/ err = fib_validate_source(skb, saddr, daddr, tos, 0, dev, in_dev, &itag); /*对于RTN_LOCAL类型或者RTN_BROADCAST类型的路由表项,如果反向路由查找失败, 也认定源地址为非法地址,在函数ip_handle_martian_source中递增in_martian_src计*/ pr_err("dev_name:%s src:%pI4-->dst:%pI4 mark:%d fib_validate_source[err:%d ]\n", in_dev->dev->name,&saddr, &daddr, skb->mark, err); if (err < 0) goto martian_source; goto local_input; } if (!IN_DEV_FORWARD(in_dev)) { err = -EHOSTUNREACH; goto no_route; } if (res.type != RTN_UNICAST) goto martian_destination; /*当查到的路由类型是指向远端的主机,把此路由加入cache中*/ err = ip_mkroute_input(skb, &res, &fl4, in_dev, daddr, saddr, tos); out: return err; brd_input:/*当目的地址是广播地址,或查到的路由类型是广播类型*/ /*报文的目的地址为广播地址;或者源地址和目的地址同时为全0; 或者fib查询的结果为RTN_BROADCAST类型路由,如果接收设备的广播转发开关开启, 有函数ip_mkroute_input创建路由缓存,但是其仅递增了in_slow_tot计数*/ if (skb->protocol != htons(ETH_P_IP)) goto e_inval; if (!ipv4_is_zeronet(saddr)) { err = fib_validate_source(skb, saddr, 0, tos, 0, dev, in_dev, &itag); if (err < 0) goto martian_source; } flags |= RTCF_BROADCAST; res.type = RTN_BROADCAST; RT_CACHE_STAT_INC(in_brd); local_input:/*当查找到的路由指向本机时*/ do_cache = false; if (res.fi) { if (!itag) { rth = rcu_dereference(FIB_RES_NH(res).nh_rth_input); if (rt_cache_valid(rth)) { skb_dst_set_noref(skb, &rth->dst); err = 0; goto out; } do_cache = true; } } rth = rt_dst_alloc(net->loopback_dev, flags | RTCF_LOCAL, res.type, IN_DEV_CONF_GET(in_dev, NOPOLICY), false, do_cache); /* rth->dst.input= ip_local_deliver; ----->路由查找结束后会调用此函数把报文送给上层处理*/ if (!rth) goto e_nobufs; rth->dst.output= ip_rt_bug; #ifdef CONFIG_IP_ROUTE_CLASSID rth->dst.tclassid = itag; #endif rth->rt_is_input = 1; if (res.table) rth->rt_table_id = res.table->tb_id; RT_CACHE_STAT_INC(in_slow_tot);//统计路由cache分配次数 if (res.type == RTN_UNREACHABLE) {// no route rth->dst.input= ip_error;/*PS:没有查找到路由的时候也会向缓存中添加一条不可达路由项*/ rth->dst.error= -err; rth->rt_flags &= ~RTCF_LOCAL; } if (do_cache) {//需要执行缓存操作 do_cache ,函数rt_cache_route来实现 if (unlikely(!rt_cache_route(&FIB_RES_NH(res), rth))) { rth->dst.flags |= DST_NOCACHE; rt_add_uncached_list(rth);//如果缓存失败,将路由项添加到uncached_list链表 } } skb_dst_set(skb, &rth->dst); err = 0; goto out; no_route: /*没有查找到路由的时候,向缓存中添加一条不可达路由项*/ //统计fib查询失败次数,以及路由下一跳设备转发未启用 RT_CACHE_STAT_INC(in_no_route); res.type = RTN_UNREACHABLE; res.fi = NULL; res.table = NULL; goto local_input; /* *Do not cache martian addresses: they should be logged (RFC1812)如果报文的目的地址为0;或者为回环地址 但是接收设备不允许回环地址(可通过PROC文件配置,例如eth0配置文件: /proc/sys/net/ipv4/conf/eth0/route_localnet)。 或者,查询fib表的结果得到的路由类型不等于RTN_BROADCAST、RTN_LOCAL和RTN_UNICAST中的任何一个, 认为此报文的目的地址为非法地址。增加in_martian_dst计数。 */ martian_destination: RT_CACHE_STAT_INC(in_martian_dst); #ifdef CONFIG_IP_ROUTE_VERBOSE if (IN_DEV_LOG_MARTIANS(in_dev)) net_warn_ratelimited("martian destination %pI4 from %pI4, dev %s\n", &daddr, &saddr, dev->name); #endif e_inval: err = -EINVAL; goto out; e_nobufs: err = -ENOBUFS; goto out; /*在查找路由时,如果报文的源地址为多播地址,或者全F的广播地址,或者源地址为0。 或者目的地址不是回环地址,但是源地址为回环地址,并且接收设备没有开启接收开关, 认定报文的源地址为非法地址。 对于RTN_LOCAL类型或者RTN_BROADCAST类型的路由表项,如果反向路由查找失败, 也认定源地址为非法地址,在函数ip_handle_martian_source中递增in_martian_src计*/ martian_source: ip_handle_martian_source(dev, in_dev, skb, daddr, saddr); goto out; }
/* called in rcu_read_lock() section */ static int __mkroute_input(struct sk_buff *skb, const struct fib_result *res, struct in_device *in_dev, __be32 daddr, __be32 saddr, u32 tos) { struct fib_nh_exception *fnhe; struct rtable *rth; int err; struct in_device *out_dev; bool do_cache; u32 itag = 0; /* get a working reference to the output device */ out_dev = __in_dev_get_rcu(FIB_RES_DEV(*res));;//获取输出报文的网络设备 if (!out_dev) { net_crit_ratelimited("Bug in ip_route_input_slow(). Please report.\n"); return -EINVAL; } //路由合法性检查,在调用该函数前,已经找到一条从saddr->daddr的路由项, //需要进行判断daddr->saddr反向路由是否存在,否则认为它是非法的 err = fib_validate_source(skb, saddr, daddr, tos, FIB_RES_OIF(*res), in_dev->dev, in_dev, &itag); if (err < 0) { ip_handle_martian_source(in_dev->dev, in_dev, skb, daddr, saddr); goto cleanup; } do_cache = res->fi && !itag; if (out_dev == in_dev && err && IN_DEV_TX_REDIRECTS(out_dev) && skb->protocol == htons(ETH_P_IP) && (IN_DEV_SHARED_MEDIA(out_dev) || inet_addr_onlink(out_dev, saddr, FIB_RES_GW(*res)))) IPCB(skb)->flags |= IPSKB_DOREDIRECT; if (skb->protocol != htons(ETH_P_IP)) {//如果不是ip 比如arp /* Not IP (i.e. ARP). Do not create route, if it is * invalid for proxy arp. DNAT routes are always valid. * * Proxy arp feature have been extended to allow, ARP * replies back to the same interface, to support * Private VLAN switch technologies. See arp.c. */// arp 报文的处理 也会涉及到路由处理 if (out_dev == in_dev && IN_DEV_PROXY_ARP_PVLAN(in_dev) == 0) { err = -EINVAL; goto cleanup; } } // 查找 fl4->daddr 是否存在 fib_nh_exception fnhe = find_exception(&FIB_RES_NH(*res), daddr); if (do_cache) { if (fnhe) { rth = rcu_dereference(fnhe->fnhe_rth_input); if (rth && rth->dst.expires && time_after(jiffies, rth->dst.expires)) {//检验是否过期等 ip_del_fnhe(&FIB_RES_NH(*res), daddr); fnhe = NULL; } else { // 如果有 且没过期可以使用,直接使用其绑定的路由缓存 goto rt_cache; } } rth = rcu_dereference(FIB_RES_NH(*res).nh_rth_input); rt_cache: if (rt_cache_valid(rth)) { // 如果有,直接使用其绑定的路由缓存 skb_dst_set_noref(skb, &rth->dst); goto out; } } //创建新的路由缓存项 rth = rt_dst_alloc(out_dev->dev, 0, res->type, IN_DEV_CONF_GET(in_dev, NOPOLICY), IN_DEV_CONF_GET(out_dev, NOXFRM), do_cache); if (!rth) { err = -ENOBUFS; goto cleanup; } rth->rt_is_input = 1; if (res->table) rth->rt_table_id = res->table->tb_id; RT_CACHE_STAT_INC(in_slow_tot);//统计路由cache分配次数 //so RT_CACHE_STAT_INC(in_slow_mc) 统计多播路由cache分配次数 rth->dst.input = ip_forward; //对于转发路由,由函数rt_set_nexthop处理路由缓存 rt_set_nexthop(rth, daddr, res, fnhe, res->fi, res->type, itag); if (lwtunnel_output_redirect(rth->dst.lwtstate)) { rth->dst.lwtstate->orig_output = rth->dst.output; rth->dst.output = lwtunnel_output; } if (lwtunnel_input_redirect(rth->dst.lwtstate)) { rth->dst.lwtstate->orig_input = rth->dst.input; rth->dst.input = lwtunnel_input; } //设置更新skb的dst 路由信息 skb_dst_set(skb, &rth->dst); out: err = 0; cleanup: return err; }
在缓存路由项时,如果缓存成功(cmpxchg),并且原有缓存值不为空,将原有路由缓存值添加到uncached_list链
static bool rt_cache_route(struct fib_nh *nh, struct rtable *rt) { struct rtable *orig, *prev, **p; bool ret = true; if (rt_is_input_route(rt)) { p = (struct rtable **)&nh->nh_rth_input; } else { p = (struct rtable **)raw_cpu_ptr(nh->nh_pcpu_rth_output); } orig = *p; /*在缓存路由项时,如果缓存成功(cmpxchg),并且原有缓存值不为空,并执行释放操作(有延迟)。 否者,如果缓存操作失败,返回错误 */ prev = cmpxchg(p, orig, rt); if (prev == orig) { if (orig) rt_free(orig); } else ret = false; return ret; }
对于转发路由,由函数rt_set_nexthop处理路由缓存。
static void rt_set_nexthop(struct rtable *rt, __be32 daddr, const struct fib_result *res, struct fib_nh_exception *fnhe, struct fib_info *fi, u16 type, u32 itag) { bool cached = false; if (fi) { struct fib_nh *nh = &FIB_RES_NH(*res); if (nh->nh_gw && nh->nh_scope == RT_SCOPE_LINK) { rt->rt_gateway = nh->nh_gw; rt->rt_uses_gateway = 1; } dst_init_metrics(&rt->dst, fi->fib_metrics->metrics, true); if (fi->fib_metrics != &dst_default_metrics) { rt->dst._metrics |= DST_METRICS_REFCOUNTED; atomic_inc(&fi->fib_metrics->refcnt); } #ifdef CONFIG_IP_ROUTE_CLASSID rt->dst.tclassid = nh->nh_tclassid; #endif rt->dst.lwtstate = lwtstate_get(nh->nh_lwtstate); /*如果fnhe有值,由函数rt_bind_exception处理,并进行路由缓存;否则,如果do_cache为真, 由之前介绍的函数rt_cache_route进行缓存操作。最后如果路由缓存操作失败的话, 将路由项链接到uncached_list链表上*/ if (unlikely(fnhe)) cached = rt_bind_exception(rt, fnhe, daddr); else if (!(rt->dst.flags & DST_NOCACHE)) cached = rt_cache_route(nh, rt); if (unlikely(!cached)) { /* Routes we intend to cache in nexthop exception or * FIB nexthop have the DST_NOCACHE bit clear. * However, if we are unsuccessful at storing this * route into the cache we really need to set it. */ rt->dst.flags |= DST_NOCACHE; if (!rt->rt_gateway) rt->rt_gateway = daddr; rt_add_uncached_list(rt); } } else { //如果fib_info结构变量fi为空(没有路由缓存位置),不进行路由缓存,直接加入uncached_list链表 rt_add_uncached_list(rt); } #ifdef CONFIG_IP_ROUTE_CLASSID #ifdef CONFIG_IP_MULTIPLE_TABLES set_class_tag(rt, res->tclassid); #endif set_class_tag(rt, itag); #endif }
在函数rt_bind_exception中,没有使用rt_cache_route函数中的cmpxchg指令。而是由rcu_dereference和rcu_assign_pointer进行类似的路由缓存操作,之后,如果原有缓存不为空,对其进行释放。
static bool rt_bind_exception(struct rtable *rt, struct fib_nh_exception *fnhe, __be32 daddr) { bool ret = false; spin_lock_bh(&fnhe_lock); /*在函数rt_bind_exception中,没有使用rt_cache_route函数中的cmpxchg指令。 而是由rcu_dereference和rcu_assign_pointer进行类似的路由缓存操作, 之后,如果原有缓存不为空,对其进行释放。 如果缓存操作未执行,将由以上的调用函数rt_set_nexthop将路由缓存项添加到uncached_list链表。 */ if (daddr == fnhe->fnhe_daddr) { struct rtable __rcu **porig; struct rtable *orig; int genid = fnhe_genid(dev_net(rt->dst.dev)); if (rt_is_input_route(rt)) porig = &fnhe->fnhe_rth_input; else porig = &fnhe->fnhe_rth_output; orig = rcu_dereference(*porig); if (fnhe->fnhe_genid != genid) { fnhe->fnhe_genid = genid; fnhe->fnhe_gw = 0; fnhe->fnhe_pmtu = 0; fnhe->fnhe_expires = 0; fnhe_flush_routes(fnhe); orig = NULL; } fill_route_from_fnhe(rt, fnhe); if (!rt->rt_gateway) rt->rt_gateway = daddr; if (!(rt->dst.flags & DST_NOCACHE)) { rcu_assign_pointer(*porig, rt); if (orig) rt_free(orig); ret = true; } fnhe->fnhe_stamp = jiffies; } spin_unlock_bh(&fnhe_lock); return ret; }
uncached_list链表删除
//当释放路由缓存时, 检测其是否在uncached_list链表中,为真将其从链表中删除 static void ipv4_dst_destroy(struct dst_entry *dst) { struct dst_metrics *p = (struct dst_metrics *)DST_METRICS_PTR(dst); struct rtable *rt = (struct rtable *) dst; if (p != &dst_default_metrics && atomic_dec_and_test(&p->refcnt)) kfree(p); if (!list_empty(&rt->rt_uncached)) { struct uncached_list *ul = rt->rt_uncached_list; spin_lock_bh(&ul->lock); list_del(&rt->rt_uncached); spin_unlock_bh(&ul->lock); } }
当系统注销一个网络设备时,遍历所有的uncached_list链表上的路由缓存项,如果其路由设备等于要注销的设备,将设备更换为黑洞设备blackhole_netdev,路由到此设备的报文都将被丢弃。
/*当系统注销一个网络设备时,遍历所有的uncached_list链表上的路由缓存项, 如果其路由设备等于要注销的设备,将设备更换为黑洞设备blackhole_netdev, 路由到此设备的报文都将被丢弃。*/ void rt_flush_dev(struct net_device *dev) { struct net *net = dev_net(dev); struct rtable *rt; int cpu; for_each_possible_cpu(cpu) { struct uncached_list *ul = &per_cpu(rt_uncached_list, cpu); spin_lock_bh(&ul->lock); list_for_each_entry(rt, &ul->head, rt_uncached) { if (rt->dst.dev != dev) continue; rt->dst.dev = net->loopback_dev;== blackhole_netdev dev_hold(rt->dst.dev); dev_put(dev); } spin_unlock_bh(&ul->lock); } }