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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章