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;
}
View Code
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);
}
}
http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!!
但行好事 莫问前程
--身高体重180的胖子