内核版本: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()来创建并插入新的缓存项。

[cpp] view plaincopy
  1. hash = rt_hash(daddr, saddr, iif, rt_genid(net));

在ip_route_input_slow()中查询路由表,因为发往本机,在会LOCAL表中匹配192.168.1.2条目,查询结果res.type==RTN_LOCAL。

[cpp] view plaincopy
  1. if ((err = fib_lookup(net, &fl, &res)) != 0) {
  2. if (!IN_DEV_FORWARD(in_dev))
  3. goto e_hostunreach;
  4. goto no_route;
  5. }

然后根据res.type跳转到local_input代码段,创建新的路由缓存项,并插入陆由缓存。

[cpp] view plaincopy
  1. rth = dst_alloc(&ipv4_dst_ops);
  2. ……
  3. rth->u.dst.dev = net->loopback_dev;
  4. rth->rt_dst = daddr;
  5. rth->rt_src = saddr;
  6. rth->rt_gateway = daddr;
  7. rth->rt_spec_dst = spec_dst; (spec_dst=daddr)
  8. ……
  9. hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net));
  10. 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。

[cpp] view plaincopy
  1. if (fib_lookup(net, &fl, &res)) {
  2. …..
  3. }

然后调用__mkroute_output()来生成新的路由缓存,信息如下:

[cpp] view plaincopy
  1. rth->u.dst.dev = dev_out;
  2. rth->rt_dst = fl->fl4_dst;
  3. rth->rt_src = fl->fl4_src;
  4. rth->rt_gateway = fl->fl4_dst;
  5. rth->rt_spec_dst= fl->fl4_src;
  6. 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。

[cpp] view plaincopy
  1. if (!fl.fl4_src)
  2. 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才能保证后续报文使用该条目。

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

  1. Linux内核分析 - 网络[十六]:TCP三次握手

    内核:2.6.34       TCP是应用最广泛的传输层协议,其提供了面向连接的.可靠的字节流服务,但也正是因为这些特性,使得TCP较之UDP异常复杂,还是分两部分[创建与使用]来进行分析.这篇主要 ...

  2. Linux内核分析 - 网络[十]:ARP杂谈

    内核版本:2.6.34 杂谈一:重复地址检测 Linux协议栈中处理重复地址检测报文的是arp_process()中的一段代码,RFC2131是DHCP的草案,相应的sip==0是DHCP服务器用来检 ...

  3. Linux内核分析 - 网络[十二]:UDP模块 - socket

    内核版本:2.6.34 这部分内容在于说明socket创建后如何被内核协议栈访问到,只关注两个问题:sock何时插入内核表的,sock如何被内核访问的.对于核心的sock的插入.查找函数都给出了流程图 ...

  4. Linux内核分析 - 网络[十四]:IP选项

    内核版本:2.6.34       在发送报文时,可以调用函数setsockopt()来设置相应的选项,本文主要分析IP选项的生成,发送以及接收所执行的流程,选取了LSRR为例子进行说明,主要分为选项 ...

  5. Linux内核分析 - 网络[十二]:UDP模块 - 收发

    内核版本:2.6.34 UDP报文接收        UDP报文的接收可以分为两个部分:协议栈收到udp报文,插入相应队列中:用户调用recvfrom()或recv()系统调用从队列中取出报文,这里的 ...

  6. linux内核分析 网络九,“Linux内核分析”实验报告(九)

    一 Linux内核分析博客简介及其索引 本次实验简单的分析了计算机如何进行工作,并通过简单的汇编实例进行解释分析 在本次实验中 通过听老师的视频分析,和自己的学习,初步了解了进程切换的原理.操作系统通 ...

  7. Linux内核分析 - 网络[九]:邻居表

    内核版本:2.6.34 这部分的重点是三个核心的数据结构-邻居表.邻居缓存.代理邻居表,以及NUD状态转移图. 总的来说,要成功添加一条邻居表项,需要满足两个条件:1. 本机使用该表项:2. 对方主机 ...

  8. Linux内核分析 - 网络[五]:vlan协议-802.1q

    内核版本:2.6.34 802.1q 1. 注册vlan网络系统子空间, [cpp] view plaincopy err = register_pernet_subsys(&vlan_net ...

  9. Linux内核分析 - 网络[六]:网桥

    看完了路由表,重新回到netif_receive_skb ()函数,在提交给上层协议处理前,会执行下面一句,这就是网桥的相关操作,也是这篇要讲解的内容. [cpp] view plaincopy sk ...

最新文章

  1. 微软系统遭*** 谴责人为失误
  2. jQuery Ajax – Servlets集成:构建完整的应用程序
  3. csr8670 修改key_CSR8670 DFU user guide
  4. Linux监控工具介绍系列——smem
  5. Python | 在列表中指定索引处添加元素的程序
  6. UIUC CS241 系统编程中文讲义校对活动 | ApacheCN
  7. DOM的​getElementById() 和 getElementsByTagName() 方法
  8. 利用matlab求解二维水动力,[转载]MIKE系列软件之二维河口与海岸模拟软件MIKE 21...
  9. 目标检测、语义分割性能指标
  10. RJ45墙上网线插座的线序与接法
  11. 看漫画学python pdf下载_看漫画学Python 有趣、有料、好玩、好用(全彩版)
  12. 光盘文件格式-udf、iso9660、Joliet、Romeo
  13. python属于什么类型的选修课_河北大学python选修课00次作业
  14. 前端-HTML的架构和常见标签
  15. 墙裂推荐免费的试题搜索网站
  16. 2022(一等奖)B2548基于群体智慧的中国人口多情景时空仿真建模与预测
  17. mysql唯一key_MySQL唯一约束(UNIQUE KEY)
  18. Invertible Denoising Network: A Light Solution for Real Noise Removal
  19. Leetcode1710:卡车上的最大单元数
  20. 计算两个时间中的每个月的月初和月末

热门文章

  1. 鸿蒙硬件HI3861-INA226-电压测量(外挂方案)
  2. idea jdbc封装_IDEA 中 MyBatis还可以这么玩!!!
  3. pandas从dataframe中选择部分行、列
  4. 人脸识别dlib库 记录
  5. Emgu-WPF学习使用-阈值化
  6. 苹果机的时间格式转换为时间搓
  7. 设计模式三(工厂方法模式)学习笔记
  8. SNMP简单网络管理协议
  9. 一些html5和css3的一些常见面试题
  10. 效率源希捷硬盘远程维修案例-K9盘卡死引起的F级处理