為了描述方便,以下將不再提起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);