dst=lookup_fib_table(skb);
dst_nexthop=alloc_entry(dst);
neigh=bind_neigh(dst_nexthop);
neigh.output(skb);
release_entry(dst_nexthop);这是一个完美的过程,然而在协议栈的实现层面,出现了新的问题,即 alloc/release会带来巨大的内存抖动,我们知道,内存分配与释放是一个必须要在CPU外部完成的事务,它的开销是巨大的,虽然在Linux中有slab cache,但是我们同样也知道,cache是分层的。事实上,Linux在3.6以后,实现了新的路由cache,不再缓存一个路由项,因为那需要 skb的元组精确匹配,而是缓存下一跳,找到这个cache必须经过lookup_fib_table这个例程。
这是个创举,因为缓存的东西是唯一的,除非发生一些例外!这就破解了解决多对一以及多对少的问题,在找到缓存之前,你必须先查找路由表,而查找完毕之后,理论上你已经知道了下一跳,除非一些例外(再次重申!)这个新的下一跳缓存只是为了避免内存的分配/释放!伪代码如下:
dst=lookup_fib_table(skb);
dst_nexthop=lookup_nh_cache(dst);
if dst_nexthop == NULL;
then
dst_nexthop=alloc_entry(dst);
if dst_nexthop.cache == true;
then
insert_into_nh_cache(dst_nexthop);
endif
endif
neigh=bind_neigh(dst_nexthop);
neigh.output(skb);
if dst_nexthop.cache == false
then
release_entry(dst_nexthop);
endif就这样,路由cache不再缓存整个路由项,而是缓存路由表查找结果的下一跳。
鉴于一般而言,一个路由项只有一个下一跳,因此这个缓存是极其有意义的。这意味着,在大多数时候,当路由查找的结果是一个确定的dst时,其下一跳缓存会命中,此时便不再需要重新分配新的dst_nexthop结构体,而是直接使用缓存中的即可,如果很不幸,没有命中,那么重新分配一个 dst_nexthop,将其尽可能地插入到下一跳缓存,如果再次很不幸,没有成功插入,那么设置NOCACHE标志,这意味着该dst_nexthop 使用完毕后将会被直接释放。
上述段落说明的是下一跳缓存命中的情况,那么在什么情况下会不命中呢,这很简单,无非就是在上述的lookup_nh_cache例程中返回NULL的时候,有不多的几种情况会导致其发生,比如某种原因将既有的路由项删除或者更新等。这个我随后会通过一个p2p虚拟网卡mtu问题给予说明,在此之前,我还要阐述另外一种常见的情形,那就是重定向路由。
所谓的重定向路由,它会更新本节点路由表的一个路由项条目,要注意的是,这个更新并不是永久的,而是临时的,所以Linux的做法并不是直接修改路由表,而是修改下一跳缓存!这个过程是异步的,伪代码如下:
# IP_OUT例程执行IP发送逻辑,它首先会查找标准路由表,然后在下一跳缓存中查找下一跳dst_nexthop,以决定是否重新分配一个新的dst_nexthop,除非你一开始指定NOCACHE标志,否则几乎都会在查找下一跳缓存失败进而创建新的dst_nexthop之后将其插入到下一跳缓存,以留给后续的数据包发送时使用,这样就避免了每次重新分配/释放新的内存空间。
func IP_OUT:
dst=lookup_fib_table(skb);
dst_nexthop = loopup_redirect_nh(skb.daddr, dst);
if dst_nexthop == NULL;
then
dst_nexthop=lookup_nh_cache(dst);
endif
if dst_nexthop == NULL;
then
dst_nexthop=alloc_entry(dst);
if dst_nexthop.cache == true;
then
insert_into_nh_cache(dst_nexthop);
endif
endif
neigh=bind_neigh(dst_nexthop);
neigh.output(skb);
if dst_nexthop.cache == false
then
release_entry(dst_nexthop);
endif
endfunc
# IP_ROUTE_REDIRECT例程将创建或者更新一个dst_nexthop,并将其插入到一个链表中,该链表由数据包的目标地址作为查找键。
func IP_ROUTE_REDIRECT:
dst=lookup_fib_table(icmp.redirect.daddr);
dst_nexthop = new_dst_nexthop(dst, icmp.redirect.newnexthop);
insert_into_redirect_nh(dst_nexthop);
endfunc
以上就是3.6以后内核的下一跳缓存逻辑,值得注意,它并没有减少路由查找的开销,而是减少了内存分配/释放的开销!路由查找是绕不过去的,但是路由查找结果是路由项,它和下一跳结构体以及邻居结构体之间还有层次关系,其关系如下:
路由项-下一跳结构体-邻居项
一个数据包在发送过程中,必须在路由查找结束后绑定一个下一跳结构体,然后绑定一个邻居,路由表只是一个静态表,数据通道没有权限修改它,它只是用来查找,协议栈必须用查找到的路由项信息来构造一个下一跳结构体,这个时候就体现了缓存下一跳的重要性,因为它减少了构造的开销!