为了描述方便,以下将不再提起rtable,将路由查找结果一律用dst_entry代替!下面的代码并不是实际上的Linux协议栈的代码,而是为了表述方便抽象而成的伪代码,因此dst_entry并不是内核中的dst_entry结构体,而只是代表一个路由项!这么做的理由是,dst_entry表示的是与协议无关的部分,本文的内容也是与具体协议无关的,因此在伪代码中不再使用协议相关的rtable结构体表示路由项。
Fib_info {
Address nexhop;
Hash_list exception;
};
这个exception表的表项类似下面的样子:
Exception_entry {
Match_info info;
Address new_nexthop;
};
这样的话,当收到Reidrect路由的时候,会初始化一个Exception_entry记录并且插入到相应的exception哈希表,在查询路由的时候,比如说最终找到了一个Fib_info,在构建最终的dst_entry之前,要先用诸如源IP信息之类的Match_info去查找exception哈希表,如果找到一个匹配的Exception_entry,则不再使用Fib_info中的nexhop构建dst_entry,而是使用找到的Exception_entry中的new_nexthop来构建dst_entry。
dst_entry = 路由表查找(或者路由cache查找,通过skb的destination作键值)
nexthop = dst_entry.nexthop
neigh = neighbour表查找(通过nexthop作为键值)
然而Linux协议栈的实现却远远比这更复杂,这一切还得从3.5内核重构前开始说起。
func ip_output(skb):
dst_entry = lookup_from_cache(skb.destination);
if dst_entry == NULL
then
dst_entry = lookup_fib(skb.destination);
nexthop = dst_entry.gateway?:skb.destination;
neigh = lookup(neighbour_table, nexthop);
if neigh == NULL
then
neigh = create(neighbour_table, nexthop);
neighbour_add_timer(neigh);
end
dst_entry.neighbour = neigh;
insert_into_route_cache(dst_entry);
end
neigh = dst_entry.neighbour;
neigh.output(neigh, skb);
endfunc
---->TO Layer2
试看以下几个问题:
func ip_output(skb):
dst_entry = lookup_from_cache(skb.destination);
if dst_entry == NULL
then
dst_entry = lookup_fib(skb.destination);
nexthop = dst_entry.gateway?:skb.destination;
neigh = lookup(neighbour_table, nexthop);
if neigh == NULL
then
neigh = create(neighbour_table, nexthop);
neighbour_add_timer(neigh);
end
inc(neigh.refcnt);
dst_entry.neighbour = neigh;
insert_into_route_cache(dst_entry);
end
neigh = dst_entry.neighbour;
# 如果是INVALID状态的neigh,需要在output回调中处理
neigh.output(neigh, skb);
endfunc
func neighbour_add_timer(neigh):
inc(neigh.refcnt);
neigh.timer.func = neighbour_timeout;
timer_start(neigh.timer);
endfunc
func neighbour_timeout(neigh):
cnt = dec(neigh.refcnt);
if cnt == 0
then
free_neigh(neigh);
else
neigh.status = INVALID;
end
endfunc
func dst_entry_timeout(dst_entry):
neigh = dst_entry.neighbour;
cnt = dec(neigh.refcnt);
if cnt == 0
then
free_neigh(neigh);
end
free_dst(dst_entry);
endfunc
我们最后看看这会带来什么问题。
func create(neighbour_table, nexthop):
retry:
neigh = alloc_neigh(nexthop);
if neigh == NULL or neighbour_table.num > MAX
then
shrink_route_cache();
retry;
end
endfunc
func ip_output(skb):
dst_entry = lookup_fib(skb.destination);
nexthop = dst_entry.gateway?:skb.destination;
neigh = lookup(neighbour_table, nexthop);
if neigh == NULL
then
neigh = create(neighbour_table, nexthop);
end
neigh.output(skb);
endfunc
路由项不再和neighbour关联,因此neighbour表就可以独立执行过期操作了,neighbour表由于路由cache的gc过慢而导致频繁爆满的情况也就消失了。
struct neighbour *neigh_create(struct neigh_table *tbl, const void *pkey,
struct net_device *dev)
{
struct neighbour *n1, *rc, *n = neigh_alloc(tbl);
......
write_lock_bh(&tbl->lock);
// 插入hash表
write_unlock_bh(&tbl->lock);
.......
}
在海量目标IP的skb通过pointopoint设备发送的时候,这是一个完全避不开的瓶颈!然而内核没有这么傻。它采用了以下的方式进行了规避:
__be32 nexthop = ((struct rtable *)dst)->rt_gateway?:ip_hdr(skb)->daddr;
if (dev->flags&(IFF_LOOPBACK|IFF_POINTOPOINT))
nexthop = 0;
这就意味着只要发送的pointopint设备相同,且伪二层(比如IPGRE的情况)信息相同,所有的skb将使用同一个neighbour,不管它们的目标地址是否相同。在IPIP Tunnel的情形下,由于这种设备没有任何的二层信息,这更是意味着所有的通过IPIP Tunnel设备的skb将使用一个单一的neighbour,即便是使用不同的IPIP Tunnel设备进行发送。
static inline __be32 rt_nexthop(const struct rtable *rt, __be32 daddr)
{
if (rt->rt_gateway)
return rt->rt_gateway;
return daddr;
}
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
......
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res = dst_neigh_output(dst, neigh, skb);
return res;
}
......
}
可以看到,dev->flags&(IFF_LOOPBACK|IFF_POINTOPOINT)这个判断消失了!这意味着内核变傻了。上一段中分析的那种现象在3.5之后的内核中将会发生,事实上也一定会发生。
static const struct neigh_ops dummy_direct_ops = {
.family = AF_INET,
.output = neigh_direct_output,
.connected_output = neigh_direct_output,
};
struct neighbour dummy_neigh;
void dummy_neigh_init()
{
memset(&dummy_neigh, 0, sizeof(dummy_neigh));
dummy_neigh.nud_state = NUD_NOARP;
dummy_neigh.ops = &dummy_direct_ops;
dummy_neigh.output = neigh_direct_output;
dummy_neigh.hh.hh_len = 0;
}
static inline int ip_finish_output2(struct sk_buff *skb)
{
......
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
if (dev->type == ARPHRD_TUNNEL) {
neigh = &dummy_neigh;
} else {
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
}
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
......
}
后来看了3.5内核之前的实现,发现了:
if (dev->flags&(IFF_LOOPBACK|IFF_POINTOPOINT))
nexthop = 0;
于是决定采用这个,代码更少也更优雅!然后就产生了下面的patch:
diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c
--- a/net/ipv4/ip_output.c
+++ b/net/ipv4/ip_output.c
@@ -202,6 +202,8 @@ static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *s
rcu_read_lock_bh();
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
+ if (dev->flags & (IFF_LOOPBACK | IFF_POINTOPOINT))
+ nexthop = 0;
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);