內核版本:2.6.34
陸由表作為三層協議的核心數據結構,理解它是至關重要的。前面已經分析過路由表,有興趣的可以參考:
第一篇:路由表 http://blog.csdn.net/qy532846454/article/details /6423496
分析了路由表的基本數據結構和基本操作
第二篇:路由表使用 http://blog.csdn.net/qy532846454/article/details /6726171
分析了路由表的基本使用
這次將以更實際的例子來分析過程中路由表的使用情況,注意下文都是對路由緩存表的描述,因為路由表在配置完網卡地址後就不會再改變了(除非人為的去改動),測試環境如下圖:
兩台主機Host1與Host2,分別配置了IP地址192.168.1.1與192.168.1.2,兩台主機間用網線直連。在兩台主機上分別執行 如下操作:
1. 在Host1上ping主機Host2
2. 在Host2上ping主機 Host1
很簡單常的兩台主機互ping的例子,下面來分析這過程中路由表的變化,准備說是路由緩存 的變化。首先,路由緩存會存在幾個條目?答案不是2條而是3條,這點很關鍵,具體可以通過/proc/net/rt_cache來查看路由緩 存表,下圖是執行上述操作後得到的結果:
brcm0.1是Host主機上的網卡設備,等同於常用的eth0,lo是環路設備。對結果稍加分析 ,可以發現,條目1和條目2是完全一樣的,除了計數的Use稍有差別,存在這種情況的原因是緩存表是以Hash表的形式存儲的, 盡管兩者內容相同,在實際插入時使用的鍵值是不同的,下面以Host2主機的路由緩存表為視角,針對互ping的過程進行逐一分 析。
假設brcm0.1設備的index = 2
步驟0:初始時陸由緩存為空
步驟1:主機Host1 ping 主機Host2
Host2收到來自Host1的echo報文(dst = 192.168.1.2, src = 192.168.1.1)
在報文進入IP層後會查詢路由表,以確定報文的接收方式,相應調用流程:
ip_route_input() -> ip_route_input_slow()
在ip_route_input()中查詢路由緩存,使用的 鍵值是[192.168.1.2, 192.168.1.1, 2, id],由於緩存表為空,查詢失敗,繼續走ip_route_input_slow()來創建並插入新的緩 存項。
hash = rt_hash(daddr, saddr, iif, rt_genid(net));
在 ip_route_input_slow()中查詢路由表,因為發往本機,在會LOCAL表中匹配192.168.1.2條目,查詢結果res.type==RTN_LOCAL。
if ((err = fib_lookup(net, &fl, &res)) != 0) { if (!IN_DEV_FORWARD(in_dev)) goto e_hostunreach; goto no_route; }
然後根據res.type跳轉到local_input代碼段,創建新的路由緩存項,並插入陸由緩存。
rth = dst_alloc(&ipv4_dst_ops); …… rth->u.dst.dev = net->loopback_dev; rth->rt_dst = daddr; rth->rt_src = saddr; rth->rt_gateway = daddr; rth->rt_spec_dst = spec_dst; (spec_dst=daddr) …… hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net)); err = rt_intern_hash(hash, rth, NULL, skb, fl.iif);
因此插入的第一條緩存信息如下:
Key = [dst = 192.168.1.2 src = 192.168.1.1 idx = 2 id = id]
Value = [Iface = lo dst = 192.168.1.2 src = 192.168.1.1 idx = 2 id = id ……]
步驟2:主機Host2 發送 echo reply報文給主機 Host1 (dst = 192.168.1.1 src = 192.168.1.2)
步驟2是緊接著步驟1的 ,Host2在收到echo報文後會立即回復echo reply報文,相應調用流程:
icmp_reply() -> ip_route_output_key() -> ip_route_output_flow() -> __ip_route_output_key() -> ip_route_output_slow() - > ip_mkroute_output() -> __mkroute_output()
在icmp_reply()中生成稍後路由查找中的 關鍵數據flowi,可以看作查找的鍵值,由於是回復已收到的報文,因此目的與源IP地址者是已知的,下面結構中 daddr=192.168.1.1,saddr=192.168.1.2。
struct flowi fl = { .nl_u = { .ip4_u = { .daddr = daddr, .saddr = rt->rt_spec_dst, .tos = RT_TOS(ip_hdr(skb)->tos) } }, .proto = IPPROTO_ICMP };
在__ip_route_output_key()時會查詢路由緩存表,查詢的鍵值是[192.168.1.1, 192.168.1.2, 0, id],由於此時路由緩存中只有一條剛剛插入的從192.168.1.1->192.168.1.2的緩存項,因而查詢失敗,繼 續走ip_route_output_slow()來創建並插入新的緩存項。
hash = rt_hash(flp->fl4_dst, flp->fl4_src, flp- >oif, rt_genid(net));
在ip_route_input_slow()中查詢路由表,因為在同一網段, 在會MAIN表中匹配192.168.1.0/24條目,查詢結果res.type==RTN_UNICAST。
if (fib_lookup(net, &fl, &res)) { ….. }
然後調用__mkroute_output()來生成新的路由緩存,信息如下:
rth ->u.dst.dev = dev_out; rth->rt_dst = fl->fl4_dst; rth->rt_src = fl->fl4_src; rth->rt_gateway = fl->fl4_dst; rth->rt_spec_dst= fl->fl4_src; rth->fl.oif = oldflp->oif; (oldflp->oif為0)
插入路由緩存表時使用的鍵值是:
hash = rt_hash(oldflp->fl4_dst, oldflp->fl4_src, oldflp->oif, rt_genid(dev_net(dev_out)));
這條語句很關鍵,緩存的存儲形式是hash表,除了生成緩存信息外,還要有相應的鍵值,這句的hash就是 產生的鍵值,可以看到,它是由(dst, src, oif, id)四元組生成的,dst和src很好理解,id對於net來說是定值,oif則是關鍵 ,注意這裡用的是oldflp->oif(它的值為0),盡管路由緩存對應的出接口設備是dev_out。所以,第二條緩存信息的如下:
Key = [dst = 192.168.1.1 src = 192.168.1.2 idx = 0 id = id]
Value = [Iface = brcm0.1 dst = 192.168.1.1 src = 192.168.1.2 idx = 2 id = id ……]
步驟3:主機Host2 ping 主機Host1
Host2向Host1發送echo報文(dst = 192.168.1.1, src = 192.168.1.2)
Host2主動發送echo報文,使用SOCK_RAW與IPPROTO_ICMP組合的套接字,相應調用流 程:
raw_sendmsg() -> ip_route_output_flow() -> __ip_route_output_key() -> ip_route_output_slow() -> ip_mkroute_output() -> __mkroute_output()
在raw_sendmsg()中生成稍後路由查找 中的關鍵數據flowi,可以看作查找的鍵值,由於是主動發送的報文,源IP地址者還是未知的,因為主機可能是多接口的,在查 詢完路由表後才能得到要走的設備接口和相應的源IP地址。下面結構中daddr=192.168.1.1,saddr=0。
struct flowi fl = { .oif = ipc.oif, .mark = sk->sk_mark, .nl_u = { .ip4_u = { .daddr = daddr, .saddr = saddr, .tos = tos } }, .proto = inet->hdrincl ? IPPROTO_RAW : sk->sk_protocol, };
在__ip_route_output_key()時會查詢路由緩存表,查詢的鍵值是[192.168.1.1, 0, 0, id],盡管此時路由緩存 中剛剛插入了192.168.1.2->192.168.1.1的條目,但由於兩者的鍵值不同,因而查詢依舊失敗,繼續走 ip_route_output_slow()來創建並插入新的緩存項。
hash = rt_hash(flp->fl4_dst, flp->fl4_src, flp- >oif, rt_genid(net));
與Host2回復Host1的echo報文相比,除了進入函數 不同(前者為icmp_reply,後者為raw_sendmsg),後續調用流程是完全相同的,導致最終路由緩存不同(准確說是鍵值)是因為初 始時flowi不同。
此處,raw_sendmsg()中,flowi的初始值:dst = 192.168.1.1, src = 0, oif = 0
對比icmp_reply()中,flowi的初始值:dst = 192.168.1.1, src = 192.168.1.2, oif = 0
在上述調用流程中,在__ip_route_output_key()中查找路由緩存,盡管此時路由緩存有從 192.168.1.2到192.168.1.1的緩存項,但它的鍵值與此次查找的鍵值[192.168.1.1, 192.168.1.2, 0],從下表可以明顯看出:
由於查找失敗,生成新的路由緩存項並插入路由緩存表,注意在ip_route_output_slow()中查找完路由表 後,設置了緩存的src。
if (!fl.fl4_src) fl.fl4_src = FIB_RES_PREFSRC(res);
因此插入的第三條緩存信息如下,它與第二條緩存完成相同,區別在於鍵值不 同:
Key = [dst = 192.168.1.1 src = 0 idx = 0 id = id]
Value = [Iface = brcm0.1 dst = 192.168.1.1 src = 192.168.1.2 idx = 2 id = id ……]
最終,路由緩存表如下:
第三條緩存條目鍵值使用src=0, idx=0的原因是當主機要發送報文給192.168.1.1的主機時,直到IP層路由查詢前,它都 無法知道該使用的接口地址(如果沒有綁定的話),而路由緩存的查找發生在路由查詢之前,所以src=0,idx=0才能保證後續報文 使用該條目。