Linux内核分析 - 网络[十五]:陆由表[再议]

来源:互联网 发布:手游市场数据分析报告 编辑:程序博客网 时间:2024/05/17 03:03

内核版本: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才能保证后续报文使用该条目。

原创粉丝点击