NETDEV 協議 二

路由表

    在內核中存在路由表fib_table_hash和路由緩存表rt_hash_table。路由緩存表主要是爲了加速路由的查找,每次路由查詢都會先查找路由緩存,再查找路由表。這和cache是一個道理,緩存存儲最近使用過的路由項,容量小,查找快速;路由表存儲所有路由項,容量大,查找慢。

首先,應該先了解路由表的意義,下面是route命令查看到的路由表:

Destination

Netmask

Gateway

Flags

Interface

Metric

169.254.0.0

255.255.0.0

*

U

eth0

1

192.168.123.0

255.255.255.0

*

U

eth0

1

default

0.0.0.0

192.168.123.254

UG

eth0

1

    一條路由其實就是告知主機要到達一個目的地址,下一跳應該走哪裏。比如發往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_LINKRT_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表示了分配多少個路由表,一般情況下至少有兩個 LOCALMAIN。注意這裏僅僅是表頭的空間分配,還沒有真正分配路由表空間。

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);

然後將localmain錶鏈入之前的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則標識了功能,255LOCAL表,254MAIN表。

 

關於這裏的struct fn_hash,它表示了不同子網掩碼長度的hash[fn_zone],對於ipv4,從0~3233個。而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_divisor只需設爲1;其它設爲16;這裏要提到fz_divisor的作用,fz->fz_hash並不是個單鏈表,而是一個哈希表,而哈希表的大小就是fz_divisor

if (z) {

         fz->fz_divisor = 16;

} else {

         fz->fz_divisor = 1;

}

fz_hashmask實際是用於求餘數的,當算出hash值,再hash & fz_hashmask就得出了在哈希表的位置;而fz_hash就是下一層的哈希表了,前面已經提過路由表被多組分層了,這裏fz_hash就是根據fz_divisor大小來創建的;fz_order就是子網掩碼長度;fz_mask就是子網掩碼。

fz->fz_hashmask = (fz->fz_divisor - 1);

fz->fz_hash = fz_hash_alloc(fz->fz_divisor);

fz->fz_order = z;

fz->fz_mask = inet_make_mask(z);

 

從子網長度大於新添加fzfn_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]

這裏ffib_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的其餘屬性。如果scopeRT_SCOPE_HOST,則設置下一跳scopeRT_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_hashfib_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中以tosfi->fib_priority作爲鍵值查找。一個fib_node可以有多個fib_alias相對應,這些fib_alias以鏈表形式存在,並按tos並從大到小的順序排列。因此,fib_find_alias查找到的是第一個fib_alias->tos不大於tosfib_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     下一跳路由信息

 

   內核版本:2.6.34
      前篇路由表
http://blog.csdn.net/qy532846454/article/details/6423496說明了路由表的結構及路由表的創建。下面是一些路由表的使用的細枝末節,作補充說明。
      路由可以分爲兩部分:路由緩存(rt_hash_table)和路由表()
      路由緩存顧名思義就是加速路由查找的,路由緩存的插入是由內核控制的,而非人爲的插入,與之相對比的是路由表是人爲插入的,而非內核插入的。在內核中,路由緩存組織成rt_hash_table的結構。

      下面是一段IP層協議的代碼段[net/ipv4/route.c],傳入IP層的協議在查找路由時先在路由緩存中查找,如果已存在,則skb_dst_set(skb, &rth->u.dst)並返回;否則在路由表中查詢。

 

 
hash = rt_hash(daddr, saddr, iif, rt_genid(net));

rcu_read_lock();
for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
     rth = rcu_dereference(rth->u.dst.rt_next)) {
	if (((rth->fl.fl4_dst ^ daddr) |
	     (rth->fl.fl4_src ^ saddr) |
	     (rth->fl.iif ^ iif) |
	     rth->fl.oif |
	     (rth->fl.fl4_tos ^ tos)) == 0 &&
	    rth->fl.mark == skb->mark &&
	    net_eq(dev_net(rth->u.dst.dev), net) &&
	    !rt_is_expired(rth)) {
		dst_use(&rth->u.dst, jiffies);
		RT_CACHE_STAT_INC(in_hit);
		rcu_read_unlock();
		skb_dst_set(skb, &rth->u.dst);
		return 0;
	}
	RT_CACHE_STAT_INC(in_hlist_search);
}
rcu_read_unlock();

        在ip_route_input()中查詢完陸由緩存後會處理組播地址,如果是組播地址,則下面判斷會成功:ipv4_is_multicast(daddr)。
然後執行ip_route_input_mc(),它的主要作用就是生成路由緩存項rth,並插入緩存。rth的生成與初始化只給出了input函數的,其它略去了,可以看出組播報文會通過ip_local_deliver()繼續向上傳遞。

rth->u.dst.input= ip_local_deliver;
hash = rt_hash(daddr, saddr, dev->ifindex, rt_genid(dev_net(dev)));
return rt_intern_hash(hash, rth, NULL, skb, dev->ifindex);

      路由表又可以分爲兩個:RT_TABLE_LOCAL和RT_TABLE_MAIN
        RT_TABLE_LOCAL存儲目的地址是本機的路由表項,這些目的地址就是爲各個網卡配置的IP地址;
        RT_TABLE_MAIN存儲到其它主機的路由表項;
      顯然,RT_TABLE_MAIN路由表只有當主機作爲路由器時纔有作用,一般主機該表是空的,因爲主機不具有轉發數據包的功能。RT_TABLE_LOCAL對主機就足夠了,爲各個網卡配置的IP地址都會加入RT_TABLE_LOCAL中,如爲eth1配置了1.2.3.4的地址,則RT_TABLE_LOCAL中會存在1.2.3.4的路由項。只有本地的網卡地址會被加入,比如lo、eth1。IP模塊在初始化時ip_init() -> ip_rt_init() - > ip_fib_init()會註冊notifier機制,當爲網卡地址配置時會執行fib_netdev_notifier和fib_inetaddr_notifier,使更改反映到RT_TABLE_LOCAL中。

	register_netdevice_notifier(&fib_netdev_notifier);
	register_inetaddr_notifier(&fib_inetaddr_notifier);

          而當在路由緩存中沒有查找到緩存項時,會進行路由表查詢,還是以IP層協議中的代碼段爲例[net/ipv4/route.c],fib_lookup()會在MAIN和LOCAL兩張表中進行查找。

if ((err = fib_lookup(net, &fl, &res)) != 0) {
	if (!IN_DEV_FORWARD(in_dev))
		goto e_hostunreach;
	goto no_route;
}

        如果主機配置成了支持轉發,則無論在路由表中找到與否,都會生成這次查詢的一個緩存,包括源IP、目的IP、接收的網卡,插入路由緩存中:

hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net));
err = rt_intern_hash(hash, rth, NULL, skb, fl.iif);

      不同的是,如果在路由表中查詢失敗,即數據包不是發往本機,也不能被本機轉發,則會設置插入路由緩存的緩存項u.dst.input=ip_error,而u.dst.input即爲IP層處理完後向上傳遞的函數,而ip_error()會丟棄數據包,被髮送相應的ICMP錯誤報文。不在路由表中的路由項也要插入路由緩存,這可以看作路由學習功能,下次就可以直接在路由緩存中找到。

 
rth->u.dst.input= ip_error;
rth->u.dst.error= -err;
rth->rt_flags 	&= ~RTCF_LOCAL;

      但如果主機不支持轉發,即沒有路由功能,則只有在找到時纔會添加路由緩存項,都不會生成路由緩存項。這是因爲在LOCAL表中沒有找到,表明數據包不是發往本機的,此時緩存這樣的路由項對於主機的數據包傳輸沒有一點意義。它只需要知道哪些數據包是發給它的,其餘的一律不管!

       路由查詢整合起來,就是由ip_route_input()引入,然後依次進行路由緩存和路由表查詢,並對路由緩存進行更新。路由緩存在每個數據包到來時都可能發生更新,但路由表則不一樣,只能通過RTM機制更新,LOCAL表是在網卡配置時更新的,MAIN表則是由人工插入的(inet_rtm_newroute)。
       ip_route_input()
         - 路由緩存查詢
         - 路由表查詢:ip_route_input_slow() -> fib_lookup()

發佈了0 篇原創文章 · 獲贊 7 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章