路由表
在內核中存在路由表fib_table_hash和路由緩存表rt_hash_table。路由緩存表主要是為了加速路由的查找, 每次路由查詢都會先查找路由緩存,再查找路由表。這和cache是一個道理,緩存存儲最近使用過的路由項,容量小,查找快速 ;路由表存儲所有路由項,容量大,查找慢。
首先,應該先了解路由表的意義,下面是route命令查看到的路由表:
一條路由其實就是告知主 機要到達一個目的地址,下一跳應該走哪裡。比如發往192.168.22.3報文通過查路由表,會得到下一跳為192.168.123.254,再 將其發送出去。在路由表項中,還有一個很重要的屬性-scope,它代表了到目的網絡的距離。
路由scope可取值: RT_SCOPE_UNIVERSE, RT_SCOPE_LINK, RT_SCOPE_HOST
在報文的轉發過程中,顯然是每次轉發都要使到達目的網絡的距離 要越來越小或不變,否則根本到達不了目的網絡。上面提到的scope很好的實現這個功能,在查找路由表中,表項的scope一定是 更小或相等的scope(比如RT_SCOPE_LINK,則表項scope只能為RT_SCOPE_LINK或RT_SCOPE_HOST)。
路由緩存
路由 緩存用於加速路由的查找,當收到報文或發送報文時,首先會查詢路由緩存,在內核中被組織成hash表,就是rt_hash_table。
static struct rt_hash_bucket *rt_hash_table __read_mostly; [net/ipv4/route.c]
通過 ip_route_input()進行查詢,首先是緩存操作時,通過[src_ip, dst_ip, iif,rt_genid]計算出hash值
hash = rt_hash (daddr, saddr, iif, rt_genid(net));
此時rt_hash_table[hash].chain就是要操作的緩存表項的鏈表,比如遍歷該鏈 表
for (rth = rt_hash_table[hash].chain; rth; rth = rth->u.dst.rt_next)
因此,在緩存中查找一個表 項,首先計算出hash值,取出這組表項,然後遍歷鏈表,找出指定的表項,這裡需要完全匹配[src_ip, dst_ip, iif, tos, mark, net],實際上struct rtable中有專門的屬性用於緩存的查找鍵值– struct flowi。
/* Cache lookup keys */
struct flowi fl;
當找到表項後會更新表項的最後訪問時間,並取出dst
dst_use (&rth->u.dst, jiffies);
skb_dst_set(skb, &rth->u.dst);
路由緩存的創建
inet_init () -> ip_init() -> ip_rt_init()
rt_hash_table = (struct rt_hash_bucket *)
alloc_large_system_hash("IP route cache",
sizeof(struct rt_hash_bucket),
rhash_entries,
(totalram_pages >= 128 * 1024) ?
15 : 17,
0,
&rt_hash_log,
&rt_hash_mask,
rhash_entries ? 0 : 512 * 1024);
其中 rt_hash_mask表示表的大小,rt_hash_log = log(rt_hash_mask),創建後的結構如圖所示:
路由緩存插入條目
函數rt_intern_hash()
要插入的條目是rt,相應散列值是hash,首先通過hash值找到對應的bucket
rthp = &rt_hash_table[hash].chain;
然後對bucket進行一遍查詢,這次查詢的目的有兩個:如果是超時的條目,則直接 刪除;如果是與rt相同鍵值的條目,則刪除並將rt插入頭部返回。
while ((rth = *rthp) != NULL) {
if (rt_is_expired(rth)) { // 超時的條目
*rthp = rth->u.dst.rt_next;
rt_free (rth);
continue;
}
if (compare_keys(&rth->fl, &rt->fl) && compare_netns (rth, rt)) { //重復的條目
*rthp = rth->u.dst.rt_next;
rcu_assign_pointer(rth->u.dst.rt_next, rt_hash_table[hash].chain);
rcu_assign_pointer(rt_hash_table[hash].chain, rth);
……
}
……
rthp = &rth->u.dst.rt_next;
}
在掃描一遍後,如rt還未存在,則將其插入頭部
rt ->u.dst.rt_next = rt_hash_table[hash].chain;
rcu_assign_pointer(rt_hash_table[hash].chain, rt);
如果新插入rt滿足一定條件,還要與ARP鄰居表進行綁定
Hint:緩存的每個bucket是沒有頭結點的,單向鏈表,它所使用 的插入和刪除操作是值得學習的,簡單實用。
路由緩存刪除條目
rt_del()
要刪除的條目是rt,相應散列 值是hash,首先通過hash值找到對應的bucket,然後遍歷,如果條目超時,或找到rt,則刪除它。
rthp = &rt_hash_table[hash].chain;
spin_lock_bh(rt_hash_lock_addr(hash));
ip_rt_put(rt);
while ((aux = *rthp) != NULL) {
if (aux == rt || rt_is_expired(aux)) {
*rthp = aux- >u.dst.rt_next;
rt_free(aux);
continue;
}
rthp = &aux->u.dst.rt_next;
}
spin_unlock_bh(rt_hash_lock_addr(hash));
路由表的創建
inet_init() -> ip_init() -> ip_fib_init() -> fib_net_init() -> ip_fib_net_init()[net/ipv4/fib_frontend.c]
首先為路由表分配空間, 這裡的每個表項hlist_head實際都會鏈接一個單獨的路由表,FIB_TABLE_HASHSZ表示了分配多少個路由表,一般情況下至少有兩 個– LOCAL和MAIN。注意這裡僅僅是表頭的空間分配,還沒有真正分配路由表空間。
net->ipv4.fib_table_hash = kzalloc(
sizeof(struct hlist_head)*FIB_TABLE_HASHSZ, GFP_KERNEL);
ip_fib_net_init() -> fib4_rules_init(),這裡真正分配了路由表空間
local_table = fib_hash_table(RT_TABLE_LOCAL);
main_table = fib_hash_table(RT_TABLE_MAIN);
然後將local和main表鏈入之前的fib_table_hash中
hlist_add_head_rcu (&local_table->tb_hlist,
&net->ipv4.fib_table_hash [TABLE_LOCAL_INDEX]);
hlist_add_head_rcu(&main_table->tb_hlist,
&net- >ipv4.fib_table_hash[TABLE_MAIN_INDEX]);
最終生成結構如圖,LOCAL表位於fib_table_hash[0],MAIN表位於 fib_table_hash[1];兩張表通過結構tb_hlist鏈入鏈表,而tb_id則標識了功能,255是LOCAL表,254是MAIN表。
關於這 裡的struct fn_hash,它表示了不同子網掩碼長度的hash表[即fn_zone],對於ipv4,從0~32共33個。而fn_hash的實現則是 fib_table的最後一個參數unsigned char tb_data[0]。
注意到這裡fn_zone 還只是空指針,我們還只完成了路由表初始化的一部分。在啟動階段還會調用inet_rtm_newroute() -> fib_table_insert() -> fn_new_zone() [fib_hash.c]來創建fn_zone結構,前面已經講過,fn_zone一共有33個,其中掩碼長度為0[/0]表示為默 認路由,fn_zone可以理解為相同掩碼的地址集合。
首先為fn_zone分配空間
struct fn_zone *fz = kzalloc (sizeof(struct fn_zone), GFP_KERNEL);
傳入參數z代表掩碼長度, z = 0的掩碼用於默認路由,一般只有一個,所以 fz_centerisor只需設為1;其它設為16;這裡要提到fz_centerisor的作用,fz->fz_hash並不是個單鏈表,而是一個哈希表,而哈 希表的大小就是fz_centerisor。
if (z) {
fz->fz_centerisor = 16;
} else {
fz->fz_centerisor = 1;
}
fz_hashmask實際是用於求余數的,當算出hash值,再hash & fz_hashmask就得出了在哈希表的位置;而 fz_hash就是下一層的哈希表了,前面已經提過路由表被多組分層了,這裡fz_hash就是根據fz_centerisor大小來創建的;fz_order 就是子網掩碼長度;fz_mask就是子網掩碼。
fz->fz_hashmask = (fz->fz_centerisor - 1);
fz->fz_hash = fz_hash_alloc(fz->fz_centerisor);
fz->fz_order = z;
fz->fz_mask = inet_make_mask(z);
從子網長度大於新添加fz的fn_zone中挑選一個不為空的fn_zones[i],將新創建的fz設成fn_zones[i].next;然後將fz根據掩碼 長度添加到fn_zones[]中相應位置;fn_zone_list始終指向掩碼長度最長的fn_zone。
for (i=z+1; i<=32; i++)
if (table->fn_zones[i])
break;
if (i>32) {
fz->fz_next = table- >fn_zone_list;
table->fn_zone_list = fz;
} else {
fz->fz_next = table->fn_zones [i]->fz_next;
table->fn_zones[i]->fz_next = fz;
}
table->fn_zones[z] = fz;
這裡的fn_hash是數組與鏈表的結合體,看下fn_hash定義
struct fn_hash {
struct fn_zone *fn_zones [33];
struct fn_zone *fn_zone_list;
};
fn_hash包含33數組元素,每個元素存放一定掩碼長度的 fn_zone,其中fn_zone[i]存儲掩碼長度為i。而fn_zone通過內部屬性fz_next又彼此串連起來,形成單向鏈表,其中 fn_zone_list可以看作鏈表頭,而這裡鏈表的組織順序是倒序的,即從掩碼長到短。
到這裡,fz_hash所 分配的哈希表還沒有插入內容,這部分為fib_insert_node()完成。
inet_rtm_newroute() -> fib_table_insert() -> fib_insert_node() [net/ipv4/fib_hash.c]
這裡f是fib_node,可以理解為具有相同網絡地址的路由項集合。根 據fn_key(網絡地址)和fz(掩碼長度)來計算hash值,決定將f插入fz_hash的哪個項。
struct hlist_head *head = &fz->fz_hash[fn_hash(f->fn_key, fz)];
hlist_add_head(&f->fn_hash, head);
}
如 何fib_node還不存在,則會創建它,這裡的kmem_cache_zalloc()其實就是內存分配
new_f = kmem_cache_zalloc (fn_hash_kmem, GFP_KERNEL);
if (new_f == NULL)
goto out;
INIT_HLIST_NODE(&new_f- >fn_hash);
INIT_LIST_HEAD(&new_f->fn_alias);
new_f->fn_key = key;
f = new_f;
路由表最後一層是fib_info,具體的路由信息都存儲在此,它由 fib_create_info()創建。
首先為fib_info分配空間,由於fib_info的最後一個屬性是struct fib_nh fib_nh[0],因此 大小是fib_info + nhs * fib_nh,這裡的fib_nh代表了下一跳(next hop)的信息,nhs代表了下一跳的數目,一般情況下nhs=1 ,除非配置了支持多路徑。
fi = kzalloc(sizeof(*fi)+nhs*sizeof(struct fib_nh), GFP_KERNEL);
設置fi的相 關屬性
fi->fib_net = hold_net(net);
fi->fib_protocol = cfg->fc_protocol;
fi- >fib_flags = cfg->fc_flags;
fi->fib_priority = cfg->fc_priority;
fi->fib_prefsrc = cfg->fc_prefsrc;
fi->fib_nhs = nhs;
使fi後面所有的nh->nh_parent指向fi,設置後如圖所示
change_nexthops(fi) {
nexthop_nh->nh_parent = fi;
} endfor_nexthops(fi)
設置fib_nh的屬性, 這裡僅展示了單一路徑的情況:
struct fib_nh *nh = fi->fib_nh;
nh->nh_oif = cfg- >fc_oif;
nh->nh_gw = cfg->fc_gw;
nh->nh_flags = cfg->fc_flags;
然後,再根據cfg ->fc_scope值來設置nh的其余屬性。如果scope是RT_SCOPE_HOST,則設置下一跳scope為RT_SCOPE_NOWHERE
if (cfg- >fc_scope == RT_SCOPE_HOST) {
struct fib_nh *nh = fi->fib_nh;
nh->nh_scope = RT_SCOPE_NOWHERE;
nh->nh_dev = dev_get_by_index(net, fi->fib_nh->nh_oif);
}
如果scope 是RT_SCOPE_LINK或RT_SCOPE_UNIVERSE,則設置下跳
change_nexthops(fi) {
if ((err = fib_check_nh(cfg, fi, nexthop_nh)) != 0)
goto failure;
} endfor_nexthops(fi)
最後,將fi鏈入鏈表中,這裡要注意的 是所有的fib_info(只要創建了的)都會加入fib_info_hash中,如果路由項使用了優先地址屬性,還會加入fib_info_laddrhash 中。
hlist_add_head(&fi->fib_hash,
&fib_info_hash[fib_info_hashfn(fi)]);
if (fi- >fib_prefsrc) {
struct hlist_head *head;
head = &fib_info_laddrhash[fib_laddr_hashfn(fi- >fib_prefsrc)];
hlist_add_head(&fi->fib_lhash, head);
}
無論fib_info在路由表中位於哪 個掩碼、哪個網段結構下,都與fib_info_hash和fib_info_laddrhash無關,這兩個哈希表與路由表獨立,主要是用於加速路由 信息fib_info的查找。哈希表的大小為fib_hash_size,當超過這個限制時,fib_hash_size * 2(如果哈希函數夠好,每個 bucket都有一個fib_info)。fib_info在哈希表的圖示如下:
由於路由表信息也可能要以設備dev為鍵值搜索,因此還存在fib_info_devhash哈希表,用於存儲nh的設置dev->ifindex 。
change_nexthops(fi) {
hash = fib_devindex_hashfn(nexthop_nh->nh_dev->ifindex);
head = &fib_info_devhash[hash];
hlist_add_head(&nexthop_nh->nh_hash, head);
} endfor_nexthops (fi)
上面講過了路由表各個部分的創建,現在來看下它們是如何一起工作的,在fib_table_insert() [net/ipv4/fib_hash.c]完成整個的路由表創建過程。下面來看下fib_table_insert()函數:
從fn_zones中取出掩碼長度 為fc_dst_len的項,如果該項不存在,則創建它[fn_zone的創建前面已經講過]。
fz = table->fn_zones[cfg- >fc_dst_len];
if (!fz && !(fz = fn_new_zone(table, cfg->fc_dst_len)))
return - ENOBUFS;
然後創建fib_info結構,[前面已經講過]
fi = fib_create_info(cfg);
然後在掩碼長度相同項 裡查找指定網絡地址key(如145.222.33.0/24),查找的結果如圖所示
f = fib_find_node(fz, key);
如果不存在該網絡地 址項,則創建相應的fib_node,並加入到鏈表fz_hash中
if (!f) {
new_f = kmem_cache_zalloc(fn_hash_kmem, GFP_KERNEL);
if (new_f == NULL)
goto out;
INIT_HLIST_NODE(&new_f- >fn_hash);
INIT_LIST_HEAD(&new_f->fn_alias);
new_f->fn_key = key;
f = new_f;
}
……
fib_insert_node(fz, new_f);
如果存在該網絡地址項,則在fib_node的屬性 fn_alias中以tos和fi->fib_priority作為鍵值查找。一個fib_node可以有多個fib_alias相對應,這些fib_alias以鏈表形式 存在,並按tos並從大到小的順序排列。因此,fib_find_alias查找到的是第一個fib_alias->tos不大於tos的fib_alias項。
fa = fib_find_alias(&f->fn_alias, tos, fi->fib_priority);
如果查找到的fa與與要插入的路由 項完全相同,則按照設置的標置位進行操作,NLM_F_REPLACE則替換掉舊的,NLM_F_APPEND添加在後面。
設置要插入的 fib_alias的屬性,包括最重要的fib_alias->fa_info設置為fi
new_fa->fa_info = fi;
new_fa- >fa_tos = tos;
new_fa->fa_type = cfg->fc_type;
new_fa->fa_scope = cfg- >fc_scope;
new_fa->fa_state = 0;
如果沒有要插入路由的網絡地址項fib_node,則之前已經創建了新的 ,現在將它插入到路由表中fib_insert_node();然後將new_fa鏈入到fib_node->fn_alias中
if (new_f)
fib_insert_node(fz, new_f);
list_add_tail(&new_fa->fa_list,
(fa ? &fa->fa_list : &f->fn_alias));
最後,由於新插入的路由表項,會發出通告,告知所以加入RTNLGRP_IPV4_ROUTE組的成員, 這個功能可以在linux中使用”ip route monitor”來測試。最終的路由表如圖所示:
rtmsg_fib(RTM_NEWROUTE, key, new_fa, cfg->fc_dst_len, tb->tb_id, &cfg->fc_nlinfo, 0);
至此,就完成了路由 表項的插入,加上之前的路由表的初始化,整個路由表的創建過程就講解完了,小小總結一下:
路由表的查找效率是第 一位的,因此內核在實現時使用了多級索引來進行加速
第一級:fn_zone 按不同掩碼長度分類(如/5和/24)
第二 級:fib_node 按不同網絡地址分類(如124.44.33.0/24)
第三級:fib_info 下一跳路由信息