歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[十五]:陸由表[再議]

Linux內核分析 - 網絡[十五]:陸由表[再議]

日期:2017/3/3 16:38:01   编辑:Linux內核

內核版本: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才能保證後續報文 使用該條目。

Copyright © Linux教程網 All Rights Reserved