您好,欢迎来到划驼旅游。
搜索
您的当前位置:首页understanding linux network internals 第7部分:路由协议

understanding linux network internals 第7部分:路由协议

来源:划驼旅游
Understanding Linux Network Internals 第七部分—— 路由

章 节 目 录

第30章 路由概念.........................................................................................30-1

30.1 路由器, 路由与路由表...................................................................................................30-1 30.1.1 非路由多接口主机(Nonrouting Multihomed Hosts)........................................30-2 30.1.2 各种路由配置.........................................................................................................30-3 30.1.3 本书这一部分要回答的问题.................................................................................30-4 30.2 路由的基本要素.............................................................................................................30-6 30.2.1 Scope.......................................................................................................................30-8 30.2.1.1 scope的使用....................................................................................................30-9 30.2.2 缺省网关...............................................................................................................30-10 30.2.3 定向广播(Directed Broadcasts).......................................................................30-10 30.2.4 主地址与第二地址...............................................................................................30-12 30.2.4.1 老式配置:aliasing接口...............................................................................30-13 30.2.4.2 aliasing设备与主/第二IP地址间的关系.......................................................30-14 30.3 路由表...........................................................................................................................30-14 30.3.1 特殊路由...............................................................................................................30-15 30.3.2 路由类型与动作...................................................................................................30-15 30.3.3 路由缓存...............................................................................................................30-15 30.3.4 路由表与路由缓存...............................................................................................30-16 30.3.5 路由缓存的垃圾回收...........................................................................................30-16 30.3.5.1 使路由缓存表项过期的事件举例................................................................30-17 30.3.5.2 合格的缓存受害者举例................................................................................30-17 30.4 查找...............................................................................................................................30-18 30.4.1 最长前缀匹配.......................................................................................................30-19 30.5 报文的接收与发送.......................................................................................................30-20

第31章 高级路由.........................................................................................31-1

31.1 策略路由背后的概念.....................................................................................................31-1 31.1.1 策略路由查找.........................................................................................................31-4 31.1.2 选择路由表.............................................................................................................31-5 31.2 多路径路由背后的概念.................................................................................................31-6 31.2.1 选择下一跳.............................................................................................................31-7 31.2.2 缓存支持多路径.....................................................................................................31-8 31.2.2.1 加权随机算法..................................................................................................31-8 31.2.2.2 设备轮转算法..................................................................................................31-9 31.2.3 基于流、基于连接和基于报文来分配流量.......................................................31-11 31.2.3.1 均衡算法........................................................................................................31-11 31.3 与其它内核子系统的交互...........................................................................................31-11 31.3.1 基于路由表的Classifier........................................................................................31-12 31.3.1.1 配置策略realms.............................................................................................31-13 31.3.1.2 配置路由realms.............................................................................................31-13 31.3.1.3 计算路由标签................................................................................................31-13 31.3.2 策略路由与基于防火墙的Classifier....................................................................31-15 31.4 路由协议守护进程.......................................................................................................31-15 31.5 Verbose监控.................................................................................................................31-17 章 节 目 录 Page i

Understanding Linux Network Internals 第七部分—— 路由

31.6 ICMP重定向消息........................................................................................................31-18 31.6.1 共享介质...............................................................................................................31-18 31.6.2 发送ICMP重定向消息.........................................................................................31-20 31.6.3 处理Ingress ICMP重定向消息.............................................................................31-21 31.7 反向路径过滤...............................................................................................................31-22

第32章 路由在LINUX中的实现...................................................................32-1

32.1 内核选项.........................................................................................................................32-1 32.1.1 基本选项.................................................................................................................32-2 32.1.2 高级选项.................................................................................................................32-2 32.1.3 最近废弃的选项.....................................................................................................32-3 32.2 主要的数据结构.............................................................................................................32-3 32.2.1 链表与哈希表.........................................................................................................32-6 32.3 路由与地址Scope...........................................................................................................32-7 32.3.1 路由Scope...............................................................................................................32-7 32.3.2 地址Scope...............................................................................................................32-7 32.3.3 路由scope与下一跳scope之间的关系...................................................................32-8 32.4 主IP地址与第二IP地址.................................................................................................32-9 32.5 常用的辅助程序与宏...................................................................................................32-10 32.6 全局锁...........................................................................................................................32-11 32.7 路由子系统初始化.......................................................................................................32-11 32.8 外部事件.......................................................................................................................32-13 32.8.1 辅助程序...............................................................................................................32-14 32.8.2 IP配置改变............................................................................................................32-16 32.8.2.1 添加一个IP地址............................................................................................32-17 32.8.2.2 删除一个IP地址............................................................................................32-20 32.8.3 设备状态改变.......................................................................................................32-22 32.8.3.1 对路由表的影响............................................................................................32-22 32.8.3.2 对策略数据库的影响....................................................................................32-23 32.8.3.3 对IP配置的影响............................................................................................32-23 32.9 与其它子系统的交互...................................................................................................32-24 32.9.1 Netlink通知...........................................................................................................32-24 32.9.2 策略路由与基于防火墙的Classifier....................................................................32-24 32.9.3 路由协议守护进程...............................................................................................32-24

第33章 路由缓存.........................................................................................33-1

33.1 路由缓存初始化.............................................................................................................33-1 33.2 哈希表组织.....................................................................................................................33-2 33.3 主要的缓存操作.............................................................................................................33-3 33.3.1 缓存锁.....................................................................................................................33-3 33.3.2 缓存表项的分配与引用计数.................................................................................33-3 33.3.3 添加元素到缓存中.................................................................................................33-3 33.3.4 将路由缓存绑定到ARP缓存..................................................................................33-5 33.3.5 缓存查找.................................................................................................................33-6 33.3.5.1 Ingress查找......................................................................................................33-7 33.3.5.2 Egress查找.......................................................................................................33-9 33.4 多路径缓存...................................................................................................................33-10 章 节 目 录 Page ii

Understanding Linux Network Internals 第七部分—— 路由

33.4.1 33.4.2 33.4.3 33.4.4 33.4.5 33.4.6 33.4.7 33.4.8

注册一个缓存算法...............................................................................................33-10 路由缓存与多路径之间的接口...........................................................................33-10 辅助程序...............................................................................................................33-11 算法之间的共同点...............................................................................................33-11 随机算法...............................................................................................................33-11 加权随机算法.......................................................................................................33-12 轮转算法...............................................................................................................33-12 设备轮转算法.......................................................................................................33-14

33.5 DST与调用协议之间的接口.......................................................................................33-14 33.5.1 IPsec转换与dst_entry的使用................................................................................33-16 33.5.2 外部事件...............................................................................................................33-17 33.6 Flush路由缓存.............................................................................................................33-18 33.7 垃圾回收.......................................................................................................................33-20 33.7.1 同步清理...............................................................................................................33-20 33.7.2 rt_garbage_collect函数.........................................................................................33-20 33.7.3 异步清理...............................................................................................................33-23 33.7.4 过期标准...............................................................................................................33-24 33.7.5 删除DST表项........................................................................................................33-24 33.7.6 调整和控制垃圾回收的变量...............................................................................33-27 33.8 Egress ICMP重定向限速............................................................................................33-28

第34章 路由表............................................................................................34-1

34.1 路由哈希表的组织.........................................................................................................34-1 34.1.1 Per-Netmask哈希表的组织....................................................................................34-1 34.1.1.1 哈希表组织的基本结构..................................................................................34-1 34.1.1.2 Per-Netmask哈希表容量的动态变化.............................................................34-4 34.1.2 fib_info结构的组织................................................................................................34-4 34.1.2.1 全局哈希表容量的动态变化..........................................................................34-4 34.1.3 下一跳路由器结构的组织.....................................................................................34-5 34.1.4 两个缺省路由表:ip_fib_main_table与ip_fib_local_table...................................34-6 34.2 路由表初始化.................................................................................................................34-6 34.3 路由表项的添加与删除.................................................................................................34-6 34.3.1 添加一条路由表项.................................................................................................34-7 34.3.2 删除一条路由表项.................................................................................................34-7 34.3.3 垃圾回收.................................................................................................................34-9 34.4 策略路由及其对路由表定义的影响...........................................................................34-10 34.4.1 变量与结构定义...................................................................................................34-10 34.4.2 每个函数有两个定义...........................................................................................34-11

第35章 路由查找.........................................................................................35-1

35.1 查找函数的High-Level View........................................................................................35-1 35.2 辅助程序.........................................................................................................................35-2 35.3 路由表查找:fn_hash_lookup.....................................................................................35-2 35.3.1 Semantic匹配其它标准(Subsidiary Criteria)....................................................35-4 35.3.1.1 拒绝路由的标准..............................................................................................35-4 35.3.1.2 fib_semantic_match函数的返回值.................................................................35-7 35.4 fib_lookup函数..............................................................................................................35-7 章 节 目 录 Page iii

Understanding Linux Network Internals 第七部分—— 路由

35.5 为接收和发送设置函数.................................................................................................35-8 35.5.1 针对Ingress流量的函数指针初始化......................................................................35-9 35.5.2 针对Engress流量的函数指针初始化...................................................................35-10 35.5.3 特殊情况...............................................................................................................35-10 35.6 输入和输出选路程序的一般结构...............................................................................35-10 35.7 输入选路.......................................................................................................................35-11 35.7.1 创建一条缓存表项...............................................................................................35-14 35.7.2 首选源地址选择...................................................................................................35-15 35.7.3 送往本地...............................................................................................................35-16 35.7.4 转发.......................................................................................................................35-17 35.7.5 选路失败...............................................................................................................35-18 35.8 输出选路.......................................................................................................................35-20 35.8.1 搜索Key初始化.....................................................................................................35-22 35.8.2 选择源IP地址........................................................................................................35-23 35.8.3 送往本地...............................................................................................................35-24 35.8.4 发送给其他主机...................................................................................................35-25 35.8.5 多路径选择与缺省网关选择之间的交互...........................................................35-25 35.8.6 缺省网关选择.......................................................................................................35-26 35.8.7 fn_hash_select_default函数..................................................................................35-27 35.9 多路径对下一跳选择的影响.......................................................................................35-27 35.9.1 多路径缓存...........................................................................................................35-29 35.10 策略路由...................................................................................................................35-29 35.10.1 策略路由时的fib_lookup..................................................................................35-29 35.10.2 策略路由时选择缺省网关................................................................................35-30 35.11

基于源站寻路...........................................................................................................35-32

35.12 策略路由与基于路由表的Classifier.......................................................................35-32 35.12.1 存储Realms.......................................................................................................35-32 35.12.2 辅助程序...........................................................................................................35-33 35.12.3 计算路由标签...................................................................................................35-34

第36章 相关主题.........................................................................................36-1

36.1 用户空间配置工具.........................................................................................................36-1 36.1.1 利用IPROUTE2包配置路由..................................................................................36-2 36.1.1.1 IPROUTE2用户命令与内核函数之间的对应关系.......................................36-2 36.1.1.2 inet_rtm_newroute和inet_rtm_delroute函数...................................................36-3 36.1.2 利用net-tools包配置路由.......................................................................................36-4 36.1.3 路由变化通知.........................................................................................................36-4 36.1.4 内核插入路由项:fib_magic函数.........................................................................36-5 36.2 统计.................................................................................................................................36-5 36.3 通过/proc文件系统调整路由........................................................................................36-5 36.3.1 /proc/sys/net/ipv4目录............................................................................................36-7 36.3.2 /proc/sys/net/ipv4/route目录...................................................................................36-7 36.3.3 /proc/sys/net/ipv4/conf目录....................................................................................36-9 36.3.3.1 特殊子目录......................................................................................................36-9 36.3.3.2 特殊子目录的使用........................................................................................36-10 36.3.3.3 文件描述........................................................................................................36-10 36.3.4 /proc/net目录和/proc/net/stat目录........................................................................36-11 36.4 使能和禁止转发...........................................................................................................36-12 章 节 目 录 Page iv

Understanding Linux Network Internals 第七部分—— 路由

36.5 在本书路由部分中重要的数据结构...........................................................................36-14 36.5.1 fib_table结构.........................................................................................................36-14 36.5.2 fn_zone结构..........................................................................................................36-15 36.5.3 fib_node结构.........................................................................................................36-16 36.5.4 fib_alias结构.........................................................................................................36-16 36.5.5 fib_info结构..........................................................................................................36-17 36.5.6 fib_nh结构.............................................................................................................36-20 36.5.7 fib_rule结构..........................................................................................................36-21 36.5.8 fib_result结构........................................................................................................36-23 36.5.9 rtable结构..............................................................................................................36-23 36.5.10 dst_entry结构....................................................................................................36-26 36.5.11 dst_ops结构.......................................................................................................36-28 36.5.12 flowi结构...........................................................................................................36-29 36.5.13 rt_cache_stat结构..............................................................................................36-29 36.5.14 ip_mp_alg_ops结构...........................................................................................36-31 36.6 在本书路由部分中重要的函数和变量.......................................................................36-31 36.7 在本书路由部分中重要的文件和目录.......................................................................36-35

章 节 目 录 Page v

Understanding Linux Network Internals 第七部分—— 路由

图 目 录

图 30-1 路由子系统与其它主要网络子系统之间的关系...................................................30-1 图 30-2 路由器和路由表的基本例子...................................................................................30-2 图 30-3 一台多接口主机例子...............................................................................................30-3 图 30-4 网络拓朴举例...........................................................................................................30-5 图 30-5 可路由地址与不可路由地址...................................................................................30-8 图 30-6 简单网络拓朴.........................................................................................................30-10 图 30-7 恶意定向广播举例.................................................................................................30-11 图 30-8 路由查找.................................................................................................................30-18 图 30-9 入报文与出报文的路由.........................................................................................30-20 图 31-1 可能需要策略路由的拓朴举例...............................................................................31-2 图 31-2 策略路由查找...........................................................................................................31-5 图 31-3 多路径拓朴举例.......................................................................................................31-6 图 31-4 加权随机选择举例...................................................................................................31-8 图 31-5 分配下一跳给接口的几种不同方式.....................................................................31-10 图 31-6 路由、流量控制和防火墙(Netfilter)子系统之间的交互...............................31-12 图 31-7 realm配置举例........................................................................................................31-14 图 31-8 计算路由tag所用的逻辑........................................................................................31-15 图 31-9 用户空间与内核之间的接口.................................................................................31-17 图 31-10 共享介质拓朴上配置举例...................................................................................31-19 图 31-11 需要生成一个ICMP重定向的条件.....................................................................31-20 图 31-12 需要处理ingress ICMP重定向的条件.................................................................31-22 图 31-13反向路径过滤举例................................................................................................31-23 图 32-1 内核配置(make xconfig).....................................................................................32-1 图 32-2 常见的hash表及链表的使用...................................................................................32-7 图 32-3 下一跳scope初始化举例..........................................................................................32-8 图 32-4 路由初始化时调用的主要函数顺序.....................................................................32-12 图 32-5 fib_netdev_event与fib_inetaddr_event函数...........................................................32-13 图 32-6 fib_disable_ip函数..................................................................................................32-16 图 32-7 fib_add_ifaddr函数.................................................................................................32-18 图 32-8 fib_del_ifaddr函数..................................................................................................32-21 图 33-1 路由缓存结构...........................................................................................................33-2 图 33-2b rt_intern_hash函数..................................................................................................33-5 图 33-3 (a) ip_route_input_key函数; (b) __ip_route_output_key函数..............................33-7 图 33-4 加权随机算法创建的下一跳数据库.....................................................................33-13 图 33-5 为选择下一跳而创建的临时链表举例.................................................................33-13 图 33-6 dst_ops接口............................................................................................................33-14 图 33-7 dst_entry的使用 (a) 没有IPsec;(b) 有IPsec.........................................................33-17 图 33-8b rt_garbage_collect函数.........................................................................................33-22 图 33-9 垃圾回收门限值.....................................................................................................33-23 图 33-10b dst_free函数........................................................................................................33-27 图 34-1 路由表的组织...........................................................................................................34-3 图 34-2 fib_info结构的组织..................................................................................................34-5 图 34-3b fn_hash_insert函数.................................................................................................34-9 图 34-4 fn_hash_delete函数................................................................................................34-10 图 35-1 主要的路由查找函数之间的关系...........................................................................35-1 图 35-2 fib_semantic_match函数..........................................................................................35-6 图 35-3 dst->input和dst->output初始化................................................................................35-9 图 35-4 ip_route_input_slow和ip_route_output_slow的框架............................................35-11 图 35-5b ip_route_input_slow函数......................................................................................35-13 图 35-6 rt_spec_dst的选择.................................................................................................35-16 图 35-7 ip_mkroute_input函数............................................................................................35-18 图 目 录

Page i

Understanding Linux Network Internals 第七部分—— 路由

图 35-8b ip_route_output_slow函数....................................................................................35-21 图 35-9 源IP地址选择.........................................................................................................35-24 图 35-10 处理本地生成并送往本地的报文.......................................................................35-24 图 35-11 缺省规则...............................................................................................................35-30 图 35-12 fib_lookup函数的策略路由版本.........................................................................35-31 图 35-13 对ingress流量基于源站寻路...............................................................................35-32 图 35-14 r_tclassid字段结构...............................................................................................35-32 图 35-15 fib_combine_itag函数..........................................................................................35-33 图 36-1 基于ioctl和基于Netlink的路由表操作....................................................................36-1 图 36-2 IPROUTE2包用于路由的文件和函数....................................................................36-3 图 36-3 ip monitor route命令使用举例.................................................................................36-5 图 36-4 IPv4路由子系统使用的/proc文件...........................................................................36-6 图 36-5 内核文件中的数据结构分布.................................................................................36-14 图 36-6 在本书路由部分中重要的文件和目录.................................................................36-35

图 目 录 Page ii

Understanding Linux Network Internals 第七部分—— 路由

表 目 录

表 30-1 IPv4地址基于类的划分...........................................................................................30-7 表 30-2 网络与主机部分.......................................................................................................30-7 表 30-3 IPv4网络中的不可路由地址和回环地址..............................................................30-7 表 30-4 路由表项举例.........................................................................................................30-16 表 30-5 路由缓存项举例.....................................................................................................30-16 表 30-6 路由表举例1...........................................................................................................30-19 表 30-7 路由表举例2...........................................................................................................30-19 表 31-1 单张路由表举例.......................................................................................................31-3 表 31-2 RT1被用于路由从校园网1出来的流量..................................................................31-3 表 31-3 RT2被用于路由从校园网2出来的流量..................................................................31-4 表 32-1 IP配置及相关路由表项举例.................................................................................32-19 表 32-2 当设备关闭或注销时删除的路由.........................................................................32-23 表 32-3 最常见的路由协议守护进程与Linux内核之间的接口.......................................32-25 表 33-1 DST_GC_XXX常量................................................................................................33-28 表 34-1 fib_table结构中虚函数的初始化.............................................................................34-6 表 35-1 fib_props数组的初始化...........................................................................................35-7 表 35-2 用于dst->input的程序..............................................................................................35-8 表 35-3 用于dst->output的程序............................................................................................35-9 表 36-1 在IPROUTE2包的iproute.c文件中,由do_iproute设置的参数.............................36-2 表 36-2 在IPROUTE2包的iprule.c文件中, 由do_iprule设置的参数...................................36-3 表 36-3 /proc/sys/net/ipv4/目录下用于调整路由子系统的文件.........................................36-7 表 36-4 /proc/sys/net/ipv4/route目录下用于调整路由子系统的文件.................................36-8 表 36-5 /proc/sys/net/ipv4/conf目录下用于调整路由子系统的文件..................................36-9 表 36-6 /proc/net目录下由路由子系统使用的文件及对应的内核处理钩子函数...........36-12 表 36-7 fib_nh结构中nh_flags字段的取值.........................................................................36-18 表 36-8 内核用到的fib_protocol的取值.............................................................................36-18 表 36-9 用户空间用到的fib_protocol的取值.....................................................................36-18 表 36-10 路由metrics...........................................................................................................36-19 表 36-11 rt_flags的可能取值..............................................................................................36-24 表 36-12 rt_type可能的取值...............................................................................................36-25 表 36-13 在路由子系统中用到的函数、变量和数据结构...............................................36-32

表 目 录 Page i

Understanding Linux Network Internals 第七部分—— 路由

第七部分:路由

三层协议,例如IP协议,必须找到如何到达接收每个报文的系统。接收方可能在隔壁小房间,也可能在世界的遥远处。当涉及网络数超过一个时,三层协议负责计算最有效路由(如果能够找到的话),然后根据路由将消息发送给下一个系统,也就是送给下一跳(next hop),这一过程被称为路由(routing)。* 路由在Linux网络代码中扮演着核心角色。下面给出路由部分各章所涉及的内容:

第30章 路由概念

本章介绍一个基本路由器的功能,这也是Linux内核必须提供的基本功能。

第31章 高级路由

本章介绍在更为复杂的环境下,通过使能一些可选特性来配置路由。在这些特性中,我们将看到策略路由(policy routing)与多路径路由(multipath routing),也将看到与路由子系统交互的其它子系统。

第32章 路由在Linux中的实现

本章概述路由代码用到的主要数据结构,描述路由子系统的初始化,给出路由子系统与其它内核子系统之间的交互。

第33章 路由缓存

本章描述路由缓存,包括协议无关缓存(目的缓存或DST)。主要内容有如何将元素插入缓存和如何将元素从缓存内删除,以及垃圾回收和查找算法。

第34章 路由表

本章描述路由表(routing table)结构,以及如何添加和删除路由项。

第35章 路由查找

本章描述针对入流量和出流量,配置或没有配置策略路由情况下路由表的查找。

第36章 相关主题

本书结尾的这一章详细描述了第32章中介绍的数据结构,讨论了用户空间与内核之间的接口。还描述了旧一代和新一代的管理工具,即net-tools与IPROUTE2软件包。

译者注:英文route是指一条路由表项,作名词使用。英文routing按照定义是指为入流量或出流量选择路由表项,并根据该路由将报文发送给下一跳的过程,作动词使用;也可以用在routing table,routing cache,policy routing,multipath routing,routing protocol等词组中,作形容词或动词使用。有的文档中统一将routing翻译为选择路由、路由选择、选路等术语,例如将routing table翻译为路由选择表,但这与通常的使用习惯不符。

由于“路由”一词在中文内既可以作名词,也可以做动词。因而在本文中,我们将根据上下文的不同,将英文route翻译为路由项、路由表项或路由等术语,但它们的含义相同,都是指一条路由表项。将routing table,routing cache,policy routing,multipath routing,routing protocol等词组中的routing按照习惯翻译为路由,例如将routing table翻译为路由表。只是在input routing与output routing词组中才将routing翻译为选路。

*

第 七 部 分 Page I

Understanding Linux Network Internals 第七部分—— 路由

第30章 路由概念

在图30-1中给出了在网络协议栈中路由子系统的位置(灰色框)。这张图没有包含所有细节(Netfilter、桥接等),但给出了路由前后其它主要的内核子系统。

为了解释一些特性或实现细节,我将通过用户空间配置截图(snapshot)来演示。如果用户需要进一步学习在这些演示例子中用到的用户空间工具,可以使用第36章作为一个参考。

路由部分讨论的焦点是IPv4网络,但我将给出与IPv4有明显差异的IPv6特点。

四层协议

(例如TCP/UDP/ICMP/IGMP)

发送 接收 三层协议

(例如Ipv4, IPv6, DECnet)

转发

入流量控制出流量控制

(FASTROUTE)

驱动

eth0 eth1eth2eth3 NICs

图 30-1 路由子系统与其它主要网络子系统之间的关系

30.1 路由器, 路由与路由表

路由器(Router)用最简单的形式可定义为:一台配备有多个网络接口卡(Network Interface Card ——NIC),利用网络知识正确转发ingress流量的网络设备。*

决定一个ingress报文应当送给本机还是转发所需要的信息,以及在转发时正确转发报文所需要的信息,都存储在一个被称为转发信息库(Forwarding Information Base——FIB)的数据库中,它通常被简称为路由表(routing table)。

图30-2给出了一个简单环境,一台主机处于网段为10.0.0.0/24的局域网内,一台路由器RT,局域网内的主机使用该路由器来访问互联网。

* 与IPv4不同,IPv6是在IP头中用一个特定的标志(flag)来定义路由器角色。

第30章 路由概念 Page 30-1

Understanding Linux Network Internals 第七部分—— 路由

大多数主机都不是扮演路由器的角色,通常只有一个接口,主机配置为使用一个缺省网关来访问任何非本机地址。这样在图30-2中,目的地为10.0.0.0/24网络之外任何主机的流量(用0.0.0.0/0指定)都被送给网关10.0.0.1。对目的地为10.0.0.0/24网络内的主机的流量,则使用第六部分描述的邻居子系统(neighboring subsystem)。

不考虑网络内主机扮演的角色,每个主机维护一张路由表,当需要处理网络流量的发送和接收时查询该路由表。路由器可能需要运行主机通常不会使用的特殊软件,即路由协议(routing protocols)。毕竟,路由器需要更多关于如何到达远程网络的知识,非路由器主机*

就是依赖这些知识来访问远程网络。路由协议超出了本书范围。

路由表 10.0.0.0/24 Local(eth0) 因特网 0.0.0.0/0 下一跳10.0.0.1 缺省网关

RT …

eth0:10.0.0.2eth0:10.0.0.1

子网10.0.0.0/24

图 30-2 路由器和路由表的基本例子

在特定环境下,主机所需要的路由能力可以进一步降低,例如在第28章“代理ARP服务器作为路由器”小节中所描述的。但本章中,我们主要处理常见情况。

路由表无非就是许多路由表项的一个集合,一条路由表项或路由(route)是一组参数集合,这些参数存储了向一个给定目的地转发流量所需要的信息。在第32章中,我们将看到在Linux中如何来定义路由的细节,但这里我们可以设想确定一条路由所需要的最少的参数集合,我们用图30-2作为参考。

目的网段

路由表被用于朝着目的地转发流量。那么目的网段是路由查找程序使用的最重要字段一点也不惊奇。图30-2给出了一张包含两条路由的路由表:一条路由是到本地子网10.0.0.0/24,另一跳是到任何其它地方。后一条路由被称为缺省路由(default route),在路由表中被记录为全零网络(参见“缺省网关”小节)。

egress设备

与路由项匹配的报文应当从该设备被发送出去。例如,送往10.0.0.100的报文通过eth0设备被送出。

下一跳网关

当目的网络与本地主机不是直连时,需要通过其它的路由器来转发。例如在图30-2中的主机需要通过路由器RT才能到达10.0.0.0/24网段之外的任意主机。下一跳网关是指该路由器的地址。

30.1.1 非路由多接口主机(Nonrouting Multihomed Hosts)

前面提到,一个路由器通常有多个NIC,假定它的主要任务是将从一个接口收到的数据通过另一个接口转发出去。但是,对存在多个NIC的非路由主机,尤其是服务器,它们实际上不转发任何报文。由于以下一个或多个原因,拥有多个NIC的大型服务器并不是不常见: *

译者注:这里应当是路由器主机吧?

第30章 路由概念 Page 30-2

Understanding Linux Network Internals 第七部分—— 路由

高可用性

如果一个接口down掉或失效,可以由另一个接口接管流量(该接口也可以与另一个局域网相连)。

更强的路由能力

服务器可以配置有多条路由而不只是一条缺省路由。例如,出于特定原因(例如便于记录系统日志),可以用静态路由或多个NIC来到达特定的主机或子网。图30-3给出了一个例子,一台多接口主机(multihomed host)的第二个NIC与另一个局域网相连以到达主机A。注意这台多接口主机并不在两个局域网之间转发流量。否则按照定义它应当是一台路由器。

多路传输(Channeling)

可以将多个接口绑定,使它们看上去象路由子系统的一个接口。对一个给定的连接,这个额外层(对路由子系统透明)可以使整个带宽增加,这对于高负荷服务器是很有价值的一个特性。 因特网 缺省网关 多接口主机

A

图 30-3 一台多接口主机例子

如果主机不是前面提到的这几种情况,那么被视为一台路由器,因为这些情况下不从一个接口向另一个接口转发流量。换一种说法,即这样一台主机除了在错误或非常特殊的条件下(代理、混杂接口等),永远不接收目的为非本机的流量(这里的“本机”包含广播与多播流量)。可以将多播和广播流量视为目的为主机的流量。

30.1.2 各种路由配置

路由是一个复杂话题,我们不可能分析所有可能的环境、问题和解决方案。但重要的是在阅读源代码时了解部分话题,理解为什么要考虑和特殊处理一些看似多余的情况。

图30-4给出了有助于理解路由子系统设计的三个配置。在这些配置中路由器以Rn命名。我们看看这三个配置有哪些特殊之处:

● (a) 这是最常见的情形,不同的接口配置为不同的子网,每个子网与另一个不同的局域网相连。

● (b) 路由器RT在同一个局域网有两个接口(图中路由器下面所示),但这两个接口配置为不同的子网。

● (c) 路由器RT在子网10.0.2.0/24和10.0.3.0/24上各有一个地址,但这两个地址是配置在同一个NIC上。这可以用两种不同方法来实现:通过使用由IPROUTE2软件包引入的多个IP地址能力,或通过创建老式的aliasing接口。本章后面我们将对这两种方法做简单比较。

第30章 路由概念 Page 30-3

Understanding Linux Network Internals 第七部分—— 路由

(b)和 (c)的情况不常见,但它们是完全合理的,并且表现出Linux与IP的灵活性。它们所暗含的意义对你来说可能还不是很清晰。我们将在本章后面讨论它们并证明它们的合理性,我们首先从两个简单的暗示开始。

● 一个局域网是一个广播域。属于同一个二层广播域的所有主机会收到相互发送的广播报文。这意味着在 (b)和 (c)中,如果RT(或10.0.2.0/24网络内的任何其它主机)发送一个报文给广播地址10.0.2.255,那么子网10.0.3.0/24内的所有主机都将收到该报文(虽然将丢弃该报文),RT当然也将收到该报文。

● ingress接口不一定要不同于egress接口,虽然通常情况下这两个接口是不同的。转发通常是由一个接口接收报文,由另一个接口转发出去。但在(c)中,RT利用同一个NIC接收一个子网的报文并转发给另一个子网。

在第26章中,我们看到图30-4(b)和图30-4(c)配置暗示了要使用低层邻居协议。在本章中,我们将看到它们所暗示的路由信息。

30.1.3 本书这一部分要回答的问题

此时,你可能向自己提出诸如下面一些常见的问题:

● 如果假定一台路由器转发报文,内核是如何知道能否转发呢?

● 路由是需要全局使能还是在一对接口之间使能?

● 是否存在可调整的参数,能够显著影响Linux路由器的性能?

● 路由表是如何构造的?

或者是诸如以下一些特殊问题:

● 利用什么算法来查找转发报文所需的信息?

● 路由表只是用于转发流量,还是有其它的用途?

● 内核与运行在用户空间中的动态路由协议之间是如何交互的?

利用本章和下面一些路由章节中的知识,将能够回答上面提出的所有问题。

第30章 路由概念 Page 30-4

Understanding Linux Network Internals 第七部分—— 路由

(a)

子网10.0.4.0/24

10.0.4.1 RT1

子网10.0.1.0/24

10.0.1.1RT2

10.0.1.100

RT3

10.0.1.2

子网10.0.2.0/24

10.0.2.1

10.0.3.2

子网10.0.3.0/24

(b)

子网10.0.1.0/24

10.0.1.1RT1

10.0.2.1

10.0.3.1

子网10.0.2.0/24

(c)

子网10.0.3.0/24

子网10.0.1.0/24

10.0.1.1RT1

10.0.2.1

10.0.3.1

子网10.0.2.0/24

子网10.0.3.0/24

图 30-4 网络拓朴举例

第30章 路由概念 Page 30-5

Understanding Linux Network Internals 第七部分—— 路由

30.2 路由的基本要素

本小节将介绍路由中的一些术语和基本要素。清晰理解本书这一部分中广泛使用的一些关键术语的含义,以及内核代码中在变量和函数名中出现的关键部分是非常重要的。幸运的是,路由代码使用的命名约定是非常一致的。

下面列出的一些定义已经经过简化,其它的概念则在各自的小节中出现。

因特网服务提供商(Internet Service Provider ——ISP)

提供访问因特网的公司或组织。

转发信息库(Forwarding Information Base ——FIB)

这被简称为路由表,参见前面的“路由器,路由与路由表”小节。

对称路由与非对称路由(Symmetric routes and asymmetric routes)

通常,从主机A到主机B的路由与从B返回A的路由相同,这种路由被称为对称路由。在复杂配置情况下,返回的路由可能不同,这被称为非对称路由。

Metrics

一个metric是在一条路由上配置的一个可选参数。不要将这些metric与由路由协议使用的metric混淆:后者使用metric来量化路由的好坏。路由协议metric的例子有端对端延迟、跳跃数、配置权值或cost等等。

当利用IPROUTE2来配置一条路由时,可以提供更多的metric参数,这在“路由的基本要素”小节中定义。其中包括在第18章中描述的路径最大传输单元,或Path MTU。其它的metric被TCP用作内部变量的起始值,这些变量的值可以在以后被协议调整。可以参考任意一本关于TCP的书籍来查看这些metric的含义和用途:

● 窗口

● 往返时间

● 往返时间方差

● 慢启动门限值

● 拥塞窗口

● 通告最大段Size

● Reordering

Realm

一个用数值表示的域标识。参见第31章“基于路由表的Classifier”小节。

地址类(Address class)

IP地址被划分为不同的类,如表30-1所示。表30-2 给出了对每一类IP地址,网络部分和主机部分的size(注意D类和E类是C类地址的特例)。

第30章 路由概念 Page 30-6

Understanding Linux Network Internals 第七部分—— 路由

表 30-1 IPv4地址基于类的划分

第一个地址 最后一个地址 0.0.0.0 127.255.255.255 128.0.0.0 191.255.255.255 192.0.0.0 223.255.255.255 224.0.0.0 239.255.255.255

240.0.0.0

255.255.255.255

表 30-2 网络与主机部分

类 A B C

网络地址部分size 8 16 24

主机地址部分size 24 16 8

主机数

(包括网络地址与主机地址)

16,777,216 (224)

65,535 (216)

256 (28)

类 A B C

D(多播)

E(保留)

地址最左边的比特位 0 10 110

1110

11111

可路由地址与不可路由地址(Routable and nonroutable addresses)

IP规范中已经将一些特定范围内的地址(表30-3所示)规定为不可路由地址,这意味着这些地址只能预留在一个局域网内使用。可路由地址必须由中心体

(centralized bodies)处理且是全球唯一的。相对应的,任何人都可以配置不可路由地址,但这些多数都是在路由器后面的系统内使用这些地址。不可路由地址不能被用于提供任何因特网服务,因为它们不是全球唯一的,因特网路由器不能将流量传送给它们。

127.0.0.0/8子网中的地址比较特殊,这些地址的scope*只能为它们所配置的主机。报文中无论是源地址还是目的地址为这些地址,这些报文都不能离开主机。

表 30-3 IPv4网络中的不可路由地址和回环地址

地址

10.0.0.0/8

172.16.0.0/16到172.31.0.0/16

192.168.0.0/16

127.0.0.0/8 (loopback——回环) *

1 x A类

16 x B类

256 x C类

1 x A类

“Scope”小节描述了该术语在用于IP地址时的确切含义。

第30章 路由概念 Page 30-7

Understanding Linux Network Internals 第七部分—— 路由

图30-5给出的拓朴图中包含两个10.0.1.0/24子网,它们使用相同scope的不可路由的IP地址;还包含一个可路由的子网100.0.1.0/24。如果10.0.1.0/24子网内的任何一个主机要与子网外部的主机通信,那么路由器必须使用某种网络地址转换(Network Address Translation——NAT)来隐藏本地不可路由的子网。而且注意每台主机配置有缺省的127.0.0.1地址。在连接三个路由器与各自ISP的接口上配置了由ISP分配的可路由的IP地址。

10.0.1.2 10.0.1.2 10.0.1.110.0.1.1 lo: 127.0.0.1 lo: 127.0.0.1lo: 127.0.0.1 lo: 127.0.0.1 因特网 RT1 RT2ISP2ISP1 主机B 主机A 201.201.201.1ISP3202.202.202.1… 203.203.203.1 子网10.0.1.0/24RT3子网10.0.1.0/24 (不可路由地址)(不可路由地址) 100.0.1.1子网100.0.1.0/24 100.0.1.2 主机C

图 30-5 可路由地址与不可路由地址

30.2.1 Scope

路由和IP地址都有scope(作用范围或作用域)*,这告诉内核它们在哪些情况下是有意义的,是可以被使用的。如果理解了scope的概念,就会更容易理解路由代码中各种合理性检查(sanity check),以及各种不同scope的路由和不同scope的IP地址之间的区别。

在Linux中,路由的scope表示到目的网络的距离。IP地址的scope表示该IP地址距离本地主机有多远,某种程度上也告诉你该地址的owner距离本地主机有多远。

第32章给出了更详细的scope的列表,但这里我们先来看一些例子,使用的术语与代码中的非常类似,以便更容易将代码与这些概念相关联。

我们从IP地址常用的scope开始: Host

当一个地址只用于主机自身内部通信时scope为host,该地址在主机以外不知道并且不能被使用。例如回环地址127.0.0.1。

*

译者注:英文scope在路由代码中主要用于指路由表项或IP地址的作用范围,作用域或应用范围。本文不将该词直译出来,保持英文可使文档的可读性、与代码的可参照性更好。

第30章 路由概念 Page 30-8

Understanding Linux Network Internals 第七部分—— 路由

Link

当一个地址只在一个局域网(即每一台计算机通过链路层互联的一个网络)内有意义且只在局域网内使用时,该地址的scope为link。例如子网的广播地址。子网内一台主机发送到子网广播地址的报文被送给同一子网内的其它主机*。

Universe

当一个地址可以在任何地方使用时scope为universe,这是大多数地址的缺省scope。

注意:scope不能表现不可路由(私有)地址与可路由(公开)地址之间的区别。10.0.0.1 (为不可路由地址)和165.12.12.1(为可路由地址)的scope都可以是link或universe。当系统管理员配置地址时可以指定scope(或由配置命令分配一个缺省的scope值)。因为universe scope对前面提到的两个IP地址都是缺省值,因而如果期望这两个地址存在不同,管理员就必须明确指定一个scope。广播地址和回环地址的scope是由内核来自动设置合适的值。

现在来看当同样三个scope用于路由时表示什么意思: Host

当一条路由使目的地址为本地主机时scope为host。 Link

当一条路由使目的地址为本地网络时scope为link。

Universe

当一条路由使目的地址超过一跳时scope为universe。

我们将在第32章“添加一个IP地址”小节中看到,Linux为配置的每个本地地址创建一条路由,并且为每个配置子网的广播地址添加一条路由。那一小节将有助于读者对地址scope与路由scope之间关系的理解。

30.2.1.1 scope的使用

地址scope和路由scope被路由代码和内核其他部分广泛使用。

首先记住在Linux中,虽然管理员是在接口上配置IP地址,但这些地址属于主机而不属于接口,参见第28章“对多个接口作出响应”小节中更详细的描述。

一台主机或是在一个接口上,或是在多个接口上配置多个地址并不是不常见。当本地系统发送一个报文时,内核需要选择一个源IP地址。当主机只有一个配置了一个IP地址的NIC时情况很简单,但是当一个复杂配置中存在不同scope的多个地址时就不简单了。根据目的地址的位置,你可能更愿意选择一个有特定scope的源IP地址,目的地可以用它来返回流量或远端站点将它用于其它目的。

路由代码也使用scopes来加强对配置进行简单而又强有力的合理性检查。假定需要发送一个报文给远端主机B,它与本地主机配置的任何子网都不是直达的。路由查找将返回网关RT使用的地址。现在我们知道为了到达主机B,需要将报文发送给RT,RT将负责转发该报文。为了避免出现环路,RT必须比本地主机更靠近目的地。换句话说,到达主机B的路由scope必须比到达RT的路由scope更大。(也存在特殊配置条件时的例外情况。)

当然也有例外。参见“定向广播”小节中给出的例子。

*

第30章 路由概念 Page 30-9

Understanding Linux Network Internals 第七部分—— 路由

我们使用图30-6中的拓朴来看一个例子。从主机A到达主机B,主机A进行路由查找,返回经过10.0.1.1的缺省路由,其scope为RT_SCOPE_UNIVERSE。根据图中所示的其它路由,通过A的eth0接口可以直达网关地址10.0.1.1。第二条路由的scope为RT_SCOPE_LINK,它比前一条路由的scope更窄,因而可以用接口eth0来发送报文给scope更广的地址。

在第33章“Egress查找”小节中,可以找到在讨论ARP时使用scope的一个例子。

图 30-6 简单网络拓朴

30.2.2 缺省网关

缺省网关通常是指0.0.0.0/0路由,当到一个目的地址不存在明确的路由项时使用该路由。*与因特网相连的主机通常配置有到本地网络的一条路由(根据NIC的配置间接得出)和访问因特网的一条缺省路由(通常由ISP给出)。而另一方面,路由器可能配置缺省路由,但也可能没有配置缺省路由,这依赖于路由器在网络拓朴中的位置以及它所扮演的角色而定。

Linux内核不对配置的缺省网关数量做任何,详见第35章。

30.2.3 定向广播(Directed Broadcasts)

广播报文简单讲就是送给子网广播地址的报文。子网广播报文通常是由位于同一子网内的主机生成,这意味着广播报文是要送给在本子网内所有的主机。

另一方面,定向广播是送给一个远程子网的广播地址。定向广播应用的一个例子就是SAMBA服务器使用远程公告特性向远程子网通告资源(打印机或文件夹)。

我们通过参看图30-7(a)来描述定向广播。当主机A发送一个报文到地址10.0.0.255时,生成的是一个本地广播(图中没有画出)。但是当主机A发送一个报文到地址10.0.1.255时,生成的是一个定向广播。在我们的例子中,主机A和目的网络(10.0.1.0/24)只间隔一跳,但它们之间的距离可以更远。只要发送方发送的广播地址不属于本地网络,那么就属于定向广播。

*

在一些拓朴中不需要缺省网关。参见第28章“代理ARP作为路由器”小节中描述的一个例子。

第30章 路由概念 Page 30-10

Understanding Linux Network Internals 第七部分—— 路由

图 30-7 恶意定向广播举例

一台主机只有属于定向广播报文所定向的子网,才能够标识定向广播报文。例如,在图30-7(c),RT1不知道目的地为100.0.1.127的报文是否为子网广播,但RT2知道。所以,定向

第30章 路由概念 Page 30-11

Understanding Linux Network Internals 第七部分—— 路由

广播只能由到达目的子网路径上的最后一个网关识别,因为这个网关配置有该子网的一个IP地址。

你可能奇怪为什么这一点很重要呢?因为滥用定向广播可以生成拒绝服务(DoS)攻击,不幸的是很难将恶意的定向广播与正常善意的定向广播区分开。但有一种情况很可能是恶意的:定向广播发送ICMP ECHO REQUEST报文(即pings)。

我们来看图30-7(a)和(b),设想10.0.0.200发送一个ICMP ECHO REQUEST报文到广播地址10.0.1.255(该地址不是10.0.0.200所在的子网),使用的源IP地址为10.0.0.100(该地址不是10.0.0.200自己的地址)。这将使远程子网10.0.1.0/24中的每一台主机都对该ICMP ECHO REQUEST作出回应,分别发送一个ICMP ECHO REPLY报文到IP地址为10.0.0.100的受害主机。因为受害主机从来没有发送任何ICMP ECHO REQUEST报文,所以只是简单地丢弃这些报文,但丢弃大量的报文也将耗费大量的CPU时间。你可以想象一下,如果在子网10.0.1.0/24内有大量的主机,那么受害主机将被大量的垃圾流量淹没。

Linux内核的路由子系统不允许丢弃任何定向广播(但你可以使用过滤系统来清除定向广播)。Linux对目的为广播地址的ICMP ECHO REQUESTS报文作出特殊处理:当目的地址为本地子网广播地址时,管理员可以指示主机是否应当对ICMP ECHO REQUEST报文作出回应。

30.2.4 主地址与第二地址

有时侯需要在同一个NIC上配置多个IP地址,这可能是由于以下一些需求:

● 一台主机上运行多项服务,而且每一项服务要有不同的IP地址。这也可以简化防火墙规则。

● 你可能缺少硬件被迫临时将两个子网合并到一个hub或switch。此时,一个NIC就可以将两个子网相连。

当听说在同一个NIC上配置多个IP地址,但内核中的路由代码将它们并不是同等对待,即使它们都分配有相同的scope时,你可能感到比较奇怪。区别就在于某些地址为主(Primary)地址,而其它地址为第二(Secondary)地址。

当在一个接口上配置一个IP地址时,同时也需要提供一个网络掩码。如果没有提供网络掩码,系统也不会抱怨,这意味着系统为你选择了一个缺省的网络掩码(例如可以基于IP地址所属的类来设置一个缺省的网络掩码,见“路由的基本要素”一节)。如果没有网络掩码,路由子系统将不清楚哪些地址通过该接口是可以直达的,所以每个IP地址都伴随有一个网络掩码。如果在同一个接口上配置了多个IP地址,则需要为每一个IP地址指定一个网络掩码。这些网络掩码可以相同,也可以不同,这要根据具体需求来配置。

当配置一个地址时,如果该地址与同一个NIC上已经配置的地址在同一子网内,那么该地址被视为第二地址。这包含子网相同的情况。因而地址配置顺序就很重要:当配置IP地址时虽然没有明确指明它是主地址还是第二地址,但根据已经配置的地址及子网就能够自动作出决策。

我们下面来看几个例子。

下面是在名为eth0的一个NIC上的配置,配置了两个网络掩码相同的地址,首先配置10.0.0.1/24,然后配置10.0.0.2/24。因为这两个地址落在同一个子网10.0.0.0/24内,所以配置的第一个为主地址,另一个为第二地址。

[root@router kernel]# ip address add 10.0.0.1/24 broadcast 10.0.0.255 dev eth0 [root@router kernel]# ip address list dev eth0

4: eth0: mtu 1500 qdisc pfifo_fast qlen 100 link/ether 00:60:97:77:d1:8c brd ff:ff:ff:ff:ff:ff 第30章 路由概念 Page 30-12

Understanding Linux Network Internals 第七部分—— 路由

inet 10.0.0.1/24 brd 10.0.0.255 scope global eth0

[root@router kernel]# ip address add 10.0.0.2/24 broadcast 10.0.0.255 dev eth0 [root@router kernel]# ip address list dev eth0

4: eth0: mtu 1500 qdisc pfifo_fast qlen 100 link/ether 00:60:97:77:d1:8c brd ff:ff:ff:ff:ff:ff inet 10.0.0.1/24 brd 10.0.0.255 scope global eth0

inet 10.0.0.2/24 brd 10.0.0.255 scope global secondary eth0

只要你愿意,你就可以在每一个接口上配置多个主地址和多个第二地址。对一个特定的网络掩码(例子中的网络掩码为/24),只能有一个主地址。如果我们接下来添加第三个地址10.0.0.3/24,那么它将被划分为第二地址,与主地址10.0.0.1/24相关联。

但另一方面,10.0.0.1/24和10.0.0.3/25位于不同的子网(因为网络掩码不同),虽然两个子网覆盖的地址有部分重叠。所以,如果在前面例子中接下来添加10.0.0.3/25地址,该地址将成为eth0的另一个主地址。下面给出ip address list命令的输出信息:

[root@router kernel]# ip address add 10.0.0.3/25 broadcast 10.0.0.127 dev eth0 [root@router kernel]# ip address list dev eth0

4: eth0: mtu 1500 qdisc pfifo_fast qlen 100 link/ether 00:60:97:77:d1:8c brd ff:ff:ff:ff:ff:ff inet 10.0.0.1/24 brd 10.0.0.255 scope global eth0

inet 10.0.0.2/24 brd 10.0.0.255 scope global secondary eth0 inet 10.0.0.3/25 brd 10.0.0.127 scope global eth0

在第35章“辅助程序”小节中,我们将看到当一个接口中存在子网重叠的多个主IP地址时,如何从中选择一个IP地址。

简言之,决定是主地址还是第二地址不只是考虑IP地址,还需要考虑标识子网的网络掩码。当在一个接口上配置多个IP地址时,重要的是要理解主地址与第二地址之间的区别。这在阅读路由代码时也很重要。我们将在第32章中看到对许多事件和条件作出响应依赖于IP地址为主地址还是第二地址。下面给一些例子:

● Primary addresses contribute to the entropy of the CPU that happens to run the code that applies the configuration.

● 当删除一个主地址时,所有相关的第二地址也被删除。但通过/proc可以配置一个选项,在当前主地址被删除时可以将第二地址提升为主地址(参见第18章)。

● 当主机为本地生成的流量选择源IP地址时,只考虑主地址。

30.2.4.1 老式配置:aliasing接口

在前面一些小节中你可能已经注意到,我总是使用ip address命令来配置地址。理由在于ifconfig是老一代的接口配置命令,它是Unix系统管理员最常使用的工具。它不区分主地址与第二地址,甚至不显示第二地址。所以在前面一些小节给出的例子中,ifconfig和ip address list命令的输出不一致。 第36章对Linux net-tools工具包提供的ifconfig和由IPROUTE2软件包提供的新一代的ip address进行了深度对比。

在引入IPROUTE2及其高级路由能力之前,Linux使用aliasing接口概念,为了保持向后兼容,在新内核中仍然可以看到aliasing接口。利用ifconfig在一个NIC上配置多个地址,唯一的方法就是定义类似eth0:0、eth0:1等虚设备。可以将每个虚设备当作一个真正的NIC使用:可以在它上面配置一个地址,当配置路由时用做一个设备等等。

第30章 路由概念 Page 30-13

Understanding Linux Network Internals 第七部分—— 路由

30.2.4.2 aliasing设备与主/第二IP地址间的关系

因为内核既支持IPROUTE2的高级路由能力,又支持老式的aliasing接口,所以这两种模式需要共存。我们将在第32章中探究内核的内部细节,但这里我们可以从用户空间视角来看看这两种模式是如何共存的。

当配置一个aliasing设备时,仍然是根据在“主地址与第二地址”小节中介绍的规则来划分主地址与第二地址。但ip address list命令的输出现在添加了一个到aliasing设备的引用,下面的截图展示了这样一个例子。我们从一个已经配置了地址的接口eth1开始,在aliasing设备eth1:1上添加在同一个子网内的一个地址,然后在另一个aliasing设备eth1:2上添加不同子网内的另一个地址。因为子网不同,所以eth1:1设备上的地址为第二地址,而eth1:2设备上的地址为主地址。

[root@router kernel]# ip address list ...

11: eth1: mtu 1500 qdisc pfifo_fast qlen 1000 link/ether 00:0a:41:04:bd:16 brd ff:ff:ff:ff:ff:ff

inet 192.168.1.101/24 brd 192.168.1.255 scope global eth1 ...

[root@router kernel]# ifconfig eth1:1 192.168.1.102 netmask 255.255.255.0 [root@router kernel]# ifconfig eth1:2 192.168.1.103 netmask 255.255.255.128 [root@router kernel]# ip address list ...

11: eth1: mtu 1500 qdisc pfifo_fast qlen 1000 link/ether 00:0a:41:04:bd:16 brd ff:ff:ff:ff:ff:ff

inet 192.168.1.101/24 brd 192.168.1.255 scope global eth1 inet 192.168.1.103/25 brd 192.168.1.255 scope global eth1:2

inet 192.168.1.102/24 brd 192.168.1.255 scope global secondary eth1:1 ...

一个明显的问题是是否可以用IPROUTE2在一个aliasing设备上配置多个地址。这是不可能的,因为IPROUTE2并不象ifconfig那样将aliasing设备看作真正的、的设备:对IPROUTE2而言,aliasing设备只是IP地址的一个标签(label)。

[root@router kernel]# ip address add 192.168.1.104/24 dev eth1:1 Cannot find device \"eth1:1\"

30.3 路由表

路由子系统的核心是路由表。路由表最简单的定义是:它是由一个路由表项数据库组成,提供多种接口供诸如IPv4等其它子系统使用,其中最重要的接口是路由查找。

就象你已经想到的那样,路由并不只是由在“路由器、路由与路由表”小节中给出的基本信息组成。随着时间的推移,由于代码优化和引入新的特性,路由表中一条表项所包含的信息量已经增加了许多,我们将在第34章看到这些细节。

在下面的小节中,我们将简单看到:

● Linux是如何路由目的为本地地址的报文。

● 使用什么算法来查找路由表中的地址。

● 对找到匹配路由的流量,除了进行缺省转发外还有哪些管理动作(action)?

● 上层协议为了便利,在路由项中保存了那些额外信息?

第30章 路由概念 Page 30-14

Understanding Linux Network Internals 第七部分—— 路由

30.3.1 特殊路由

当收到一个报文时,路由器需要决定将它送给本地相邻的更高层(因为本地主机是最终目的地)还是转发。一种简单方法是将所有的本地地址存储在一个链表内,在路由查找时对每个报文扫描该链表。当然,链表不会是最佳的选择,还有许多更好的数据结构可以提供更快速的查找。Linux中使用一张的、只存储本地地址的、基于哈希的路由表。更准确地说,在这张路由表中存储所有的监听地址,既包括本地配置的地址,也包括子网广播地址。

这意味着,在缺省情况下,Linux使用两张路由表:

● 一张表用于本地地址。从该表中查找到匹配项表明报文要送给自己,我们将在第32章中看到内核是如何将路由表项插入该表。

● 一张表用于所有其它的路由,路由表项由用户手工静态配置或由路由协议动态插入。

30.3.2 路由类型与动作

我们在“路由器、路由与路由表”小节中看到一条路由包含的基本信息。缺省情况下,当一个报文在路由表中有匹配的路由项时,缺省动作是根据该路由项的转发信息,即下一跳路由器和出接口来转发该报文。

但Linux同时允许用户根据自己的需要来定义其它类型的动作(Actions)。*下面给出一些主要动作:

黑洞(Black hole)

匹配这一类型的路由被丢弃。

不可到达(Unreachable)

匹配这一类型的路由被丢弃,并生成一个互联控制消息协议(Internet Control Message Protocol——ICMP)主机不可达的消息。

禁止(Prohibit)

匹配这一类型的路由被丢弃,并生成一个ICMP报文被过滤消息。

扔出(Throw)

这一类型的路由与策略路由联合使用,策略路由特性参见第31章中的描述。当配置了策略路由时,匹配这一类型的路由将放弃查找当前表,如果还存在其它路由表时继续查找下一张表。

30.3.3 路由缓存

根据路由器所扮演的角色,路由表中的路由项数量范围可以从几条到数十万条。因此很显然,维护一张更小的表来缓存路由查找结果既有优点也有缺点。Linux将路由缓存分为两部分(这里所说的协议是指Ipv4和Ipv6等三层协议):

● 一个是与协议相关的缓存

● 一个是与协议无关的缓存,通常被称为DST

* 我们将在第36章中看到,只有使用新一代的配置工具IPROUTE2,才能够配置这些可供选择的路由类型。

第30章 路由概念 Page 30-15

Understanding Linux Network Internals 第七部分—— 路由

第一部分是缓存框架部分,每个元素被定义为一个由具体协议字段组成的集合。第二部分嵌套在第一部分内,只存储与协议无关的信息。协议相关缓存部分和协议无关缓存部分都在第33章中描述。

我们将在第31章中看到,在一个Linux系统中可能创建多张的路由表来支持策略路由特性。但Linux中无论路由表的数量有多少,只使用一个路由缓存。如果支持策略路由,那么缓存就不能够提供任何公平性,可能有的路由表要比其它路由表使用更多的缓存项(即各个路由表在缓存中占用的空间是不均等的)。但这种方法可以确保整个系统有更大的路由吞吐量(greater routing throughput overall)。

30.3.4 路由表与路由缓存

路由表和路由缓存除了在容量和结构不同以外,对象粒度也不同。路由表使用连续地址的集合即子网,而缓存项与单个IP地址相关联。因此,路由表和路由缓存使用的查找算法也不同,我们将在“查找”小节中描述。

我们来看一个例子。假定路由表中包含一条表30-4中给出的路由项,它是唯一一条到子网10.0.1.0/24的路由项。

表 30-4 路由表项举例

目的地

10.0.1.0/24

下一跳

10.0.0.1

出接口 eth0

我们假定内核需要发送两个报文,分别到达地址10.0.1.100和10.0.1.101。因为表30-4中的路由项匹配这两种情况,因而内核使用它来路由这两个报文,这样将在路由缓存中加入两个表项,见表30-5。

表 30-5 路由缓存项举例

目的地

10.0.1.100

10.0.1.101

下一跳

10.0.0.1

10.0.0.1

出接口 eth0 eth0

当然,表30-5中给出的元素是简化版本。在第33章中,我们也将看到包含源地址的路由缓存项。

30.3.5 路由缓存的垃圾回收

垃圾回收负责清理路由子系统中不再被使用的数据结构。但是,有的数据结构仍然在被使用也可以被删除,例如为了存储更为重要的信息而需要释放内存时。垃圾回收所做的删除操作不会导致任何数据的丢失,因为所有被清理的表项可以被重建。在最坏情况下,从缓存中删除一个元素只会导致缓存查找时不命中。

垃圾回收有两种类型:

同步回收

当路由子系统看到需要释放内存时,立即进行清理。路由代码在两种情况下强制垃圾回收而无需等待定时器:

第30章 路由概念 Page 30-16

Understanding Linux Network Internals 第七部分—— 路由

● 当一条新表项需要被添加到路由缓存内,而缓存中当前的表项数量已经到达用户配置的一个特定门限值。

● 当邻居子系统缓存需要内存时。我们在第27章中看到路由缓存和邻居子系统缓存相互引用对方。创建一条新的路由缓存项将可能触发创建一条新的邻居缓存项。如果邻居协议,例如ARP无法分配所需的内存,那么路由子系统将强制垃圾回收来间接释放邻居协议所占用的数据结构,以帮助后来者找到所需要的内存。

异步回收

为了保持缓存的容量合理,使用一个周期性定时器来触发定期的清理操作。缺省情况下,路由缓存项不会过期(expire)。但可能有些外部子系统告诉路由缓存在一段给定时间之后过期一定数量的表项。路由子系统运行一个周期性定时器来扫描缓存,查找以下表项:

● 过期且应当被删除的表项

● 没有过期,但如果内核需要释放内存可以被牺牲的表项

30.3.5.1 使路由缓存表项过期的事件举例

路由缓存表项只有在特殊情况下才被设置为过期,这些情况包括:

● 当本地系统接收到一条ICMP UNREACHABLE 或 ICMP FRAGMENTATION

NEEDED消息后,转交给ICMP层处理。这些消息告诉本地主机:在到达目的地路途中,自己先前发送的一条报文长度超过了一台路由器的MTU。ICMP处理钩子将扫描路由缓存,更新所有受影响表项的PMTU字段,并且设置这些受影响表项在一段可配置时间之后过期,缺省为10分钟。

ICMP也通知与触发该ICMP消息的报文相关的L4协议。例如TCP可以将这些通知用于路径MTU发现算法,参见第18章和第25章中关于路径MTU发现的更多细节。

● 当邻居协议无法解析L3到L2映射(参见第27章),或者当本地主机处于IP隧道的一端而另一端由于某种原因不可达时(例如由于路由问题或配置错误),则认为目的IP地址是不可达。

当一个目的IP地址被认为是不可达时,缓存中与该地址相关的所有表项需要被flush,因而立即被设置为过期。

30.3.5.2 合格的缓存受害者举例

在一些情况下,内核需要释放一些缓存表项来为新表项腾出空间,周期性定时器本身不能够保证缓存中总存在可用空间(即保持容量在某个门限值以下)。那么在这些情况下,主机必须删除周期性定时器没有处理的表项,因为这些表项仍然是有效的。虽然垃圾回收系统要从有效表项中选择受害者,但可以通过选择那些可以被快速重建的表项作为受害者,从而以较少代价减少伤害。

表项被删除的好的候选者包括到广播地址和多播地址的路由。一般来讲,当路由子系统删除一条路由缓存表项时,可能间接删除了L3到L2的关联。在这种情况下,如果主机下一次发送数据给L3地址,邻居子系统就需要再次解析 L3到L2的关联。但广播地址和多播地址解析的代价很小,因为它们不需要任何solicitation请求(参见第26章中“特殊情况”小节)。

第30章 路由概念 Page 30-17

Understanding Linux Network Internals 第七部分—— 路由

表项被删除的特别差的(代价高的)候选者包括:

重定向(REDIRECT)路由

这种路由项是通过一条ICMP重定向消息学到的。如果它被删除,主机将使用次优路由来转发报文。删除这种表项也可能浪费时间,因为主机将很有可能收到另一条ICMP重定向消息来将该路由项重新插入。

管理员手工配置的路由项

这些路由项是上层应用通过诸如ip route get 10.0.0.1 monitor等命令,要求内核在改变路由状态时(通过NetLink套接字)发送通知。上层应用可能由于某种原因认为该路由项很重要,参见第36章中表36-11提供的更多信息。

在任何情况下,不会将引用计数非零的表项视为合格的受害者而删除。

30.4 查找

在“路由缓存”小节中提到,Linux使用路由缓存和路由表。图30-8总结了路由表查找步骤。为了简化“路由/递交(deliver)报文”,图中没有给出在前面“特殊路由”小节中描述的各种路由。

路由缓存查找是从一张简单哈希表中寻找完全匹配,而在容量更大、更为复杂的路由表中查找是基于最长前缀匹配(Longest Prefix Match——LPM)算法,这在下面小节中描述。我们将在第34章看到,一张路由表是多种数据结构复杂交织而成的,这种组织方式使LPM算法速度更快,实现更容易,处理大量路由表项时伸缩性好,而且减少了共享数据结构实例的复制。

Hit

缓存查找

Miss

Hit 本地地址查找 Miss

Hit 远端地址查找

Miss

丢弃报文路由/递交报文

图 30-8 路由查找

第30章 路由概念 Page 30-18

Understanding Linux Network Internals 第七部分—— 路由

30.4.1 最长前缀匹配

如果到每个目的地只有一条路由,那么路由查找将很简单。只要找到一条路由,其目的子网包含目的地址,那么就完成了查找的关键部分。然而路由是一个复杂的话题。如果深入了解网络拓朴细节或复杂路由情况,那么可以说到同一目的地存在多条路由并不是不常见的。这些路由项之间的重叠可能小到一个地址,大到整个子网。

在有多条路由项匹配情况下,路由算法需要一个规则来决定应当选择哪一条作为最佳的候选路由。这就需要用LPM算法来专门解决该问题:最具体的路由是最佳路由。也就是子网最小或网络掩码最长的路由项。

我们来看一个例子。假定路由表中有两条表项与目的地址10.0.0.100的查找匹配,见表30-6。

表 30-6 路由表举例1

目的地

10.0.0.0/16

10.0.0.0/24

下一跳

10.0.1.1

10.0.0.1

出接口 eth0 eth1

因为第二条路由与目的地址有24个比特位相同,而第一条路由只有16个比特位相同,那么称第二条路由为最长前缀匹配而获胜。与表30-6给出的路由项相似的情况并不是不常见,即一条路由项可到达的目的地是另一条路由项的一个子集。例如由于管理或安全原因,可能需要将目的地址为某个特定子网的路由与该网络中的其它部分分开,这也是配置路由最容易的方法。另一种方法是将10.0.0.0/16划分为多个/24子网(即从10.0.0.0/24 到

10.0.255.0/24),因而在路由表中有255条路由,其中2条路由的下一跳相同。而这将减慢路由查找速度,耗费更多的CPU资源。

但是当存在多条路由匹配某个给定的目的地时,仅靠LPM算法不足以决定选择哪条路由。我们来看表30-7中给出的例子。

表 30-7 路由表举例2

目的地

10.0.0.0/16

10.0.0.0/24

10.0.0.0/24

下一跳

10.0.1.1

10.0.0.1

10.0.0.2

出接口 eth0 eth1 eth1

此时,存在两条路由有着相同的前缀长度。

我们将在第35章中看到在搜索关键字中包含服务类型(Type of Service ——TOS)的查找:这意味着在平局时可以使用TOS来区分获胜者。

当根据TOS还无法选出路由时,优先级更高(优先级取值更低)的路由项被选中。

如果根据优先级还无法选出路由时,内核就简单选择第一条。这意味着与到同一目的地、有着相同前缀长度的路由项被添加到路由表中的顺序有关。

第30章 路由概念 Page 30-19

Understanding Linux Network Internals 第七部分—— 路由

30.5 报文的接收与发送

在发送报文和接收报文时使用路由表来选择路由,因为任意一种类型都可能提交给本地或是转发。但除了这些明显使用路由表的情况外,还有一些使用不太明显。

图30-9给出了使用路由表的一些例子。这里将由数据接收触发的查找(左侧)和由数据发送触发的查找(右侧)分开。注意当报文是第一次接收时,采集转发入报文所需要的路由信息,这解释了在图30-9中为什么转发模块没有箭头指向路由模块。这张图也给出了在哪些章节中可找到相应内核模块的详细信息。下面给出图中所示的一些活动细节:

● 地址解析协议(ARP)报文不被路由,但ARP可能需要路由查找来加强某种合理性检查,参见第28章。

● IP-over-IP是一个简单的隧道协议,它将IP报文封装到更大的IP报文内。当IP处理钩子处理一个进入的IP-over-IP报文时,需要将payload重新送给IP层。内部的IP报文象其它ingress报文一样被路由,所以路由子系统需要进行另一次路由查找。

● 路由一个报文一般只需要一次查找,而不考虑其源于何处(本地或远端)。查找返回路由该报文所需的所有信息,包括将接管该报文的内核函数。但也有一些例外:可能有一个特性由于某种原因需要多次查找,就象刚才描述的IP-over-IP中的情况。

● 路由内核首先检查缓存中是否已经包含所需要的信息,如果没有则检查路由表。

图 30-9 入报文与出报文的路由

第30章 路由概念 Page 30-20

Understanding Linux Network Internals 第七部分——路由

第31章 高级路由

前一章介绍了路由基础部分。本章介绍在更为复杂的环境下,使用诸如策略路由

(policy routing)与多路径路由(multipath routing)等可选特性来配置路由。本章也将介绍路由是如何与由QoS负责的流量控制子系统,以及防火墙代码(Netfilter)相互作用。在结束时介绍两个小特性:ICMP重定向与反向路径过滤(reverse path filtering)。

31.1 策略路由背后的概念

我们在第30章“特殊路由”一节中看到,Linux内核在缺省情况下使用两张路由表,一张表用于本地路由,另一张可以由管理员来配置。如果内核编译时支持策略路由,那么可以有多达255张不同的、相互的路由表。在本章中,我们将看到策略路由的使用,在第35章中将看到它对路由子系统设计产生的影响。

策略路由背后的主要思想是允许用户不只是根据目的IP地址一个参数,而可以根据多个参数来配置路由。

在因特网大发展的一些年里,大多数路由器被配置为只能够根据报文内的目的IP地址来路由。(为了简化,这里不考虑诸如跨越多个ISPs或国界等特殊因素)。只基于目的地址(以及一些额外的配置参数)来路由,使路由表在面临大多种情况时都相当完美。

但商业界还需要考虑更多的因素,例如出于安全或计费考虑需要分开流量,或者对某条特殊路由需要发送实时流量。从而出现了策略路由。因为路由可以有各种各样的标准,所以在本章中,我们将不只是基于目的地址的路由都认为是策略路由。

使用策略路由的一个例子是,一家ISP需要基于原有的客户或基于服务质量(QoS)需求来路由流量。通常可以根据ISP路由器上流量的入端口、源IP地址,或二者的组合来很容易标识出是原有的客户。路由器也可以基于源地址与目的地址的组合来标识一组流量或从某个源地址来的汇聚流量。QoS需求可以根据IP头中的DiffServ Code Point (DSCP)字段来识别,以及根据更高层头部多个字段的组合来识别(这些可以识别出上层应用)。

因为本书主要是讨论内核的内部细节,我们想知道这些策略是如何传送给内核,它们在路由表中是如何实现,以及它们对于路由查找的影响。我们将学习到所有这些内容,但我们首先来看一个例子,使用图31-1中给出的拓朴结构作为参考。

路由器RT用于将校园网1和2连接到校园网3和因特网(我们这里仅是一个例子,不关心路由器是如何来转换不可路由地址10.0.x.x)。我们来看路由器RT上的配置。我们也假设需要执行以下两个策略:

● 从校园网1到校园网3的流量送给路由器RT1,从校园网2到校园网3的流量送给RT2。一种原因可能是校园网2的管理方愿意付费更多,因而允许使用更快速的网络来连接RT与校园网3。

● 从校园网1到因特网的流量(例如目的地址不是这三所校园网)送给DG1(缺省网关1),从校园网2到因特网的流量送给DG2。这可能是出于安全或带宽策略。

这是一个简单例子,只有几条路由和两个策略。当然,只有在非常大型且更复杂的环境中才能够体现出多张路由表的好处。虽然这个例子较小,但我们还是要忽略从因特网进入校园网的路由。

第31章 高级路由 Page 31-1

Understanding Linux Network Internals 第七部分——路由

图 31-1 可能需要策略路由的拓朴举例

在路由器RT上配置路由有两种可能的方式,一种是在Linux中使用的方法(即多路由表方法)。

单路由表方法

表31-1是在RT中实现前面提到的两个策略的单路由表配置的简化版本。注意,因为校园网1是与RT的eth0设备相连的唯一网络,校园网2是与RT的eth1设备相连的唯一网络,所以路由不需要指定源IP地址。这样,路由不只是基于目的地址,因为同一个目的地址可能匹配多条路由,所以需要检查多个条件来选择一条唯一的路由。这里所指的条件就是检查入设备和目的地址。

第31章 高级路由 Page 31-2

Understanding Linux Network Internals 第七部分——路由

表 31-1 单张路由表举例

源IP 目的IP

未指定

未指定

未指定

未指定

未指定

未指定

10.0.3.0/24

10.0.2.0/24

10.0.3.0/24

10.0.1.0/24

0.0.0.0/0

0.0.0.0/0

Ingress设备

校园网1出来的流量路由到校园网2和3 eth0 eth0

校园网2出来的流量路由到校园网1和3 eth1 eth1

校园网1和2的缺省路由 eth0 eth1

下一跳 10.0.0.10 (RT1)

未指定 10.0.0.20 (RT2)

未指定 10.0.0.11 (DG1) 10.0.0.21 (DG2)

Egress设备 eth2 eth1 eth2 eth0 eth2 eth2

多路由表方法

因为在每次路由查找时都可能要检查多个条件,所以主机维护多张的路由表,根据特定的条件选择正确的路由表,可以更加快速便捷地查找出路由。例如可以利用源IP地址或ingress设备来选择一张路由表,该表中可能包含更多的条件来帮助选择最终的路由。

这样当使用多路由表时,内核在路由查找之前需要选择出正确的路由表,这时就要利用策略来选择路由表。因而在我们例子中,可以根据以下规则,以及表31-2和表31-3来确定路由器RT上的路由选择:

● 从eth0进入的流量检查路由表1(表31-2)。

● 从eth1进入的流量检查路由表2(表31-3)。

表 31-2 RT1被用于路由从校园网1出来的流量

目的IP

10.0.2.0/24

10.0.3.0/24

0.0.0.0/0

下一跳

None

10.0.0.10 (RT1)

10.0.0.11 (DG1)

出设备 eth1 eth2 eth2

第31章 高级路由 Page 31-3

Understanding Linux Network Internals 第七部分——路由

表 31-3 RT2被用于路由从校园网2出来的流量

下一跳 出设备 None eth0 10.0.0.20 (RT2) eth2 10.0.0.21 (DG2) eth2

目的IP

10.0.1.0/24

10.0.3.0/24

0.0.0.0/0

在表31-2和表31-3中的第一条路由表项不需要用户直接配置,因为它们分别是由内核根据接口eth0和eth1上的配置派生出的。我们将在第32章看到这些表项是如何产生的。

我们将在第33章中看到,Linux只维护一张路由缓存,该缓存由所有的路由表来更新。这些路由表也共享用于构建这些表而分配的内存池。Linux并不强加任何公平机制,要求各个路由表必须平等地共享这些公共资源。除了可以简化实现,这种做法实际上使整个路由的吞吐量最大化,因为路由表的需求越高,所分配的系统资源也就越多。但是,这可能使外部明显感觉到差异:即当一台Linux主机使用不同的路由表管理来源不同的流量时,客户体验的感觉可能不同于由路由器所带来的感觉,甚至不同于由一台平等分配资源给路由表的主机所带来的感觉。

31.1.1 策略路由查找

当使用策略路由时,根据目的地查找路由可分为两个步骤:

1. 根据配置策略选择路由表,这个额外任务不可避免地增加了路由查找时间。

2. 从选择的路由表中查找路由。

当然,内核在执行这两步骤之前,总是首先在路由缓存内查找。

策略可以象路由一样指定一个管理类型(参见第30章“路由类型与动作”小节)。这可以使内核根据策略类型来快速决策,而不必等着查找路由。例如当匹配的策略配置的类型为UNREACHABLE时,内核生成一个ICMP HOST UNREACHABLE消息,而不用等待去查找与UNREACHABLE类型相匹配的路由项。

图31-2是第30章中图30-9的一个修订版本,添加了策略路由的支持,以及与各种策略类型相关的细节。

第31章 高级路由 Page 31-4

Understanding Linux Network Internals 第七部分——路由

缓存查找 Miss Local表查找 Miss 不匹配 策略查找结束 策略查找 (一个接一个) 匹配 缺省表 (Main) 4 查找 策略类型 Miss 不可达 禁止 12 ICMP ICMP ICMP主机 主机不可达 报文被过滤网络不可达

HitHit单播Miss表查找Hit 抛出黑洞单播 Local 广播 Anycast组播 路由类型 不可达 1静静 丢弃 路由/递交 报文 4黑洞禁止 233图 31-2 策略路由查找

31.1.2 选择路由表

内核可以基于以下参数作为在选择路由表时所用的策略:

源IP地址与/或目的IP地址

可以既指定源IP地址,也指定目的IP地址,每个地址有相应的网络掩码。

Ingress设备

在某些情况下,接收设备可能是比源IP地址更适合的路由策略标准。虽然存在同一个源IP地址可能从多个接口进入的情况,但我们还是愿意配置为根据接收设备来选择。例如从某一个设备进入的流量被认为是实时,有更高优先级,这时源IP地址就无法发挥作用。在以下这些情况下使用设备而不是源IP地址更为可取:

● 当同一个设备上有多个、范围不连续的源IP地址,我们希望将这些地址与同一张路由表相关联。这时并不对每个不同范围的IP地址添加一条规则,而是基于设备使用一条规则来简化配置。

● 当选择路由表时,针对物理网络拓朴要比流量来源做更多处理时。

第31章 高级路由 Page 31-5

Understanding Linux Network Internals 第七部分——路由

TOS

与基于流量的源地址与目的地址参数相比,使用TOS参数有助于划分流量类型(例如大批量数据、交互式等等)。

Fwmark

这是一个表示Linux防火墙威力的一个特性。可以根据防火墙分类来定义策略路由规则。当然,要使之成为可能,防火墙就要在流量进入路由系统之前进行分类。参见“策略路由与基于防火墙的Classifier”小节。

前面提到的这些参数可以任意组合来确定路由策略。

31.2 多路径路由背后的概念

多路径是指管理员可以为一条路由的目的地指定多个下一跳。这样做的原因是存在多种现实需求。一个路由器大多数时间只用一个ISP,当由于某种原因第一个ISP失效时转向另一个ISP。多路径的另一个应用是保持一条路径为待命(standby)状态,只有在带宽需求超过预定的门限值时才使能该路径。

图31-3给出了一个拓朴,左侧网络通过路由器RT与因特网相连,RT通过两条上行链路与两个不同的ISPs同时相连。

我们假定RT使用RT1和RT2作为缺省网关,且二者总是可用。我们在RT上可定义一条多路径路由,该路由有多个下一跳。利用更新式的IPROUTE2软件包,输入以下用户空间命令来配置多路径路由:

ip route add default scope global nexthop via 100.100.100.1 weight 1 nexthop via 200.200.200.1 weight 2

图 31-3 多路径拓朴举例

注意虽然这条路由有多个下一跳,但仍然被视为一条路由。因此,给定了一条有多个下一跳的路由(在我们例子中,缺省路由为0.0.0.0/0),内核在每次路由查找时需要一种机制来选择下一跳。这可以有多种方法来实现,但每一种方法有各自的优缺点。对多路径路由常用算法分析感兴趣的读者,建议阅读RFCs 2991和2992。

第31章 高级路由 Page 31-6

Understanding Linux Network Internals 第七部分——路由

Linux通过提供weight关键字,使管理员为每个下一跳分配一个权值,从而提供了很大的灵活性。下一跳被选择的次数与其权值对所有其他下一跳的权值比例成正比。如果所有的下一跳分配的权值相同,那么,该算法就成为所谓的等价多路径算法(equal cost multipath algorithm)。

但是请注意,在下一跳之间分配流量的粒度不是以报文数来衡量,而是以路由缓存表项的数量来衡量。这是因为一旦选择了一个下一跳,就向缓存内添加一条表项。因为路由子系统在查找路由表之前总是先查找缓存,所以属于该流的后续报文将直接根据缓存来处理。就象在第36章所解释的那样,一个流(flow)是匹配一组标准的报文集合,这些标准主要由源地址或目的地址、ingress设备或egress设备、IP头部TOS字段等组成。在“基于流、基于连接和基于报文来分配流量”小节中,你将看到当缓存支持多路径时,流量(traffic)也可能是基于每个连接而不是基于每个流(flow)来分配的。

从纯粹的吞吐量来看,这种粒度可能是次优的。因为不同的流可能对带宽的需求不同,因而当所有的下一跳配置的权值相等时,内核可能造成不公正,更为糟糕的是,这种不公正可能是不确定的。所以Linux提供了一个选项,该选项以每个报文为粒度,而不是以每个流为粒度(参见“均衡算法”小节)。但大多数情况下,由于经过一个路由器通常有大量的流,平均来看,下一跳被选中的概率与其权值成比例关系。

31.2.1 选择下一跳

根据加权轮转算法(weighted round-robin algorithm)来选择下一跳。

在前面小节的一个例子中,使用一个用户空间命令为每个下一跳指定一个权值。管理员通常为每一条路径分配一个权值来表示其选择程度。该权值在轮转算法中使用。权值的确定是由管理员根据诸如带宽、费用等因素决定,所以这里不详细讨论。

选择下一跳最容易的方法是,每个下一跳一个接一个消耗掉它所占用的tokens,然后重新开始新的一轮。例如如果有两个下一跳,权值分别为3和5,我们将选择第一个下一跳三次,然后是第二个下一跳五次,接下来又是第一个下一跳三次,等等。但采用这种方法来分配流量可能过于突然(bursty)。

所以Linux添加了一个随机因素来选择下一跳。假定第i个下一跳的权值为Wi,所有下一跳的权值总和为W,Linux每W次随机选择一个下一跳,每个下一跳被选择次数等于其权值Wi。引入的这种随机性不太准确,但它是可被接受的一种近似。当所有下一跳分配的权值都为1时就相当于简单的连续选择(从第一个下一跳到最后一个)。

下面给出如何实现加权轮转算法。内核定义轮转预算(budget)为所有下一跳的总和。每个下一跳的预算初始值为该下一跳的权值。在每一转时,内核生成一个范围在0到轮转预算之间的一个值,然后遍历下一跳列表,直到找到某个下一跳的预算大于或等于该随机值。在一个下一跳被选择后,将轮转预算和被选中下一跳的预算都递减。

注意在第一转时可能找不到匹配的下一跳。想象有三个下一跳,权值分别为1、2、3,那么总预算为6。有效的随机值的范围为0到5,但随机值4和5将不会选出下一跳,因为每个下一跳的预算值都没有这么大。当出现这种情况时,内核从总预算中减去每个不匹配下一跳的权值,然后再次检查。

我们继续前面的例子来看如何工作。假定生成的随机数为5。我们开始遍历下一跳列表。第一个下一跳的权值为1,该值不够大,因此不被选中。将随机值减少为5-1,即4来降低我们的需求。接下来的下一跳权值为2,该值又不够大。因此我们再次降低随机值为4-2,即2。最后一个下一跳的预算为3,比2大,因而被选中。这从性能上讲是最坏情况:最后一个下一跳被选中。

当一条多路径路由的某些下一跳暂时不可用时(参见第35章“多路径对下一跳选择的影响”小节),这些下一跳当然不会被下一跳选择算法考虑。

第31章 高级路由 Page 31-7

Understanding Linux Network Internals 第七部分——路由 31.2.2 缓存支持多路径

缺省情况下,路由缓存不支持多路径。所以,就象我们在“多路径路由背后的概念”小节中看到的,一旦在“选择下一跳”小节中的算法选中一个下一跳,该下一跳将被随后与该相同查找key匹配的所有流量使用,因为一条引用该下一跳的路由已经被添加到缓存内。

从版本2.6.12开始,Linux内核有一个选项可以使用户使能缓存支持多路径,而且允许系统管理员针对一条多路径路由,选择算法在此路由的不同下一跳之间分配流量。

下面给出可用的算法:

随机算法

随机选择使用的下一跳。该算法由于无需保持任何状态信息所以速度很快。平均来讲,该算法对每一个下一跳分配的流量相同。

加权随机算法

下一跳被分配有一个权值,该算法对每一个下一跳分配的流量与其权值成比例。

轮转算法

标准的轮转算法,按照顺序将每个下一跳分配给下一条路由。

设备轮转算法

不是基于路由来分配流量,而是在接口上按照轮转方式来分配流量。共享同一个设备的多个下一跳被视为一个单元。

当利用IPROUTE2中的ip route命令来配置路由时,可以用新的mpath关键字来选择算法。在下面路由配置例子中选择的是轮转算法:

ip route add 10.0.1.0/24 mpath rr nexthop via 192.168.1.1 weight 1 nexthop via 192.168.1.2 weight 2

当没有指定mpath关键字时,该路由不支持多路径缓存。

在下面小节中,将详细讨论加权随机算法和设备轮转算法。

31.2.2.1 加权随机算法

假定我们有一条多路径路由,它有四个下一跳,权值分别为1、1、2、4。我们在图31-4中将四个权值沿一条线排列。权值总和为8,所以如果产生一个在范围0-8内的随机数时,就可以确切标识出线中排列的下一跳,值2.8将选择第三个下一跳。很显然下一跳被选择是与其权值成比例的。

1 1 24

8 2.8图 31-4 加权随机选择举例

第31章 高级路由 Page 31-8

Understanding Linux Network Internals 第七部分——路由 31.2.2.2 设备轮转算法

一条多路径路由的所有下一跳可以通过一个设备到达,每个下一跳也可以是通过一个不同的设备到达,也可以是混合情况。这三种情况在图31-5中给出。

一个纯粹的轮转算法将流量平均分配给下一跳,但对与下一跳相关联的设备则不一定是平均分配。例如在图31-5(c)中,一条多路径路由有三个下一跳,其中两个下一跳共享一个egress设备,这将使两个egress设备中的一个设备分配的流量是另一个的两倍。

因而,设备轮转算法就是要将流量在一组设备之间平均分配,而不是基于每条多路径路由来分配流量。对配置了使用该算法的路由项,与这些路由项匹配的流量被视为单个汇聚流量,它们被平均分配给一些设备。所以,对于一条给定的多路径路由,决定使用哪个设备不仅依赖于该路由项流量以前所用的设备,而且也依赖于其他多路径路由项流量所用的设备。

注意,虽然一个纯粹的轮转算法假定转发路径中的瓶颈是目标路由器的CPUs,但设备轮转算法目标是优化设备带宽的使用,以及降低目标CPU的重要性。

第31章 高级路由 Page 31-9

Understanding Linux Network Internals 第七部分——路由

图 31-5 分配下一跳给接口的几种不同方式

第31章 高级路由 Page 31-10

Understanding Linux Network Internals 第七部分——路由 31.2.3 基于流、基于连接和基于报文来分配流量

给定一条多路径路由,与该路由项匹配的流量可以基于流、基于连接和基于报文在下一跳之间分配:

基于流

根据源IP地址和目的IP地址的组合来选择下一跳。所以,在主机之间地址对(即源地址和目的地址)相同的多个连接将只选择一个下一跳。

基于连接

每当启动一个新连接时选择一个下一跳。这意味着在主机之间地址对相同的多个连接可以分配给多个下一跳。一个连接一般是由源IP、目的IP、四层协议、四层源端口和四层目的端口五个元素来标识。

基于报文

根据每个报文来选择下一跳。属于相同连接的报文可以分配给多个下一跳。

为了使诸如TCP等面向连接的协议工作正确,需要基于连接和基于流来分配流量。而为了使诸如UDP等无连接协议工作正确,需要基于报文来分配流量。

当不支持多路径缓存时,Linux总是基于流来将流量在多路径路由的不同下一跳之间分配,参见“多路径路由背后的概念”一节。

当多路径缓存使能时,根据来源不同来分配流量。

本地生成的流量

本地生成的流量利用在“缓存支持多路径”小节中给出的算法,基于连接来分配。

需要转发的Ingress流量

需要转发的Ingress流量的分配就象不支持多路径缓存一样:总是使用第一条匹配的缓存路由项。这对于减少在任意给定连接上的IP报文到达目的主机时顺序出错的概率是很有必要的。

31.2.3.1 均衡算法

Linux内核曾经提供基于报文的分配,这被称为平均化(equalization)。下面给出一个例子,命令中可通过eql选项来平均化路由(当前的Linux内核还没有实现该选项):

# ip route add eql 100.100.100.0/24 nexthop via 10.0.0.2 nexthop via 10.0.0.3 # ip route list

100.100.100.0/24 equalize

nexthop via 10.0.0.2 dev eth0 weight 1 nexthop via 10.0.0.3 dev eth0 weight 1 ...

自从宣布支持该选项以来已经过去了很长时间,但可能因为没有需求,因而该选项以后不大可能被添加到内核中。

31.3 与其它内核子系统的交互

从一个报文出现在系统中,这时它是从某个接口收到或是本地生成,到它被送给下一跳(如果转发)或是送给本地(如果目的地为本地主机)这一段时间内,可能有多个网络子系统要处理该报文。这其中就包括防火墙子系统和流量控制子系统。这两个子系统可以根据各第31章 高级路由 Page 31-11

Understanding Linux Network Internals 第七部分——路由

种信息数据库将流量分类,并将分类结果存储到缓冲描述符(译者注:这是指sk_buff)的一个字段。路由子系统也可以将流量分类并将结果存储到缓冲描述符。

图31-6给出的是一张简化图,描述了路由、防火墙和流量控制三个子系统之间如何交互,以及一个子系统何时出现在图中。这张图给出了一个入报文是如何通过各种子系统,以及如何初始化其防火墙标签(tag)和路由标签的。

在接下来的几个小节中,我们将进一步描述策略路由和防火墙是如何计算各自的tag,并将它们提供给其它内核子系统使用。

31.3.1 基于路由表的Classifier

在流量控制子系统可用的许多classifiers中,有一个被称为基于路由表的classifier,它能够基于realms来将路由分类。Realms是可以在策略和路由上指定的数字标签。每一条路由和每一个策略最多可以指定两个realms:一个是ingress realm,一个是egress realm。

在接下来的小节中,为了更加熟悉realms特性,我们首先来看如何配置realms。然后我将描述路由代码根据realm配置得出分类tag(该tag将被流量控制子系统使用)所使用的逻辑。

在IPROUTE2软件包包含的文件ip-cref.ps中,给出了一些例子来描述realm意图和使用。在本书中,我们只考虑如何通过IPROUTE2命令来配置realms。

使用ip命令既可以配置路由,也可以配置策略。ip接下来的第一个关键字确定配置的对象类型。route关键字表示配置路由,而rule关键字表示配置策略。

图 31-6 路由、流量控制和防火墙(Netfilter)子系统之间的交互

第31章 高级路由 Page 31-12

Understanding Linux Network Internals 第七部分——路由 31.3.1.1 配置策略realms

利用IPROUTE2软件包中的ip rule命令来配置路由策略,其语法为:

ip rule add...realms [source_realm/]destination_realm

从上面的语法可以看出,源realm是可选的,而目的realm不是可选的。这表示如果只提供了一个值,那么配置的是目的realm。

下面用配置策略realm的一对命令来举例。下面这条命令将源于10.0.1.0/24子网的所有流量,与策略目的地realm 128相关联:

ip rule add from 10.0.1.0/24 realms 128

下面这条命令将源于0.0.1.0/24子网且目的为10.0.2.0/24子网的所有流量,与策略源realm 和策略目的地realm 128相关联:

ip rule add from 10.0.1.0/24 to 10.0.2.0/24 realms /128

31.3.1.2 配置路由realms

路由realms的配置与策略realms的配置很相似,其语法为:

ip route add...realms [source_realm/]destination_realm

注意:虽然该命令的帮助信息没有给出源realm和目的realm,但其语法与策略realm命令的语法完全相同。

下面以配置路由realm的一对命令来举例。下面这条命令将目的地为10.0.1.0/24子网且通过网关地址10.0.0.3转发的流量,与目的地realm 100相关联:

ip route add 10.0.1.0/24 via 10.0.0.3 realms 100

下面这条命令将目的地为10.0.1.0/24子网且通过网关地址10.0.0.3转发的流量,与源realm 100和目的地realm 200相关联:

ip route add 10.0.1.0/24 via 10.0.0.3 realms 100/200

31.3.1.3 计算路由标签

因为可以对每一条路由和策略来指定realms,那么对单个方向上的路由决策就可以得出两个realm:例如可以根据策略得出一个ingress目的realm,根据路由得出另一个ingress目的realm。如果出现这种情况,从路由得出的realm的优先级更高。通常情况下,该决定只对目的realm才有必要,管理员几乎不会根据路由来确定源realm。

如果路由或策略没有指定realm,内核将计算反向路由(从本地主机回到被分类的报文源),检查是否可以使用与该反向路由相关的realms。例如,如果内核针对ingress流量不能得出源realm,那么就根据反向路径上的egress流量来计算目的realm并使用它。这种计算是假定在ingress和egress方向上的realm配置应当是对称的。

我们使用图31-7中的拓朴来看一个简单例子,图中给出在两个网络之间的一台路由器。在该路由器上,策略路由配置为从子网10.0.1.0/24来的流量都属于Realm A,从子网

10.0.2.0/24来的流量都属于Realm B。假定没有配置路由realm,只有图31-7中给出的两个策略realm配置。这两个策略都只是提供了源realm,所以当转发时,可以为ingress方向上的流量指定realm,而egress方向上没有realm。我们现在假定该路由器收到一个源是主机10.0.1.100(Realm A)且目的地址为10.0.2.200(Realm B)的报文。当路由子系统为该报文查找路由时,也要计算路由标签。下面给出具体过程:

第31章 高级路由 Page 31-13

Understanding Linux Network Internals 第七部分——路由

1. 路由查找返回的是路由R2和策略P1。因为在路由R2上没有配置realm,所以使用策略P1上的源realm A。

2. 因为没有目的realm,内核计算从10.0.2.200到10.0.1.100的反向路由。这一次路由查找返回的是路由R1和策略P2。在匹配的路由R1上又是没有配置realm,所以内核依赖于策略realm,即realm B。但因为这是在反向查找时找到的,所以反向路径上的源realm B 被用做转发路径上的目的realm。

最终,路由tag被初始化为源realm A和目的realm B。当流量稍后通过QoS层时,QoS层可以使用这两个realm来将报文正确地分类。

图 31-7 realm配置举例

第31章 高级路由 Page 31-14

Understanding Linux Network Internals 第七部分——路由

图31-8总结了计算路由标签所用的逻辑。 Yes 使用它 Ingress流量only 计算反向路由 (从本地主机到源)策略路由only

路由有目的 realm? No 策略有目的 realm? No 对路由和策略都交换源与目的地realmYes 目的realm设 置?YesNo若路由/策略中有源realm,则用作目的realm 使用它策略有源 realm? No Yes 使用它源 realm设置?YesNo若路由/策略中有源realm,则用作目的realm源realm和目的 realm都设置?YesNo 结束图 31-8 计算路由tag所用的逻辑

31.3.2 策略路由与基于防火墙的Classifier

Netfilter防火墙软件可以根据自己的过滤条件将流量分类,判断是否需要将报文丢弃或破坏(mangle)。也可以将防火墙配置为利用其功能强大的分类引擎将报文简单分类,以便向其它内核子系统提供服务。在网络协议栈中防火墙有许多钩子函数。如果某个钩子函数计算了一个标签后,路由子系统或流量控制子系统运行,这些子系统可以看到并使用该标签。在本章前面的图31-6中,给出了各个子系统以及防火墙钩子函数处理报文的顺序。

31.4 路由协议守护进程

将路由插入到内核的路由表中可以通过三种途径:

● 通过用户命令来静态配置(例如ip route,route)

● 通过诸如边界网关协议(BGP)、外部网关协议(RGP)、开放式最短路径优先(OSPF)等路由协议来动态配置,这些路由协议在用户空间被实现为路由守护进程(routing daemons)。

● 由于配置不理想而由内核接收和处理的ICMP重定向消息。

第31章 高级路由 Page 31-15

Understanding Linux Network Internals 第七部分——路由

我们将在第36章讨论第一个途径,在本章后面讨论第三个途径,现在我们来看第二个途径,尤其是可用于Linux系统的路由守护进程。这些路由守护进程与内核的交互将在第32章中讨论。下面列出的路由项目不再被维护但仍然很有意思:

Routed

这是最古老的Unix路由协议守护进程。它只包含RIP协议的版本1和版本2(见RFC2453)。

GateD (http://www.gated.org)

包含大部分路由协议。这最初是由Merit GateD联盟发起的一个研究项目,但后来版权属于NextHop。该研究版本不再被维护。

BIRD (http://bird.network.cz)

由布拉格Charles 大学发起的一个项目,支持大部分常用的路由协议。

下面列出的路由协议套件仍然被维护并且被部署:

Zebra (http://www.zebra.org)

包含大部分路由协议,已经被广泛部署且其邮件列表仍然很活跃。但其发布周期已经变慢,导致出现了Quagga。

Quagga (http://www.quagga.net)

Zebra的一个分支,创建于2003年,其开发周期和BUG修复更快,并且可以向用户提供更多的文档。

XORP (http://www.xorp.org)

由加利福尼亚伯克利大学国际计算机科学委员会发起的一个新项目。

参考括号内的URL来查找每个软件包提供的具体协议以及扩展。

本书不涉及路由守护进程的实现,因为它们不属于内核,但我们简要来看看它们是如何与内核通信的。重要的是需要了解这些守护进程是如何将它们从其peer或用户配置学习到的路由插入到路由表中,以及如何删除不再有效的路由。

每一个路由守护进程是在用户空间维护自己的路由表。选择路由并不是直接使用这些路由表,而是根据内核中存储的路由表来选择路由。但就象在本节前面提到的那样,这些路由守护进程是内核路由表中路由项生成的一个途径,前面介绍的大部分守护进程都实现了多种路由协议。每一种路由协议在运行时维护各自的路由表。根据守护进程的设计,每个协议可以将自己路由表中的路由项插入到内核路由表中(参见图31-9中的左侧示意图),或是几个协议共享守护进程内的一个公共层来与内核通信(参见图31-9中的右侧示意图)。选用哪种方法要根据用户空间内的路由协议设计来选择,这已经超出本书范围。

路由协议和内核之间的通信是双向的:

● 路由协议将路由插入到内核的路由表中,当确定路由项过期或不再有效时删除路由项。

● 内核将插入路由、删除路由,以及本地设备链路状态改变(这当然间接影响到相关的所有路由)通知路由协议。这只有在路由守护进程与内核通过Netlink套接字通信时才有可能,因为Netlink套接字是一个双向通道。

第31章 高级路由 Page 31-16

Understanding Linux Network Internals 第七部分——路由

IPROUTE2软件包不仅允许用户配置路由,而且可以监听前面提到的由内核和路由守护进程生成的通知。这样,管理员可以将它们通过日志记录下来,或在屏幕上输出以用于调试。

路由表路由表 路由表路由表路由表 路由协议A路由协议B 静态配路由路由 路由 置(CLI)协议X协议Y 协议Z 公共接口 用户 用户/内核接口 内核 路由缓存 核心 路由表 图 31-9 用户空间与内核之间的接口

31.5 Verbose监控

当内核支持verbose监控选项且使能该选项(缺省为disabled)时,在输入报文的源IP地址或目的IP地址可疑或无效时,内核将在console上输出告警信息。这些告警信息输出速度被为每五秒钟输出一条信息,以避免可能的DoS攻击。

当Ingress报文由于源地址或目的地址错误而被路由子系统中的合理性检查丢弃时,将触发一个告警信息。内核可以使用第30章中表30-1和表30-2中列出的地址分类来做一些很容易的检查。总结一下,这些分类是:

● 源地址:多播、回环、保留地址、无效地址(零网络地址:zeronet)

● 目的地址:回环、保留地址、无效地址(零网络地址)

内核根据路由表要对ingress报文做更多的合理性检查,尤其是:

● 当使能反向路径过滤(一种反IP欺骗检查)时,源IP地址必须通过接收该报文的接口可达,参见“反向路径过滤”小节。

● 源IP地址不能是子网广播地址或接收接口上配置的地址。该检查可以防止IP欺骗(例如另一台主机声称拥有接收接口上配置的地址),并且可以检测出可能由于DHCP错误配置而引发的地址重复情况。

第31章 高级路由 Page 31-17

Understanding Linux Network Internals 第七部分——路由

当使能Verbose监控特性后,ICMP层在一些特殊条件下也可能生成告警信息:

发送ICMP重定向消息

当内核向一个远端主机发送一定数量的ICMP重定向消息而该主机忽略时,则内核输出一个告警信息。该数目可以配置,参见“发送ICMP重定向消息”小节。

接收ICMP重定向消息

一旦进入的ICMP重定向消息被拒绝,内核就输出一个告警信息。进入的ICMP重定向消息的处理要比发送较为复杂,因为内核可能由于多种原因而拒绝进入的ICMP重定向消息,其中一些原因可以由用户配置。参见“处理Ingress ICMP重定向消息”小节。

31.6 ICMP重定向消息

ICMP协议定义了许多不同消息来控制流量以及向主机通知网络问题。其中的一个消息为重定向消息,*它被用于向报文源通知使用的路由不是最优路由。参看第25章中对ICMP消息的详细描述。在本章中,我们主要来看由路由子系统提供的调整参数,判断是否处理

ingress ICMP重定向消息,以及当所需要的缺省条件满足时是否发送一个ICMP重定向消息。

决定是否应当发送一个ICMP重定向消息,以及是否应当处理一个ingress ICMP重定向消息,都会受到用户配置的影响。尤其是,用户的配置可以是:

● 是否接收ingress ICMP重定向消息或生成egress ICMP重定向消息。我们将在第36章看到,这可以在每一个设备上进行配置。

● 对于ingress ICMP报文,也可能指定是否只接收安全的重定向消息。当一个ICMP重定向消息中的新网关已经被本地主机知道且作为一个网关时,该消息被视为安全的。例如,这可以通过查找路由表,确定建议的网关是否已作为任意已配置路由项的下一跳来检查。

● 每个设备可以配置有一个flag,表示该设备是否与一个共享介质(shared medium)相关联。

最后一项需要一些解释,这在下一小节中描述。

31.6.1 共享介质

在19世纪90年代初期,IP协议设计者开始看到了在诸如以太网等介质上构建局域网,以及将这些局域网与其它网络相连的趋势(这在当时是新的趋势,而现在已经很普遍了)。管理员有时候将配置在不同IP子网的多组主机连到一个局域网内,而使用路由器将它们隔离。这样做是与历史上的多种原因相关或是由于便利,而现在几乎没有任何理由这样做,而且也几乎找不到这样的做法。然而,路由子系统在设计时必须处理它。

当配置为不同子网的主机被连到同一个局域网时,IP路由文档称之为共享介质(shared media)。注意这个术语是指网络配置而不是设备能力。换句话说,在常见的共享一个二层连接的所有主机也共享一个IP子网情况下,该术语不适用。这里也不关心这些话题;在本章中,我们所关心的是更有可能生成ICMP 重定向消息的情况。

图31-10给出了一个共享介质的例子。在同一个局域网相连的主机被配置为三个不同子网。两个路由器RT1和RT2被用于连接IP子网:每一个路由器将两个子网分开,在每一侧各有一个NIC配置有一个地址。

* ICMP重定向消息有四个子类。本章只讨论将流量重定向到一个特定IP地址的主机重定向。其它子类可参看第25章。

第31章 高级路由 Page 31-18

Understanding Linux Network Internals 第七部分——路由

接下来,描述图31-10中配置路由的典型方法(虽然这不是唯一的配置方法)。

图 31-10 共享介质拓朴上配置举例

子网10.0.0.0/24的主机将RT1定为自己的缺省网关,通过该网关将流量送给其它子网。通过RT1可以到达其它两个子网,以及访问因特网的网关。

子网10.0.1.0/24的主机有类似的配置,将RT2定为自己的缺省网关。但它们需要另外一条通过RT1到达子网10.0.0.0/24的路由。

子网10.0.2.0/24的主机将DG定为自己的缺省网关,并配置有两条通过RT2到达其它两个子网的路由。

这个路由环境的关键之处在于指定的路由效率很差。例如子网10.0.1.0/24和10.0.0.2/24的主机可以通过二层交换报文而不需要任何路由,但配置要求它们使用两个路由器。一个更为聪明的配置是只在主机上配置缺省网关,由缺省网关来处理局域网中到其它子网的路由。

幸运的是,路由子系统有了克服自己这种低效率的办法,可以逐步找到直连主机。这种机制就是ICMP重定向消息。我们将忽略DG以及因特网连接的存在。

假定主机A想要与主机B通信。主机A根据自己的路由表,看到通过路由器RT1可以到达主机B。然而,当RT1收到主机A发送给主机B的报文时,它会意识到主机A可以直接发送报文给主机B,因为它们(主机A和主机B)通过同一个设备eth0是可达的。这看上去就象触发ICMP重定向消息的经典条件:路由不理想。但是有一个条件:虽然主机A和主机B连到同一个共享介质并且因此能够从链路层角度上(即以太网)相互通信,但是从IP层来看是不可能通信的。主机A不知道通过eth0可以到达10.0.1.0/24子网的主机,主机A所知道的就是通过RT1可以到达子网10.0.1.0/24。

为了理解这其中的原因,如果必要的话可返回参看第26章中的“何时发送和处理免费请求(Solicitation Request)”小节。一台主机要在三层地址基础上与另一台主机通信,首先必须根据三层地址解析得到二层地址。一个发送方只能对与它相连的属于同一子网的主机做这种解析,而对于其它情况下的查找主机则是路由器要做的事情。在我们这个例子中,主机B不属于主机A的子网。这意味着如果一个ICMP重定向消息告诉主机A可以与主机B直接通信,这些主机必须能够接受所谓的外部重定向(foreign redirect):一个重定向建议新的下一跳不属于本地子网,但重定向的接收方知道该下一跳。

外部重定向只有在类似图31-10中描述的共享介质情况下才有意义。这是因为在主机A收到一个重定向消息并接受之后,发送一个ARP来请求主机B的地址。*在图31-10的拓朴中, * 我们前面提到一个IP主机永远不会对不属于本地配置子网的IP地址发送ARP请求。注意这里描述的行为(即一个主机向位于外部子网的IP地址发送ARP请求)是可能的,因为在路由缓存中包含有重定向路由,所以可以绕过路由表(该表不可能向IP发送ARP请求)。

第31章 高级路由 Page 31-19

Understanding Linux Network Internals 第七部分——路由

正是由于共享介质,主机B将接收到该ARP请求。如果主机B位于另一个局域网内,将不会收到来自主机A的任何ARP请求。*

看看如果主机A想要与主机C通信时使用RT1发生的有意思的情况。RT1知道通过RT2可以到达主机C,所以RT1发送一个ICMP重定向消息给主机A,建议RT2为新网关。当RT2稍后收到主机A向主机C发送另一个报文的请求时,RT2也将检测到相同的次优路由条件。所以RT2将发送一个ICMP重定向消息,主机A将最终认识到它可以直接到达主机C。简言之,通过外部重定向来解析共享介质连接的过程是一个反复过程。

在这一小节中,你已经看到对连到同一共享介质的不同子网的主机进行配置所暗含的细节。RFC 1620更为详尽地探讨了该话题,值得好好阅读。在接下来的两个小节中,你将看到用户对连到一个共享介质的接口进行明确配置,是如何影响到ingress ICMP重定向消息的发送和接收,以便与本小节中类似的环境都可以正确地处理。

31.6.2 发送ICMP重定向消息

当路由子系统看到入设备和出设备已经匹配时,还检查共享介质配置似乎是多余的。实际上,这种检查不是多余的。在前一小节描述了使用ICMP重定向消息而造成的这种转发捷径,可能并非总是人们所期望的。这些捷径使主机绕过路由器,但管理员可能在这些路由器上要对所有流量应用一些策略,一个例子就是当RT1和RT2为防火墙†或代理主机时的情况。

图31-11给出了在路由一个需要转发的ingress报文时的路由代码逻辑。该流程图中只有一个部分还没有描述:当设备没有配置为共享介质时是如何检查的。在这种情况下,只有在发送方(通过源IP地址来标识发送方)与通过路由表查找到的下一跳属于同一子网时,才向该发送方发送ICMP重定向消息。当上述条件不满足时,发送方将不能够(依据路由器的知识)到达新的下一跳。这就是为什么在检查网关是相同子网之前先要检查共享介质。

No 入设备= 出设备?

Yes

Yes是NAT或 MASQ路由?

No

新网关与发送方No设备被配置为No 在同一子网?共享介质? Yes Yes 发送ICMP重定向 不发送ICMP重定向

图 31-11 需要生成一个ICMP重定向的条件

注意在内核2.6中已经不再支持快速NAT(参见第32章“最近废弃的选项”小节),所以对NAT/MASQ将检查将永远不成功。

有意思的是注意IPv6有优化的ICMP重定向。在IPv6情况下,重定向消息包含了建议的新网关的二层地址。所以一个收到ICMP重定向消息的主机不需要做从三层地址到二层地址的解析,就知道新网关的地址。 †

在图31-10中,所有主机共享一个二层广播域,在类似这样的环境中使用防火墙无论如何来讲都是一个很差的选择。但这可以使你理解为什么人们并不总是期望有路由捷径。

*第31章 高级路由 Page 31-20

Understanding Linux Network Internals 第七部分——路由

参见第20章和第35章“转发”小节中关于ip_forward程序如何确定是否发送ICMP重定向消息的细节。而且,发送ICMP消息的更多细节可以参见第25章。

31.6.3 处理Ingress ICMP重定向消息

要使一个ingress ICMP重定向消息被接受,就需要经过一些合理性检查并且要与用户配置一致。图31-12用一个流程图给出了所有相关的逻辑。我们一个一个来看:

首先,需要通过一些基本的合理性检查并且与用户配置一致:

● ICMP重定向消息中的新网关应当不同于当前网关(否则不需要ICMP重定向)。

● 新网关的IP地址不能是多播地址、无效地址(零网络地址)或保留地址。

● 接收接口必须被配置为接受ingress ICMP重定向消息。

第二,路由子系统必须处理共享介质配置:

如果设备没有被配置为共享介质

这时,只有当主机根据路由表知道新网关与旧网关位于同一子网时(即它与该主机为直连),才能够接受这个新网关。

如果设备被配置为共享介质

只有当主机根据路由表知道如何到达该新网关时,才能够接受这个新网关。执行另外两项合理性检查:该网关地址一定不能是主机的本地地址,不能是广播地址。

就象在“共享介质”小节中已经提到的,可以配置该设备,以便只有当设备知道这个新网关已经是一个网关时,才可以接受重定向。

ip_rt_redirect函数基于本小节描述的逻辑来处理ingress ICMP重定向消息,具体分析参见第25章。

第31章 高级路由 Page 31-21

Understanding Linux Network Internals 第七部分——路由

1

No通过基本的 合理性检查?YesNo设备被配置为 ingress 处理ICMP重定向?YesNo设备被配置为Yes 共享介质?知道新网关是一 个远端单播主机地址? No No 新网关为直连?YesYesYes 设备被配置为只 接受安全ICMP重定向消息? No1该ICMP 重定向消息安全? NoYes 用户配置拒绝 接受 图 31-12 需要处理ingress ICMP重定向的条件

31.7 反向路径过滤

我们在第30章“路由的基本要素”小节中看到什么是非对称路由(asymmetric route)。

非对称路由并不常见,但在某些特定情况下可能就有必要。Linux的缺省行为是质疑非对称路由,因而将根据路由表丢弃那些源IP地址通过接收接口为不可达的报文。然而,可以通过/proc来调整每一个设备的这种行为,我们将在第36章讨论这一点,也可以参见第35章“输入选路”小节。

在第30章的图30-7(a)和(b)中,我们看到一个恶意用户利用其同一子网内另一台主机地址作为源IP地址来发送ICMP ECHO REQUEST消息的例子。图31-13给出了另一个恶意用户,这次他使用的是目的子网内的一个地址(也就是他的受害者)作为源IP地址来发送ICMP ECHO REQUEST消息。如图31-13(a)所示,Linux缺省可以检测出这种企图,缺省将这种报文丢弃。图31-13(b)展示了如果ICMP ECHO REQUEST消息没有被路由器RT丢弃所发生的情况。

第31章 高级路由 Page 31-22

Understanding Linux Network Internals 第七部分——路由

图 31-13反向路径过滤举例

上述例子使用的是定向广播ICMP报文,但反向路径过滤(reverse path filtering)可以适用于任意类型的报文流。

第31章 高级路由 Page 31-23

Understanding Linux Network Internals 第七部分——路由

第32章 路由在Linux中的实现

第30章概要介绍了路由子系统的主要任务,第31章介绍了在基本路由功能之上IP实现诸如策略路由和多路径等特性。在本章,将介绍路由代码用到的主要数据结构,然后将描述:

● 如何定义路由scope和IP地址scope

● 如何初始化路由子系统

● 哪些外部事件需要通知路由子系统,以便更新路由信息

后面的章节将详细描述路由缓存、路由表和路由查找。

32.1 内核选项

就象我们将在本章后面看到的那样,路由过程(routing)并不仅仅包含从一个接口接收到一个报文后,查找路由表,将该报文从正确的出接口转发出去这些内容,同时还需要考虑其他一些任务。在Linux内核中已经实现了与路由相关的许多有意义的特性。而且我们在本章后面还将看到,还有许多特性正在等待来自Linus或其他子系统owner的认可,以便集成到内核中。

这里,简要介绍影响路由代码行为的一些Linux内核特性,以免读者在仔细阅读源代码时受到混淆。在本章和接下来的章节中将会进一步描述每一个特性。

路由选项可以划分为两类:

● 一类是永远支持的,只需要用户通过诸如/proc等来配置或使能的选项。

● 另一类是重新编译内核时可以添加或删除的选项。可以将

CONFIG_WAN_ROUTER选项和CONFIG_IP_ROUTE_MULTIPATH_CACHED菜单下的选项编译为模块,而其它的选项必须被编译进主内核。

图 32-1 内核配置(make xconfig)

第32章 路由在Linux中的实现 Page 32-1

Understanding Linux Network Internals 第七部分——路由

我们将在本章和接下来的章节中看到这两类选项,但本小节内我们首先来看编译时选项。这些选项可以利用网络选项菜单来配置,如图32-1所示的在执行make xconfig时的一个截图。

下面两个小节在括号内给出与每个选项相关的CONFIG_XXX符号表示的内核配置,可以利用这些符号来区别那些只有在内核支持该特性条件时才执行的代码。但是当一个文件被某个特性限定时,在该文件内找不到CONFIG_XXX符号。

32.1.1 基本选项

下面给出一些基本路由选项,这些选项在本章都不涉及。

IP:多播路由(CONFIG_IP_MROUTE)

IP:PIM-SM版本1支持(CONFIG_IP_PIMSM_V1)

IP:PIM-SM版本2支持(CONFIG_IP_PIMSM_V2)

如果使能IP多播路由,可以选择使能内核支持的PIM(Protocol Independent Multicast)协议的两个版本中的一个。本书不考虑多播路由。

WAN路由器(CONFIG_WAN_ROUTER)

该选项可以在广域网设备上配置X.25、帧中继、HDLC及其它非IP协议来进行路由。在内核配置菜单中,可以在“网络设备支持Æ广域网接口”下方看到可用的驱动列表。为了配置广域网设备,需要下载部分代码,这些代码在常见的Linux版本发布中默认是不包含的。*本书不考虑广域网路由。

32.1.2 高级选项

如果在网络选项菜单中使能“IP:高级路由器”选项,则可以使能更多的选项。在第31章中已经介绍了每一个选项,下面再简要概述一下:

IP:策略路由(CONFIG_IP_MULTIPLE_TABLES)

在一些情况下,流量处理除了依据目的IP地址以外,还必须依据其它一些条件。策略路由是回答该限定条件的一种答案。在这些情况下,必须增强路由代码以考虑更多的参数。参见第31章“策略路由背后的概念”小节。

IP:使用netfilter MARK值作为路由key(CONFIG_IP_ROUTE_FWMARK)

当使能该选项时,路由表查找时可以考虑由防火墙设置的一个标签。参见第31章“策略路由与基于防火墙的Classifier”小节。只有先使能“IP:策略路由”选项才可以看到该选项。

IP:等价多路径(CONFIG_IP_ROUTE_MULTIPATH)

有时侯,一条路由可以有多个下一跳,这时在所有路由上都发送流量将消耗大量带宽,这正是该特性需要处理的。在第31章“多路径路由背后的概念”小节中,我们看到实现该特性并不象它字面意思那么简单。

*

参见文件documentation/networking/wan-router.txt。

第32章 路由在Linux中的实现 Page 32-2

Understanding Linux Network Internals 第七部分——路由

IP:缓存支持的等价多路径(CONFIG_IP_ROUTE_MULTIPATH_CACHED)

支持将多路径路由添加到路由缓存内,只有在前一选项使能时才可以选择该选项。当选择这个选项时,将看到一个子菜单,列出各种从缓存路由内选择下一跳的算法。参见第31章“缓存支持多路径”。

IP:详细的路由监控(CONFIG_IP_ROUTE_VERBOSE)

代码中会有一些地方检测到一些稀奇古怪的条件,例如在流量处理过程中进行合理性检查时。在这些情况下输出一些告警信息是非常有用的,这正是该特性的目的。为了避免DoS攻击,将这些告警信息的输出限速为每5秒钟输出一条。

32.1.3 最近废弃的选项

下面给出在2.4系列中内核支持,而在2.6系列中被废弃的一些选项:

IP:快速网络转换(CONFIG_IP_ROUTE_NAT)

NAT是一个在路由器上配置的典型特性,它基于特定的配置修改转发的IP报文的源IP或目的IP地址。由路由代码实现的NAT与由防火墙代码实现的NAT无关,因而该选项被认为是多余,在内核版本2.6.9中被完全删除。

快速交换(CONFIG_NET_FASTROUTE)

该特性允许在设备驱动层直接转发NIC之间的数据流。从出接口转发的报文不必经过更高层(IP层),因而不需要查找路由表。在第30章的图30-1中,该特性由驱动框中的点线表示。目前只有NIC的一个系列,即Tulip卡支持该特性。*

该特性在内核版本2.6.8中被删除,它与其它重要特性不兼容的简单原因在于,它属于低层交换而将绕过这些重要特性。这些重要特性的例子有Netfilter防火墙、高级路由、虚拟设备:即bonding。

高速接口间转发(CONFIG_NET_HW_FLOWCONTROL)

该特性可以使网卡根据其内存中可用的缓冲空间大小,决定开始或停止发送由内核向它送来的报文。但并不是所有的网卡都支持该特性,支持该特性的网卡例子为Tulip系列网卡(drivers/net/tulip/*)。由于第10章中描述的NAPI的引入,这个有意思的但几乎不被使用的特性已经从内核版本2.6.10中被删除。

32.2 主要的数据结构

路由代码使用了大量的相互引用的各种数据结构。为了理解当前的路由代码以及未来的改进,重要的是需要清晰地了解这些数据结构之间的关系。

任何代码性能都会由于数据结构和代码架构设计的选择受到明显影响,这对于内核代码来说尤其如此。诸如路由等内核子系统是网络协议栈的核心,因此要确保它们不仅能提供稳健的功能,而且要考虑性能。我们将在下面的章节看到本小节中列出的这些数据结构是如何相互结合,从占用CPU和RAM以及缓存等各个方面来看,都更容易实现算法的优化。

下面给出路由代码中定义和使用的主要数据结构并做简单描述,在第36章中对那些最重要的数据结构则一个个字段详细描述。在数据结构名称中的rt,fib与fn前缀分别表示路由(route),转发信息库(Forwarding Information Base)和功能(function)。 你可以下载一个补丁应用于内核,使Tulip 8390卡支持2.4内核中的“快速交换”特性。如果要使能该特性(例如用make xconfig),可以点击帮助窗口内提供的到该补丁的链接。

*

第32章 路由在Linux中的实现 Page 32-3

Understanding Linux Network Internals 第七部分——路由

struct ip_rt_acct

该结构被基于路由表的classifier使用(参见第31章“基于路由表的Classifier”小节),用于跟踪与一个标签(tag)相关联的路由流量的统计信息,该统计信息中包含字节数和报文数两类信息。这个结构包含一个counters数组,每个处理器有256个元素。*大小为256是因为路由标签的取值范围为0到255。IPv4中是由ip_rt_init接口为该向量分配空间,IPv6中没有为该向量分配空间。ip_rt_acct结构中的四个字段是在ip_rcv_finish接口中更新,详见第35章“策略路由与基于路由表的Classifier”小节。

struct rt_cache_stat

存储路由查找的统计信息。对每个处理器有该数据结构的一个实例。虽然这个结构名称好象是只计数与路由表缓存相关的counters,但一些实例被用于更通用的路由查找的统计,参见第36章“统计”小节。

struct inet_peer

维护远程IP peers长期存在的信息。该数据结构在第23章“长期存在的IP peers信息”小节中描述。

struct fib_result

查找路由表返回该结构。它的内容并不是简单地包含下一跳信息,而且包含由诸如策略路由等特性所需要的更多参数。

struct fib_rule

表示由策略路由选择相应路由表的规则,参见第31章“策略路由背后的概念”小节。

struct flowi

flowi从某种程度上讲类似于访问控制列表(ACL):它是基于从L3与L4包头中选择的诸如IP地址、L4端口号等字段值来定义一个流量集。可以将它用做路由查找时的搜索key。

下面给出构造路由表的数据结构,这些结构之间的关系在第34章中有更详细的阐述。

struct fib_node

一条路由表项。例如,该数据结构用于存储由route add或ip route add命令添加一条路由时生成的信息。

struct fn_zone

一个zone表示网络掩码长度相同的一组路由表项。因为网络掩码占用32比特(对IPv4而言),因而每个路由表有33个zone。这样,到达子网10.0.1.0/24与10.0.2.0/24的路由项都将放在24比特的zone链表(即第25个zone)内,到达子网10.0.3.128/25的路由项将放在25比特的zone链表内。

struct fib_table

表示一张路由表。不要将它与路由表缓存混淆。

*

不要将数据结构和有着相同名字的数组混淆。

第32章 路由在Linux中的实现 Page 32-4

Understanding Linux Network Internals 第七部分——路由

struct fib_info

不同路由表项之间可以共享一些参数,这些参数被存储在fib_info数据结构内。当一个新的路由表项所用的一组参数与一个已存在的路由项所用的参数匹配,则复用已存在的fib_info结构。一个引用计数用于跟踪用户数量,参见第34章中图34-1所示的例子。

struct fib_alias

到达同一目的网段但诸如TOS等参数不同的路由表项是通过fib_alias实例来区分的。

struct fib_nh

下一跳。如果使用诸如ip route add 10.0.0.0/24 scope global nexthop via 192.168.1.1命令定义一条路由,那么下一跳为192.168.1.1。一条路由表项一般只有一个下一跳,但当内核支持多路径特性时,那么,就可以对一条路由项配置多个下一跳。参见第31章“多路径路由背后的概念”一节。

struct fn_hash

该结构内包含指向33个fn_zone链表的头指针,以及将活动zone(active zone,即那些至少有一个元素的zone)链接在一起的一个链表。后面一个链表中的元素按照网络掩码长度的降序排列,参见第34章中图34-1。

下面这些数据结构用于与协议相关的路由缓存代码和与协议无关的路由缓存代码(DST),更详细的描述可参见第33章。

struct dst_entry

路由表缓存项中与协议无关的部分(DST)。适用于任意三层协议(例如IPv4, IPv6, DECnet)的路由表缓存项字段被放在该结构内。在三层协议所用到的数据结构内,通常嵌入该结构来表示路由表缓存项。

struct dst_ops

DST核心代码使用虚函数表(Virtual Function Table ——VFT)向三层协议通知特定的事件(例如链路失效)。每个三层协议各自提供一组函数,按照自己的方式来处理这些事件。VFT的每一个字段并不是被所有的协议都使用,参见第33章中“DST与调用协议之间的接口”小节。

struct rtable

IPv4中表示一条路由表缓存项的数据结构。*

下面的数据结构通常是用于配置:

struct kern_rta

当内核接收到来自用户空间中一条IPROUTE2命令要求添加或删除一条路由时,内核解析请求并存储到kern_rta结构内,参见第36章“inet_rtm_newroute与inet_rtm_delroute函数”小节。

*

IPv6中使用struct rt6_info。

第32章 路由在Linux中的实现 Page 32-5

Understanding Linux Network Internals 第七部分——路由

struct rtentry

当使用route命令,向内核发送添加或删除路由表项的请求时所用的数据结构。IPROUTE2中的ip route 命令使用的是另一个不同的数据结构。

struct fib_iter_state

当遍历组成一张路由表的数据结构实例时存储的上下文信息,在处理/proc接口的代码中使用该数据结构。

下面的数据结构用于多路径缓存特性,这些结构在第33章中描述:

struct ip_mp_alg_ops

由函数指针组成的多路径缓存算法。

struct multipath_device

在设备轮转缓存算法中用于跟踪设备信息。

struct multipath_candidate

struct multipath_dest

struct multipath_bucket

struct multipath_route

在加权随机缓存算法中用于跟踪算法所需的状态信息。

32.2.1 链表与哈希表

路由代码用到了另外两个通用的数据结构。在本书的这一部分将会经常看到它们,所以这里简单介绍一下。

hlist_head

hlist_node

哈希表是由这两种类型的数据结构来实现的。哈希表的桶用hlist_head类型来定义,添加到哈希表中的实际元素中要嵌入一个类型为hlist_node的元素,以便将元素链接到哈希表中。这两种类型的唯一区别在于hlist_head只包含一个前向指针,而hlist_node包含前向和后向指针。

因而,在哈希表桶中的链表是双向的。由于head没有后向指针,链表不能成环,所以扫描到链表尾部代价很高,但这对哈希表而言并不是问题。在哈希表桶的head结构中没有了后向指针,这种实现方法将桶占用的空间缩小了50%,因而同样的内存可以存储的桶容量就能够翻一倍。

图32-2给出了一个利用hlist_head和hlist_node数据结构构造的哈希表例子。注意hlist_node结构不必是它所链接结构的第一个字段。

路由代码中定义的哈希表并不是都与图32-2中的结构相同:在第34章中将看到路由表及其数据结构组织中涉及哈希表的一些例子,在第33章中将看到路由缓存使用了自己定义的哈希表。

第32章 路由在Linux中的实现 Page 32-6

Understanding Linux Network Internals 第七部分——路由

在head元素中也可以用list_head结构来实现包含两个指针的链表,可以从

include/linux/list.h文件中找到有关这两种链表的更多细节,包括最常见的操作程序(添加、删除、遍历等)的定义。

struct struct struct struct hlist_headhlist_node hlist_nodehlist_node

struct hlist_head struct XXX struct XXXstruct XXX… 图 32-2 常见的hash表及链表的使用

32.3 路由与地址Scope

在第30章“Scope”小节中我们已经介绍了scope,这里来看在那一小节中描述的scope在内核中是如何描述的,及其使用的几个例子。

在内核include/linux/rtnetlink.h文件内,定义了一个rt_scope_t枚举,给出了可能的scope。取值范围是从0到255,其中的0(RT_SCOPE_UNIVERSE)表示broadest scope。内核实际上只使用了其中的几个值,其它的值留给用户使用,而目前这些留给用户的值还没有实际使用。

32.3.1 路由Scope

路由的scope被保存在fib_alias数据结构内的fa_scope字段(参见第34章中的图34-1)。下面按照scope递增顺序给出IPv4路由代码中使用的主要scope:

RT_SCOPE_NOWHERE

在第30章并没有给出这个值,它被代码视为非法scope。它的字面含义是路由项不通往任何地方,这基本上就意味着没有到达目的地的路由。

RT_SCOPE_HOST

scope为RT_SCOPE_HOST的路由项的例子:为本地接口配置IP地址时自动创建的路由表项(参见“添加一个IP地址”小节)。

RT_SCOPE_LINK

为本地接口配置地址时,派生的目的地为本地网络地址(由网络掩码定义)和子网广播地址的路由表项的scope就是RT_SCOPE_LINK(参见“添加一个IP地址”小节)。

RT_SCOPE_UNIVERSE

该scope被用于所有的通往远程非直连目的地的路由表项(也就是需要一个下一跳网关的路由项)。

32.3.2 地址Scope

地址的scope被保存在in_ifaddr结构内的ifa_scope字段。对设备上配置的每一个IP地址对应一个in_ifaddr实例(参见第19章)。我们在第30章给出了与主要scope对应的一些地址例子。

第32章 路由在Linux中的实现 Page 32-7

Understanding Linux Network Internals 第七部分——路由

路由表项中的下一跳网关是另一个具有scope的对象类型。每一条路由项可以有零个、一个或多个下一跳,每个下一跳是由一个fib_nh结构表示(参见第34章中的图34-1)。fib_nh结构中有nh_gw和nh_scope两个字段:nh_gw是下一跳网关的IP地址,nh_scope是该地址的scope(这两个字段表示了从本地主机到这个下一跳网关的路由表项的scope)。

32.3.3 路由scope与下一跳scope之间的关系

尽管路由scope和本地配置地址scope或者是由用户通过命令来设定,或是由内核分配一个缺省值,但路由下一跳(fib_bh结构)的scope总是由内核来设定。

在第34章“添加一条路由表项”小节中,将看到fn_hash_insert是如何将一条新路由表项插入到路由表中。这里可以讲fn_hash_insert使用fib_create_info接口为新路由表项分配必要的数据结构,并初始化下一跳的scope。后一个步骤是由fib_create_info在fib_check_nh接口的帮助下实现的。下一跳的nh_scope范围是根据配置的路由表项的scope计算出来的:一般来讲,给了一条路由及其下一跳,那么分配给nh_scope的值就是通往该下一跳的路由scope。但也有一些特殊情况规则不同,例如在到达本地配置地址及直连的路由表项中没有下一跳。

现在我们知道nh_scope是如何被初始化的,接下来看看如何利用它来加强对路由表项的合理性检查。

路由代码对路由scope和下一跳scope的合理性检查是在不同地方实现的。这些合理性检查主要是基于路由scope及其下一跳scope之间很有意思的关系。当主机转发一个IP包时,假定更接近最终的目的地。*[*]根据这一简单性质,可以得出路由scope总是应当大于或等于其下一跳的scope。

1) AÆC BA 路由scope:RT_SCOPE_UNIVERSE

nh_gw=RT nh_scope= RT_SCOPE_LINK(路由scope:RT_SCOPE_LINK

nh_gw=0

RTnh_scope= RT_SCOPE_HOST(2) AÆA

路由scope:RT_SCOPE_HOST nh_gw=0 nh_scope= RT_SCOPE_NOWHERE(图 32-3 下一跳scope初始化举例

我们使用图32-3中的拓朴来看两个例子。记住对每一个路由,nh_scope是下一跳的scope,nh_gw是下一跳的IP地址。

● 当主机A向主机C发送一个包时,匹配的路由scope为RT_SCOPE_UNIVERSE,下一跳为RT。为了使路由汇聚,到RT的路由scope必须比RT_SCOPE_UNIVERSE范围更小。因而,从路由表中查找主机A到主机RT的路由将返回scope为RT_SCOPE_LINK的路由项,它比RT_SCOPE_UNIVERSE的范围更小,因而是正确的。因为一条

RT_SCOPE_LINK路由项不需要网关(事实上从主机A到主机RT的路由项不需要网 这并不一定意味着物理上更接近。有时侯,复杂的路由环境可能迫使报文穿越多个专门的设备,这可能需要多条非最优的路由表项。然而,假定给出了包从源到目的地的路径,那么转发包的每个系统都要使包朝着最终目的地跳跃步数超过1。

*

第32章 路由在Linux中的实现 Page 32-8

Understanding Linux Network Internals 第七部分——路由

关),内核初始化nh_gw为0,nh_scope范围为一个比RT_SCOPE_LINK范围更小的值(例如RT_SCOPE_HOST)。

● 当主机A向自己发送一个包时,匹配的路由scope为RT_SCOPE_HOST。此时不需要网关,所以nh_gw被设置为0,nh_scope被设置为比RT_SCOPE_HOST更小的值:RT_SCOPE_NOWHERE。

当路由查找结果为一条直连路由(即不需要下一跳网关)时,上述递归过程终止。下面给出两种可能的情况:

路由查找返回scope为RT_SCOPE_HOST的路由项。

此时,目的地是本地配置的地址,所以主机可以直接发送该包。

路由查找返回scope为RT_SCOPE_LINK的路由项。

因为目的地为直连所以不需要有网关,主机可以使用二层协议直接向目的地发送包。*

32.4 主IP地址与第二IP地址

我们在第30章“主地址与第二地址”小节中看到,在设备上可以将一个IP地址配置为主IP或第二IP。内核常常需要遍历一个设备上配置的所有地址,来查找与某个条件相匹配的地址。我们下面来看如何区分这两种类型地址以及如何来遍历地址。

IPv4的第二地址在其in_ifaddr数据结构内带有IFA_F_SECONDARY标志(参见第19章)。因为只有主IP与第二IP这两种配置,所以不需要有IFA_F_PRIMARY标志:如果一个地址不是第二IP,那么被视为主IP。

内核在include/linux/inetdevice.h文件中定义了更容易遍历接口的宏以满足指定的标准。对选择的每一个标准,通常在一个循环前后需要两个宏:编程人员在处理单个地址的一段代码前后分别用一个宏来表示开始,用另一个宏来表示终止。作用是将一个循环应用于一段代码,该段代码检查每一个地址是否满足选择标准。

下面是一个宏使用例子:

for_ifa {

处理ifa

} endfor_ifa

for_ifa宏开始一个循环,变量ifa表示选择的每一个地址。两个宏之间的代码不需要放在花括号内部,但通常都放在花括号内部,这是为了使循环内使用的诸如ifa等变量只在该花括号内使用。

这些宏主要有:

for_ifa endfor_ifa

给定一个设备,用这两个宏来遍历其in_device数据结构上的所有地址。†

for_primary_ifa endfor_ifa

*† 这包括第33章中描述的直连路由。

在第19章我们看到in_device数据结构被用于存储一个网络设备上的IP配置。

第32章 路由在Linux中的实现 Page 32-9

Understanding Linux Network Internals 第七部分——路由

给定一个设备,用这两个宏来遍历只与主IP地址相关的in_device实例。

32.5 常用的辅助程序与宏

路由代码使用了许多小程序和宏,这使得代码的可读性更好。本章给出其中常用的一部分,更专门的部分将在后面的“辅助程序”小节中介绍。重要的是必须清楚同一个函数或宏可能有不同的定义,这主要是依赖于以下这些因素:

● 内核对策略性路由的支持

● 内核对多路径路由的支持

● 三层协议(例如IPv4、DECnet)

下面给出常用的程序:

FIB_RES_XXX

这些宏从一个给定的fib_result结构中提取特定的字段,例如FIB_RES_DEV提取出nh_dev字段。这些宏在include/net/ip_fib.h文件中定义。

change_nexthops for_nexthops endfor_nexthops

这些宏用于遍历一个fib_info实例中所有的fib_nh结构。change_nexthops宏开始一个针对这些fib_nh结构的循环,每一个fib_nh结构用局部变量nh表示。就象从这个宏的字面所给出的,该宏可以用于改变这些结构。for_nexthops宏与之类似,它们都是以相同的endfor_nexthops宏来结束循环。唯一的区别在于for_nexthops定义局部变量nh为一个常量指针,因而循环内的代码不能改变所遍历的任何fib_nh实例的内容。

对于IPv4,这些宏被定义在net/ipv4/fib_semantics.c文件中。注意对每一个宏都有两个版本:一个是内核中支持策略路由时使用,一个是不支持策略路由时使用。在不支持策略路由情况下,考虑到每个fib_info实例总是最多只有一个fib_nh实例(即每条路由最多只有一个下一跳),因而这个版本的宏被优化。

inet_ifa_byprefix

给定一个设备、一个网络前缀prefix与一个掩码,该函数遍历该设备上配置的所有的主IP地址,来查找一个与入参数prefix及掩码相匹配的地址。成功时返回匹配地址。

fib_get_table

给定一个路由表ID(从0到255的一个数值),该函数从fib_tables数组返回相应的fib_info结构,如第34章中图34-1所示。该函数在include/net/ip_fib.h文件中定义。

fib_new_table

这个函数创建并初始化一个新路由表,并将它链接到fib_tables数组内(参见第34章中的图34-1)。

LOOPBACK ZERONET MULTICAST

LOCAL_MCAST/BADCLASS 第32章 路由在Linux中的实现 Page 32-10

Understanding Linux Network Internals 第七部分——路由

在include/linux/in.h文件中定义的这些宏用于将IP地址快速划分到众所周知的类别中,参见第30章中的表30-1与表30-2。

LOOPBACK表示127.x.x.x地址。

ZERONET表示0.x.x.x/8地址,大部分情况下不是合法地址。

MULTICAST表示D类地址。

LOCAL_MCAST表示D类中用于本地多播的地址子集:224.0.0.0/24。

BADCLASS表示E类地址。

32.6 全局锁

路由代码使用了一些锁来保护竞争条件。下面只列出了一些全局锁,那些嵌入在数据结构内(即用于单个表项)的锁将在描述相关数据结构时讨论。

fib_hash_lock

这个读写spin锁(rwlock)保护所有的路由表。例如插入一个新的fib_node实例需要在互斥模式下捕获该锁,而路由表查找只需要在共享模式下捕获该锁。因为所有的路由表只有这一个锁,也就意味着不可能将两条路由表项同时添加到两个不同的路由表内。但这并不意味着是一个瓶颈,因为配置改变是稀有事件,用户主要使用共享锁,因而不会对路由器性能产生任何大的影响。

fib_info_lock

这个rwlock锁保护所有的fib_info数据结构。在第34章“fib_info结构的组织”小节中描述的通过哈希表来访问fib_info结构时使用该锁。

fib_rules_lock

这个rwlock锁保护类型为fib_rule数据结构的fib_rules全局链表。

rt_flush_lock

在rt_cache_flush接口中,使用这个spin锁保护对rt_deadline全局变量和rt_flush_timer定时器的操作。缓存是由每个桶一个锁来加以保护,参见第33章中的图33-1及“Flush路由缓存”小节。

fib_multipath_lock

当修改fib_info结构中由多路径特性使用的字段时,使用这个spin锁。

alg_table_lock

这个spin锁用于对ip_mp_alg_table数组的顺序访问,在multipath_alg_register和multipath_alg_unregister函数中使用。参见第33章“注册一个缓存算法”小节。

32.7 路由子系统初始化

Ipv4路由代码的初始化是从net/ipv4/route.c文件中的ip_rt_init开始,它是系统启动时被net/ipv4/ip_output.c文件中初始化IP子系统的ip_init接口调用。ip_init在第19章中描述,这里我们来看看ip_rt_init。* 图32-4给出了路由初始化时调用的主要程序。 IPv6做法类似,是在inet6_init中调用ip6_route_init。

*第32章 路由在Linux中的实现 Page 32-11

Understanding Linux Network Internals 第七部分——路由

在ip_rt_init函数中,IPv4路由代码初始化其数据结构和全局变量。另外,该函数还:

● 根据可用内存确定路由缓存的容量。

● 创建用于分配路由缓存元素的内存池。

● 初始化路由缓存

● 确定由垃圾回收算法使用的gc_thresh门限值(参见第33章“rt_garbage_collect函数”小节)。

● 启动rt_periodic_timer定时器(参见第33章“垃圾回收”小节)。

● 启动rt_secret_timer定时器(参见第33章“Flush路由缓存”小节)。

● 添加一些文件到/proc文件系统(参见第36章“通过/proc文件系统调整路由”小节)。 ip_init

@net/ipv4/ip_output.c

ip_rt_init

@net/ipv4/route.c devinet_init @net/ipv4/devinet.c

ip_fib_init

@net/ipv4/fib_frontend.c 策略路由 非策略路由 fib_rules_init fib_hash_init (LOCA_TABLE, @net/ipv4/fib_rules.cMAIN_TABLE)

图 32-4 路由初始化时调用的主要函数顺序

下面给出两个尤其感兴趣的函数:

ip_fib_init

初始化缺省路由表,为两个通知链*netdev_chain与inetaddr_chain(参见“外部事件”小节)分别注册处理钩子(handler)。

devinet_init

为通知链netdev_chain注册另一个处理钩子,注册Netlink套接字上地址和路由命令(即ip addr ... 与ip route ..命令)的处理钩子函数,并创建/proc/sys/net/conf和/proc/sys/net/conf/default目录。参见第36章是如何来实现后两个任务。

如果内核编译时支持IPsec,那么ip_rt_init也调用两个IPsec初始化函数(xfrm_init与xfrm4_init)。

ip_rt_init中对rt_hash_xxx全局变量初始化的详细描述可参见第33章“路由缓存初始化”小节。

策略路由的初始化是由在net/ipv4/fib_rules.c文件中定义的fib_rules_init函数来实现。初始化就是简单为netdev_chain通知链注册处理钩子。注册的处理钩子为fib_rules_event,它的描述可参见“对策略数据库的影响”小节。 *

通知链在第4章中描述。

第32章 路由在Linux中的实现 Page 32-12

Understanding Linux Network Internals 第七部分——路由 32.8 外部事件

在网络协议栈中路由子系统处于中心地位。因此,需要知道发生的哪些变化可能影响到路由表和路由缓存。网络拓朴变化是由运行在用户空间的路由协议来处理,而另一方面,本地主机配置变化则需要内核的介入。

路由子系统尤其对两种事件感兴趣:

● 网络设备状态的变化 ● 网络设备上IP配置的变化

为了在这些变化发生时收到通知,路由子系统分别向netdev_chain和inetaddr_chain通知链注册。在“设备状态改变”和“IP配置改变”两个小节中,详细讨论了向这两类事件注册的处理钩子。

图32-5从高层级上描述了在ip_rt_init函数中注册的两个处理钩子,它们在“对路由表的影响”和“对IP配置的影响”两个小节中描述。一些事件是通过调用一些有各种输入参数的辅助程序来处理,其中的一些辅助程序在下面的“辅助程序”小节中描述,对fib_sync_down的描述可参见这一小节,force参数的含义在图32-5中给出。

我们将看到许多事件都导致路由缓存的flush,参见第33章“Flush路由缓存”小节来查看完整的事件列表。

fib_netdev_event fib_inetaddr_event NETDEV_UNREGISTERNETDEV_DOWN NETDEV_UNETDEV_UP 事件类型 事件类型 P 删除导出的路由项NETDEV_DOWN 添加导出的路由项 (fib_del_ifaddr)(fib_add_ifaddr) NETDEV_CHANGE NETDEV_CHANGEMTU Yes刚才删除的IP是否为 立即flush路由表缓CONFIG_IP_ROUTE_MULTIPATH 接口上最后一个?存 DEAD的下Enable标记有 End对设备上配一跳(fib_sync_up) No 置的每个IP(A) (B)(C)地址 Next disable设备上的IP协议在缺省延迟后Flush路由 表缓存 在缺省延迟后Flush路由添加导出的路由项 表缓存 CONFIG_IP_ROUTE_MULTIPATH(fib_add_ifaddr) Enable标记有DEAD的下 一跳(fib_sync_up) 在缺省延迟后Flush路由 表缓存 (A) Force = 0 (B) Force = 1

(C) Force = 2

图 32-5 fib_netdev_event与fib_inetaddr_event函数

第32章 路由在Linux中的实现 Page 32-13

Understanding Linux Network Internals 第七部分——路由 32.8.1 辅助程序

在下面小节中,我们将看到fib_netdev_event和fib_inetaddr_event是如何详细实现的。本节简要概述由这两个处理钩子调用的一些程序,在阅读这两个处理钩子的代码时可以用本小节作为参考。

void rt_cache_flush(int delay)

在一个给定的时间段(由输入参数指定)后,调度一次路由缓存的flush。参见第33章“Flush路由缓存”小节。

int fib_sync_down(u32 local, struct net_device *dev, into force)

当一个设备被关闭或一个本地地址被删除时更新路由表。下面给出输入参数的含义: local

已经被删除的IP地址。 dev

已经被关闭的设备。 force

决定何时执行相应的活动。参考图32-5来看下面每个值在何时使用。含义如下:

0:一个IP地址已经被删除。 1:一个设备已经被关闭。 2:一个设备已经被注销

这个force参数有多个用途,它被用于确定两件事情:

被删除路由表项的scope

当force为0时,处理钩子删除除本地配置地址产生的路由表项(即scope为

RT_SCOPE_HOST)外所有合法的路由表项。当force为1或2时,处理钩子删除所有合法的路由表项,此时与路由scope无关。

如何处理多路径路由

当force为2时,如果一条多路径路由的多个下一跳中至少有一个使用了输入参数dev,处理钩子就删除该路由项。当force为0或1时,只有当一条多路径路由的所有下一跳都为dead时,处理钩子才删除该路由项。

当fib_sync_down被调用时,通常一次只处理一类事件。所以或者dev或者local参数被设置,而另一个参数为空。

当local参数被提供时,fib_sync_down删除所有以local为首选源地址(preferred source address)的路由表项。记住路由项可以有一个首选源地址,该地址不必是路由项出设备上配置的IP地址。

当dev参数被提供时,fib_sync_down删除所有的下一跳是通过dev可达的路由表项。

第32章 路由在Linux中的实现 Page 32-14

Understanding Linux Network Internals 第七部分——路由

在这两种情况下并不是直接删除路由表项,而是通过设置RTNH_F_DEAD标志来标记这些表项为dead(不可用)。只有当一条多路径路由的所有下一跳都被标记为dead,该路由项才被标记为dead。而且,当一条多路径路由的一个下一跳被标记为dead时,也要更新fib_power与nh_power参数,以反映当前下一跳的状态(参见第35章“多路径对下一跳选择的影响”小节)。

fib_sync_down返回值为标记为dead的fib_info结构数量。调用方(诸如本小节后面描述的fib_disable_ip)可利用该返回值来判断是否要flush路由缓存。

int fib_sync_up(struct net_device *dev)

只有在内核支持多路径时才使用该函数。它的主要目的是在路由的一些下一跳为alive时更新fib_info结构内路由的一些参数。返回值为RTNH_F_DEAD标志被清除的fib_info结构数量。

void fib_flush(void)

扫描ip_fib_main_table与ip_fib_local_table路由表,删除所有的设置有

RTNH_F_DEAD 标志的fib_info结构。它既删除fib_info结构,也删除相关联的fib_alias结构。当一个fib_node实例不再有fib_alias结构时,该fib_node实例也被删除。fib_flush调用的缺省程序可参见第34章中的表34-1,前面提到的数据结构之间的关系可参见第34章中的图34-1。

当内核支持多路径时,fib_flush扫描所有的路由表。

当至少有一个fib_info实例被删除时,调用rt_cache_flush对路由缓存flush。

返回值是被删除的fib_info实例的数量。

static void fib_disable_ip(struct net_device *dev, int force)

通过调用fib_sync_down来禁止输入参数dev上的IP协议。当删除的路由项数量为正值时(通过fib_sync_down的返回值判断),该函数也立即flush路由表:

fib_sync_down标记路由项为dead,fib_flush执行实际的路由项删除。fib_disable_ip也立即flush路由缓存,并要求ARP从自己的缓存中清除所有的引用被关闭IP协议设备的表项。

注意输入参数force被传递给fib_sync_down。图32-6给出了fib_disable_ip函数的内部流程。

第32章 路由在Linux中的实现 Page 32-15

Understanding Linux Network Internals 第七部分——路由

fib_sync_down

对每个fib_info结构 Next 对每个nexthop Next No device和scope匹配 吗? Yes 立即flush路由缓存 标记nexthop为dead 删除与device相关的所有ARP表项 (arpifdown) End至少有一个fib_node 被标记为dead? No YesEnd

清理路由表 (fibflush)No 所有的nexthop都被标 记为dead? Yes标记fib_info为dead 图 32-6 fib_disable_ip函数

32.8.2 IP配置改变

一旦设备的IP配置发生变化,路由子系统将收到一个通知,运行fib_inetaddr_event来处理该事件。图32-5总结了由这个通知链传送的所有可能事件所触发的活动,这里给出如何来处理这些主要事件:

NETDEV_UP

本地设备上已经配置了一个新的IP地址。处理钩子必须将必要的路由项添加到local_table路由表中,这是由fib_add_ifaddr程序来完成的。

NETDEV_DOWN

本地设备上已经删除了一个IP地址。处理钩子必须将以前由NETDEV_UP事件添加的路由项删除,这是由fib_del_ifaddr来完成的。

就象在第30章“特殊路由”小节中提到的,每当一个接口上配置IP地址,内核就将一组特殊的路由项添加到一个的名为ip_fib_local_table的路由表中。添加这些特殊路由项的程序是fib_add_ifaddr,它对每一条新路由表项调用fib_magic来完成添加,fib_magic在第36章中描述。

在事件处理期间调用的大多数程序已经在前面的“辅助程序”小节中描述,下面的小节描述fib_add_ifaddr和fib_del_ifaddr。

第32章 路由在Linux中的实现 Page 32-16

Understanding Linux Network Internals 第七部分——路由

32.8.2.1 添加一个IP地址

fib_add_ifaddr的逻辑流程见图32-7。当这个程序被通知一个设备上添加了一个新地址时,该设备可能还没有被使能(enabled)。是否添加到该设备的路由项依赖于该设备是否为使能。我们首先来看根据一个IP地址可以导出哪些路由,然后来看在设备为使能或禁止(disabled)时哪些路由项被添加或删除。作为一个基本例子,下面给出与IP地址10.0.1.1/24相关联的可能的路由:

到地址10.0.1.1/32的路由

简单讲,就是到具体主机地址的路由。

到网络地址10.0.1.0/24的路由

这是从IP地址和网络掩码导出的路由。在我们例子中即为10.0.1.1 & 255.255.255.0

计算的结果。

到广播地址10.0.1.255/32和10.0.1.0/32的路由

这代表了强制规范和实际情况之间的一个折衷。

Linux能够处理不同的需求:它能够将两种版本广播地址的路由项添加到

ip_fib_local_table路由表中。注意:到网络地址和受限广播地址(译者注:参见第35章“送往本地”小节)之间的路由由于网络掩码不同(10.0.1.0/24 对10.0.1.0/32)而存在区别。用户还可以配置一个指定的广播地址,此时fib_add_ifaddr程序还要添加一条到该指定广播地址的路由。

对广播地址的处理表明理论与现实之间存在距离。从理论上讲,广播地址是根据地址类别(address class)导出的。在我们的10.0.1.1/24地址中,这将产生一个10.255.255.255广播地址,如果用ifconfig来配置地址将得到这个缺省广播地址。

第32章 路由在Linux中的实现 Page 32-17

Understanding Linux Network Internals 第七部分——路由

Yes IP地址? 是第二 Yes No 存在主IP地址? No 添加到IP地址的路由 (LOCAL表) No 设备UP? Yes 明确配置了 广播地址? No Yes 是第二 IP地址? No 网络掩码小于/32? No 结束

YesYes明确配置了 广播地址? No添加到明确广播地址的路由(LOCAL表) Yes 是Loopback设备?Yes添加到网段的路由(LOCAL表)No添加到网段的路由(MAIN表)No 网段掩码小于/31?Yes添加到两个导出广播地址的路由(LOCAL图 32-7 fib_add_ifaddr函数

然而,通常人们需要一个根据网络掩码导出的广播地址。这是通过将网络地址主机部分所有的比特位都设置为1而得出的,在我们例子中将得到广播地址10.0.1.255。如果通过ip addr命令来配置地址,那么所生成的缺省广播地址就是这个更为有用的广播地址。

第32章 路由在Linux中的实现 Page 32-18

Understanding Linux Network Internals 第七部分——路由

两种解决方法之间的差异是由于10.0.0.1是A类网络10.0.0.1/8的一个地址,而A类网络通常又被细分为更小的网络。例如10.0.0.0/24是A类网络10.0.0.0/8的一个C类子网。如果我们配置地址为192.168.1.1,那么通过ifconfig和ip addr命令将得到相同的广播地址192.168.1.255,因为192.168.1.1是C类网络192.168.1.0/24的一个地址。更详细的描述可参见第30章“路由的基本要素”小节。

我们在第28章“对多个接口作出响应”小节中看到IP地址是属于系统,而不是属于所配置的那个接口。因此,到IP地址的路由总是被添加到ip_fib_local_table路由表内而与设备状态无关。但是,到网络地址和广播地址的路由并非如此:当设备为down时,网络地址和广播地址都不是可达的,所以为它们创建两条路由项是不正确的。fib_add_ifaddr中使用设备的IFF_UP标志来判断其状态。

fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim); if (!(dev->flags&IFF_UP)) return;

因此在一个disabled设备上配置一个IP地址时,只能添加到IP地址的路由。当该设备在后来为enabled时,将再次调用fib_add_ifaddr来添加所有的路由项。此时程序中将再次添加到IP地址的路由,但因为路由表中不能够存在重复表项,因而到IP地址的路由表项不会被重复添加。

注意:有时通过命令在设备上配置一个IP地址也可以使设备为使能。例如,通过ifconfig命令来配置IP地址时就可以使能设备,而在IPROUTE2中则分为两个步骤:首先使用ip addr add来配置一个IP地址,然后使用ip link set来使能或禁止设备。在阅读源代码时,重要是的要理解这些差异,试着画出一段内核代码的行为是如何与用户空间命令的对应关系。用户空间命令在第36章中有更详细的描述。

表32-1列出了一些命令及创建的路由表项。

表 32-1 IP配置及相关路由表项举例

命令 Main路由表 Local路由表

10.0.1.1/32 (address)

10.0.1.0/32 (broadcast) 10.0.1.0/24 ip addr add 10.0.1.1/24 dev eth0

10.0.1.255/32 (broadcast) 10.0.1.1/32 (address)

10.0.1.100/32 (broadcast)

10.0.1.0/24 ip addr add 10.0.1.1/24 broadcast 10.0.1.100 dev

10.0.1.0/32 (broadcast) eth0

10.0.1.255/32 (broadcast)

ip addr add 10.0.1.1/32 dev eth0 10.0.1.1/32 (address)

10.0.1.1/32 (address) ip addr add 10.0.1.1/32 broadcast 10.0.1.255 dev

eth0

10.0.1.255/32 (broadcast)

如果明确给出了广播地址且为受限广播地址255.255.255.255,那么不添加到该广播地址的路由,因为路由查找程序要检查全255的广播地址,参见第35章“输入选路”与“输出选路”小节。

if (ifa->ifa_broadcast && ifa->ifa_broadcast != 0xFFFFFFFF)

fib_magic(RTM_NEWROUTE, RTN_BROADCAST, ifa->ifa_broadcast, 32, prim); 第32章 路由在Linux中的实现 Page 32-19

Understanding Linux Network Internals 第七部分——路由

明确配置广播地址可以使用户定义一个广播地址,即使是在理论上只能有一个IP地址的/32的网段内也可以,参见表32-1中第四个例子(注意广播地址10.0.1.255不在10.0.1.1/32网段内)。

在某些条件下,这个函数不需要添加到广播地址的路由。这依赖于网络掩码长度,它被保存为局部变量prefixlen:

● 当prefixlen为32时,在子网内只有一个有效地址,所以不需要导出的广播路由或网络路由。

● 当prefixlen为31时,只有一个比特位参与,所以在子网内只有两个地址。clear比特位的地址表示网络地址,set比特位的地址表示主机地址(函数正在配置的地址)。这种情况下需要到这两个地址的路由,而不需要到导出的广播地址的路由。

● 当prefixlen小于31时,子网内包含的地址数大于或等于四个,由于本地地址、网络地址和广播地址只占用其中三个,因而子网内还可以包含其他地址。这时内核添加一条到导出的广播地址的路由及一条到导出的网段地址的路由。

以上对主地址的处理情况见下面代码:

if (!ZERONET(prefix) && !(ifa->ifa_flags&IFA_F_SECONDARY) && (prefix != addr || ifa->ifa_prefixlen < 32)) {

fib_magic(RTM_NEWROUTE, dev->flags&IFF_LOOPBACK ? RTN_LOCAL : RTN_UNICAST, prefix, ifa->ifa_prefixlen, prim); if (ifa->ifa_prefixlen < 31) {

fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix, 32, prim);

fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix|~mask, 32, prim); } } }

第二IP地址不处理这些情况。当添加一条第二IP地址时,在该设备上必须存在在同一网段(prefix)内的一个主IP地址。如果这样的主IP地址不存在,那么将得到错误,配置不能够生效。所以,第二IP地址不需要到网络地址的路由,也不需要到导出的广播地址的路由:相关的主地址配置时已经添加了这些路由项。

32.8.2.2 删除一个IP地址

当从一个接口删除一个IP地址时,路由子系统得到通知以便清理路由表和路由缓存。这是通过fib_del_ifaddr来实现的,其逻辑可见图32-8。

该函数首先是合理性检查,若是删除一个第二IP地址,那么必须有一个主IP地址与它在同一网段。如果不是,则前面某个地方可能出错因而返回一个错误。

当fib_del_ifaddr被调用时,该IP地址已经从受影响设备上配置的地址列表中删除(例如,当inet_del_ifa触发了NETDEV_DOWN事件通知)。调用fib_del_ifaddr删除与该IP地址相关的路由表项。

在前面小节中提到,到广播地址和到网段地址的路由可能没有随着到主IP地址路由的添加而添加。fib_del_ifaddr扫描设备上配置的所有地址,检查哪些需要删除。读者可以从表32-1中看出在配置一个本地IP地址时添加了哪些路由表项。

多数情况下,当删除一个第二IP地址时,路由子系统只需要删除到该IP地址的路由,而不删除到网段地址和广播地址的路由,因为主IP地址(以及其它可能存在的第二IP地址)仍然需要它们。但还可能在删除一个第二IP地址时,不需要删除到该IP地址的路由:例如,当管理员配置的一个IP地址具有两个不同的网络掩码。见下面的例子:

第32章 路由在Linux中的实现 Page 32-20

Understanding Linux Network Internals 第七部分——路由

# ip addr add dev eth0 192.168.0.1/24 # ip addr add dev eth0 192.168.0.1/16 主地址 地址类型

删除到网段的路由 第二地址

同一网段内存在 No 主IP地址?

Yes

删除不再需要的

导出的路由

YesIP地址真的被 fib_sync_down删除了吗?

End No 对每个fibinfo

Next

fib_infoNo 将该IP作为路由的首选源地址?

Yes 标记fib_info为DEAD

最少有一个fib_infoYes

标记为DEAD?

删除与DEAD fib_info相关的 No 所有fibnode(fibflush)

结束

图 32-8 fib_del_ifaddr函数

这个例子并不代表普通情况,但代码必须能够处理它。

通过这两个命令导出的到网段地址和到广播地址的路由是不同的(参见“添加一个IP地址”小节),但它们共享一条到IP地址的路由。

fib_del_ifaddr在删除了相应的路由表项后,调用fib_sync_down和fib_flush来清理路由表。

当fib_del_ifaddr从一个设备上删除最后一个IP地址时,fib_inetaddr_event函数调用fib_disable_ip来禁止该设备上的IP协议(参见图32-5)。

第32章 路由在Linux中的实现 Page 32-21

Understanding Linux Network Internals 第七部分——路由 32.8.3 设备状态改变

路由子系统向netdev_chain通知链注册三个不同的处理钩子来处理设备状态的变化:

● fib_netdev_event更新路由表。

● fib_rules_event更新策略数据库,当使用策略性路由时。

● ip_netdev_event更新设备的IP配置。*

在接下来的三个小节中,描述这些程序是如何处理接收到的事件通知。

32.8.3.1 对路由表的影响

当一个设备的状态或其某些配置部分(不包括IP配置,它是由另一个通知链处理)发生变化,路由子系统将收到通知,调用fib_netdev_event来处理该事件。

图32-5总结了由该通知链通知的所有事件触发的活动,下面给出如何来处理这些主要事件:

NETDEV_UNREGISTER

当一个设备注销时,从路由表(包括路由缓存)删除使用该设备的所有路由项。如果多路径路由项的下一跳中至少有一个使用该设备,则该路由项也被删除。

NETDEV_DOWN

当一个设备变为DOWN时,调用fib_disable_ip从路由表(包括路由缓存)删除使用该设备的所有路由项。

NETDEV_UP

当一个设备变为UP时,必须将与该设备上所有IP地址相关的路由表项添加到ip_fib_local_table路由表中。这是通过对该设备上配置的每一个IP地址,都调用fib_add_ifaddr函数来完成的。fib_add_ifaddr的描述可参见“添加一个IP地址”小节。

NETDEV_CHANGEMTU NETDEV_CHANGE

当一个设备的配置发生变化时,flush路由表缓存。最常见的配置变化是MTU或PROMISCUITY状态被修改。

注意:路由子系统对NETDEV_REGISTER事件不感兴趣。NETDEV_UP事件足以触发路由子系统对一个新激活设备的处理。

注销一个设备与关闭(shut down)一个设备对路由表产生的影响不同。设备注销的原因包括用户从内核中删除该设备或拔出一块诸如PCMCIA以太网卡等热插拔设备,而设备关闭的原因包括用户拔出网线或通过命令关闭该设备。对这两种情况而言,从路由表中删除的路由表项是不同的。

我们来看一个例子。在表32-2中的第一列给出了在执行下面两个命令后,将被添加到路由表中的路由表项,后面两列分别给出了当设备eth0被关闭或注销时应当删除的路由表项。

# ip addr add dev eth0 192.168.1.100

# ip route add 10.0.1.0/24 via 192.168.1.111 *

这个处理钩子是通过ip_rt_init函数注册的,但它实际上属于IP子系统。

第32章 路由在Linux中的实现 Page 32-22

Understanding Linux Network Internals 第七部分——路由

表 32-2 当设备关闭或注销时删除的路由

路由 路由表 关闭(shut down) 注销(Unregistered) 192.168.1.0/24 Main Yes Yes 192.168.1.0/32 Local Yes Yes 192.168.1.255/32 Local Yes Yes 192.168.1.100/32 Local No Yes 10.0.1.0/24 Main Yes Yes

当设备被关闭时到IP地址的路由没有被删除,这是因为它的IP地址属于本机而不是属于接口。只要与该地址相关联的设备存在,那么该地址就一直存在,参见第28章“对多个接口作出响应”小节。

32.8.3.2 对策略数据库的影响

一条策略(即一条规则)可以与一个设备相关联。例如可以为从设备eth0接收及目的地为子网10.0.1.0/24的数据流分配一个特定的优先级。所以,当设备被注销时,与其相关的所有策略(即fib_rule数据结构)应当被标记为不可用,即通过fib_rules_detach来设置设备ID(即fib_rule数据结构中的r_ifindex字段)为无效值-1。

另一方面,当设备注册时,如果与该设备存在相关联的disabled的策略,则通过

fib_rules_attach来重新使能这些策略。因为disabled策略的设备ID为-1,所以内核不能使用设备ID,而是使用保存在fib_rule结构中的r_ifname字段,即使用设备名来识别该设备与哪些策略相关联。

32.8.3.3 对IP配置的影响

下面来看inetdev_event程序是如何处理从netdev_chain通知链接收到的事件:

NETDEV_UNREGISTER

禁止该设备上的IP协议。

NETDEV_UP

利用ip_mc_up来使能多播配置(如果存在的话)。当该设备为loopback设备时,在它上面配置127.0.0.1/8地址。

如果该设备上配置的MTU小于使IP协议生效的最小值68,忽略该事件。这只是一项合理性检查。

NETDEV_DOWN

利用ip_mc_down来disable多播配置(如果存在的话)。

NETDEV_CHANGEMTU

检查该设备上已经设置的MTU是否低于使IP协议生效的最小值68,如果是则禁止该设备上的IP协议。

NETDEV_CHANGENAME

更新目录/proc/sys/net/ipv4/conf/devname和/proc/sys/net/ipv4/neigh/devname名称为新的设备名称。这两个目录分别在第23章和第29章描述。

第32章 路由在Linux中的实现 Page 32-23

Understanding Linux Network Internals 第七部分——路由

对于NETDEV_UNREGISTER与NETDEV_CHANGEMTU这两个事件,禁止设备上的IP协议都是由inetdev_destroy来完成,它删除该设备上所有的IP配置并调用arp_ifdown来清除对应的ARP缓存。

32.9 与其它子系统的交互

在第31章“与其它内核子系统的交互”小节中,给出了路由子系统与诸如流量控制和防火墙等子系统的交互。在下面的小节中,我们将看到更多的细节。路由子系统与基于路由表的classifier之间的交互被延后到第35章讨论,因为需要具备路由表结构与如何实现路由查找等一些背景知识。

32.9.1 Netlink通知

当添加或删除一条路由时,利用程序rtmsg_fib向Netlink组RTMGRP_IPV4_ROUTE发送一个通知。创建路由和删除路由的通知分别是由fn_hash_insert和fn_hash_delete来生成,这是我们将在第34章看到的两个重要程序,也可以参见第36章“路由变化通知”小节。

32.9.2 策略路由与基于防火墙的Classifier

在第31章“与其它内核子系统的交互”小节中提到,策略路由可以使用一个由防火墙代码初始化的标签(tag)来确定对入流量和出流量应当使用哪一张路由表。基于防火墙标签的路由过程要求内核在编译时要有特定的支持。当防火墙标签可用时,它就成为缓存和路由表查找key(用flowi结构来表示)的一部分。防火墙子系统将该标签拷贝到缓冲区字段skb->nfmark中,策略路由使用该字段来确定对入流量和出流量的路由应当使用哪一张路由表。

在第33章,你将看到两个缓存查找程序ip_route_input和__ip_route_output_key是如何检查这个防火墙标签的。在第34章,给出了两个路由表查找程序ip_route_input_slow和

ip_route_output_slow是如何利用防火墙标签skb->nfmark来初始化路由查找key flowi的nfmark字段。skb->nfmark值为0表示不存在标签。

在第31章中的图31-6给出了在网络协议栈中,防火墙何时根据自己的配置对报文缓冲区添加标签,策略路由引擎何时使用该标签用于策略规则查找。

32.9.3 路由协议守护进程

路由可以由用户通过诸如ip route或route等命令,以及由用户空间中运行的诸如BGP、IGRP与OSPF等路由协议来添加。我们在第31章“路由协议守护进程”小节的大图中已经看到这一点。在这一小节,我们来详细描述用户/内核接口,而在第36章将更为详细地描述这些工具。但是,我们不会讨论任何路由协议的内部细节,因为这超出了本书的范围。

路由协议运行在用户空间,但它们需要将自己的路由知识注入到内核中,以便将它们的路由融入到内核的路由表中。尽管路由协议代码于底层的操作系统,但这些协议将路由注入到内核中时,必须采用由底层操作系统提供的用户/内核接口。

如果你喜欢阅读源代码,那么我极力主张你看看于平台的代码(基本上就是指路由协议)是如何与平台代码交互,将路由注入到内核的路由表中。例如,你将看到不同的操作系统要求有不同的接口,甚至同一个操作系统的不同版本可能要求或提供的接口也不同。

对Linux来说,老式的ioctl接口仍然可用,但内核首选的是新型的Netlink接口,因为它的功能更为强大。尽管ioctl对所有的Unix爱好者来说是相当好的接口,但Netlink是Linux特有的,它在Linux世界中扮演着与路由套接字在BSD世界中同样的角色。重要的是要注意当Netlink被编译到内核中时,它要优于ioctl,因为它具有更好控制和双向通信能力。例如,当内核检测到一个NIC的变化时,内核可以利用Netlink套接字将该变化通知给用户空间内的应用程序,以便应用程序采取相应的行动。

第32章 路由在Linux中的实现 Page 32-24

Understanding Linux Network Internals 第七部分——路由

表32-3列出了最常见的路由协议守护进程,并给出了哪些可以处理ioctl接口和Netlink接口。第36章将更为详细地讨论这些接口。

表 32-3 最常见的路由协议守护进程与Linux内核之间的接口

ioctl Netlink 守护进程(Daemon)

ROUTED (v 0.17) Yes No GATED (v3.6) Yes Yes BIRD (v1.0.9) Yes Yes ZEBRA (v0.94) Yes Yes QUAGGA (v 0.98.0) Yes Yes MRT (v2.2.0) Yes Yes

a

XORP (v1.0) No Yes

a

XORP使用ioctl接口,但不能插入或删除路由。XORP的目的和操作在一份有趣的文档中描述,参见http://www.xorp.org/releases/current/docs/fea/fea.pdf.

第32章 路由在Linux中的实现 Page 32-25

Understanding Linux Network Internals 第七部分——路由

第33章 路由缓存

路由缓存被用于减少路由表查找的时间。路由缓存的核心是与协议无关的目的缓存

(Protocol Independent Destination Cache),简称为DST。虽然采用策略路由可以有效地创建多张路由表,但所有这些路由表都共享一个路由缓存。

缓存的主要工作是存储使路由子系统能够找到报文目的地的信息,并通过一组函数向更高层提供该信息。路由缓存还提供了一些函数用于缓存的清理(clean up)。路由缓存存储的信息为可应用于所有三层协议的路由表缓存表项,所以可以包含表示路由表缓存表项的任意数据结构。

在本章中,我们将看到:

● 如何实现缓存

● 如何将新元素插入,以及如何将已有的元素删除

● 如何实现ingress查找与egress查找,它们之间存在哪些差异

● 外部子系统如何通过DST提供的接口与缓存交互

● 有多少种不同的垃圾回收算法用于控制缓存容量

● DST是如何为egress ICMP重定向消息提供限速机制

33.1 路由缓存初始化

路由缓存是用一张哈希表来实现的。它是在路由子系统的初始化函数ip_rt_init中被初始化,参见第32章“路由子系统初始化”小节中的描述。

缓存容量依赖于主机可用的物理内存的大小。在你的系统中,你可以从系统启动输出的信息或是在系统启动后从dmesg命令输出的信息中,查找由ip_rt_init函数输出的“IP: routing cache hash table of ...”字符串,从而找到哈希表的容量。该容量被存储在rt_hash_mask,其以2为对数的值被保存在rt_hash_log中(即2rt_hash_log =rt_hash_mask)。内核指定的缺省容量可以被用户启动选项rhash_entries所覆盖,该选项存储的是哈希表的容量以用于变量rhash_entries。

ip_rt_init主要是初始化以下变量:

rt_hash_table

用一张哈希表来定义的路由缓存

rt_hash_mask

rt_hash_log

分别表示哈希表rt_hash_table的容量(即哈希桶的数量)和该容量以2为对数所得的值,当一个值必须通过比特位数量来移位时这种做法通常很有用。

rt_hash_rnd

当每次利用rt_run_flush来flush路由缓存时生成一个新的随机值,存储在rt_hash_rnd参数中。该参数被用于防止DoS攻击,它是路由缓存中元素分布算法的一个部分,使元素分布没有什么确定性。该变量首先由ip_rt_init来初始化,依据的是可用内存

第33章 路由缓存 Page 33-1

Understanding Linux Network Internals 第七部分——路由

和当前jiffies相关参数值。在系统已经运行(up)了一段时间后并且有机会建立好的熵(entropy)时,使用get_random_bytes程序来重置该变量。

33.2 哈希表组织

本小节描述的数据结构与三层协议中描述的结构稍有不同。在IPv4中,哈希桶的类型是rt_hash_bucket,该结构只包含指向缓存元素链表的一个指针和一个锁,锁的使用在“缓存锁”小节中描述。

缓存元素的类型为rtable。这个结构包含与协议相关的一些字段,参见第36章“rtable结构”小节中的描述,和一个与协议无关的、类型为dst_entry的数据结构,参见图33-1。

dst_entry结构包含了缓存与邻居层的接口、transformers (诸如IPsec)以及路由缓存管理。该数据结构的详细描述可参见第36章“dst_entry结构”小节,与邻居层的接口可参见第27章。

rtable结构的第一个字段是一个联合,这使得rtable和dst_entry结构很容易共享一些数据,诸如指向下一个碰撞哈希表项的指针。虽然指针的名称不同(dst_entry中的指针为next,而rtable中的指针为rt_next),但它们所指向的内存位置是相同的。

链 锁 rt_next/next rt_next/next

struct dst_entrystruct dst_entry struct rtable struct rtable

链 锁 rt_next/next struct dst_entry

struct rtable

锁 struct rt_hash_bucket rt_hash_mask 图 33-1 路由缓存结构

struct rtable {

union {

struct dst_entry dst; struct rtable *rt_next; } u; ... ... ... }

一个指向rtable结构的指针可以安全地映射(typecast)为一个指向dst_entry的指针,反之亦然。

当对缓存表做插入、删除或查找等操作时,路由子系统根据源IP地址、目的IP地址、TOS字段、入设备或出设备的组合来选择该缓存表的一个哈希桶。在对入流量选择路由时使用入设备ID,对本地生成的出流量选择路由时使用出设备ID。虽然对入流量总是能够知道相应的入设备,但对于出流量可能还不知道相应的出设备。出设备只有在路由查找以后才能够

第33章 路由缓存 Page 33-2

Understanding Linux Network Internals 第七部分——路由

知道,除非路由查找key中已经包含了出设备(本地生成的流量可能包含了出设备,但这一点并不是必须的)。

33.3 主要的缓存操作

缓存中与协议无关的(DST)部分是一组dst_entry数据结构。本章中的大部分活动都要涉及dst_entry结构。IPv4和IPv6数据结构rtable与rt6_info中各自包含有一个dst_entry数据结构。

在dst_entry结构中,dst_ops字段包含了一组虚函数,这使得更高层协议通过调用这些具体协议函数来操作缓存表项。DST代码位于net/core/dst.c和include/net/dst.h文件内。

操作dst_entry结构的所有函数名都是以dst_为前缀的。注意它们操作的虽然是dst_entry结构,但它们实际也影响到rtable结构。

DST的初始化是在系统启动时,由net_dev_init调用dst_init来完成的(参见第5章)。

33.3.1 缓存锁

诸如查找等只读操作使用的锁机制与诸如插入和删除等读写操作使用的锁机制不同,但它们必须协作。下面给出如何来使用它们:

只读操作

在“缓存查找”小节中给出了只读操作函数,它们是由一个读-拷贝-更新(RCU)读锁来保护的,见下面的代码片断:

rcu_read_lock( ); ...

执行查找 ...

rcu_read_unlock( );

这个代码实际上没有锁,因为读操作可以同时进行而互不干扰。

读写操作

一个缓存表项的插入(参见“添加元素到缓存中”小节)和删除(参见“删除DST表项”小节)使用每个哈希桶元素内包含的spin锁,参见图33-1所示。注意每个哈希桶有自己的spin锁,可以使不同的处理器同时写不同的哈希桶。

第一章解释了在路由表缓存中锁的RCU实现算法,以及利用RCU如何来实现读-写spin 锁的共存。

33.3.2 缓存表项的分配与引用计数

在系统启动时,通过ip_rt_init来创建一个内存池,用于分配新的缓存表项。缓存表项的分配是利用dst_alloc函数,它返回一个void指针,可以被创建者映射为相应的数据类型。从该函数名看好象分配的是dst_entry结构,但实际并非如此,它实际分配的是包含dst_entry结构的更大的表项:对IPv4分配的是rtable结构(参见图33-1),对IPv6分配的是rt6_info结构。这个函数可以被用于分配不同协议、不同大小的结构,所分配结构的大小是通过一个entry_size虚函数来表示的,这个虚函数在“DST与调用协议之间的接口”小节中描述。

33.3.3 添加元素到缓存中

每当为一个入报文或出报文选择路由时,如果缓存查找失败,则内核查找路由表并将结果保存到路由缓存内。内核利用dst_alloc分配一个新的缓存表项,根据路由表查找结果来初第33章 路由缓存 Page 33-3

Understanding Linux Network Internals 第七部分——路由

始化该表项的一些字段,最终调用rt_intern_hash将这个新表项插入到缓存表哈希桶的链表首部。当接收到一个ICMP重定向消息时,一个新的路由表项也会被添加到缓存内(参见第25章)。图33-2(a)和33-2(b)给出了rt_intern_hash函数的流程。如果内核编译时支持多路径缓存,那么缓存查找失败可能导致多个路由表项被添加到缓存内,这在“多路径缓存”小节中描述。

rt_intern_hash函数首先通过一个简单的缓存查找来确定新路由表项是否已经存在。虽然这个函数是在缓存查找失败后被调用,但该路由项可能同时已经被另一个CPU添加到缓存内。如果查找成功,则原来的缓存路由表项被移动到哈希桶链表的首部(这里假定该路由与多路径路由没有关联,即它的flag中没有设置DST_BALANCED标志);如果查找失败,则将这个新路由项添加到缓存内。

作为控制缓存容量的一种简单方法,rt_intern_hash在每次添加一个新表项时都试图删除一个表项。所以当遍历哈希桶链表时,rt_intern_hash跟踪那些符合待删除条件的路由项,并计算哈希桶链表的长度。一个路由项只有在符合被删除条件(即引用计数为0的路由项)并且哈希桶链表的长度超过配置参数ip_rt_gc_elasticity时,才被删除。如果这些条件满足,rt_intern_hash调用rt_score程序来选择最应当被删除的路由项。rt_score依据许多条件将这些路由项分为三类,从最有价值(最不适合被删除)的路由到价值最少(最适合被删除)的路由依次排列:*

选择哈希桶

锁哈希桶

End 对桶中的每个元素

Next

No匹配?

YesYes 该元素有 引用计数? 将匹配元素移到哈 希桶链表首部, 增加No 引用计数 更新待删除的最佳 候选者的信息 解锁哈希桶 更新桶链表长度 (局部变量)丢弃新缓存路由项 (rt_drop)

图33-2a rt_intern_hash函数

● 由于ICMP重定向而插入的、正被用户空间命令监控的、或由于过期将被调度的路由项。

● 出路由(用于本地生成报文的路由项)、广播路由、多播路由、到本地地址的路由(主机自己生成的报文送往本地)。 *

参见第30章“合格的缓存受害者举例”小节。

第33章 路由缓存 Page 33-4

Understanding Linux Network Internals 第七部分——路由

● 按照上一次使用的时间戳降序排列的所有其它路由项:即距离现在时间最长没有被使用的路由项先被删除。

rt_score将表项距离现在没有被使用的时长存储到一个32位变量的低30位中,然后对第二类路由项设置第31位,对第一类路由项设置第32位。最终的值是表示该路由项重要性的分值:得分越低的路由项,越有可能被rt_intern_hash选择为一个受害者。

Yes 存在待删除的 候选项? No No Yes 根据ip_rt_gc_elasticity是 否应删除 删除(rt_free) 输出路由或 Yes 转发路由? No OK Error路由(下一跳)绑 定到ARP? 解锁哈希桶 将新路由缓存添加 到桶链表的首部No 内存Error? (-ENOBUFS) 解锁哈希桶 丢弃新缓存路由项Yes (rt_drop) 返回0 返回ERROR 软中断内? No Aggressive GCYes 已运行? Yes No 丢弃新缓存路由项 (rt_drop)运行aggressive GC (rt_garbage_collect) 返回-ENOBUFS 图 33-2b rt_intern_hash函数

33.3.4 将路由缓存绑定到ARP缓存

大多数路由缓存表项要被绑定到该路由下一跳的ARP缓存表项。这意味着一条路由缓存表项或者需要一个已存在的ARP缓存表项,或者对同一个下一跳在ARP查找时能够成功。对于本地生成报文的输出路由(通过入设备ID为NULL来判断)和单播转发路由尤其需要进行绑定。在这两种情况下,需要ARP来解析下一跳的L2地址。而转发目的地为广播地址、多播地址和本机地址则不需要ARP解析,因为使用其它方法可以解析得到这个地址。

第33章 路由缓存 Page 33-5

Understanding Linux Network Internals 第七部分——路由

目的地为广播地址和多播地址的egress路由不需要相关联的ARP表项,因为通过L3地址可以得到相关的L2地址(参见第26章“特殊情况”小节)。到本地地址的路由也不需要ARP解析,因为与这类路由匹配的报文被送往本地。

通过arp_bind_neighbour来为路由创建与ARP的绑定。当函数由于缺少内存而失败时, rt_intern_hash通过调用rt_garbage_collect来强行进行一次aggressive垃圾回收操作(参见“垃圾回收”小节)。aggressive垃圾回收是通过降低ip_rt_gc_elasticity和ip_rt_gc_min_interval门限值,然后调用rt_garbage_collect来完成的。这种垃圾回收只做一次,并且只有当

rt_intern_hash的调用不是在软中断上下文中时才进行,否则将耗费大量的CPU时间。一旦完成了垃圾回收,就重新从缓存查找步骤开始来插入新的缓存表项。

33.3.5 缓存查找

一旦需要查找路由,内核首先查找路由缓存,如果缓存中没有找到则查找路由表。路由表查找过程在第35章描述,本章只讨论缓存查找。

路由子系统提供了两个不同路由查找函数,一个用于ingress,另一个用于egress:

ip_route_input

用于入流量的路由查找,这些流量可能送往本地或被转发。这个函数决定如何来处理普通报文(是送往本地、还是转发、丢弃等),但它也被其它子系统用于确定如何来处理它们的入流量。例如,ARP使用该函数来判断是否应当对一个ARPOP_REQUEST作出应答(参见第28章)。

ip_route_output_key

用于出流量的路由查找,这些流量是由本地生成,可能被送往本地或被发送出去。

这两个函数的返回值包括: 0

路由查找成功。当缓存查找失败而触发路由表查找且成功时也返回0。

-ENOBUF

由于内存问题而使查找失败。

-ENODEV

查找key包含了一个设备ID,但该设备ID无效。

-EINVAL

Generic查找失败。

内核针对这两个基本函数提供了一组封装函数,以用于特殊条件。例如,可以参见TCP是如何使用ip_route_connect和ip_route_newports。

图33-3给出了这两个从缓存查找路由的主要函数的内部流程。图中的egress函数是__ip_route_output_key,它被ip_route_output_key间接调用。

路由缓存既存储ingress路由,也存储egress路由,所以在这两种情况下都进行缓存查找。如果缓存查找失败,函数调用ip_route_input_slow或ip_route_output_slow,它们通过fib_lookup程序来查找路由表,我们将在第35章中描述这一部分。以_slow结尾的函数名是强调缓存查找和路由表查找在速度上的差异,这两种查找途径也被称为快速途径(fast path)和慢速途径(slow path)。

第33章 路由缓存 Page 33-6

Understanding Linux Network Internals 第七部分——路由

(a) ip_route_inputMiss 命中缓存查找

No 返回结果 Yes 目的地为

多播地址?

ip_route_input_slow ip_route_input_mc (b) __ip_route_output_key

Miss 命中 缓存查找

ip_route_output_slow 返回结果

图 33-3 (a) ip_route_input_key函数; (b) __ip_route_output_key函数

一旦决定进行路由,那么无论是通过缓存查找命中,还是通过查找路由表成功或失败,查找程序都返回skb,其中skb->dst->input和skb->dst->output虚函数已经被初始化,skb->dst为满足路由请求的缓存表项;当缓存查找失败时,一个新的缓存表项被创建并链接到skb->dst。

接下来,通过调用skb->dst->input(通过一个简单的名为dst_input的包装函数被调用)和skb->dst->output(通过一个名为dst_output的包装函数被调用)这两个虚函数中的一个或全部,来进一步处理报文。第18章中图18-1给出了这两个虚函数在IP协议栈中被调用的地方,以及根据流量方向被什么样的程序初始化。

第35章深入讨论了路由表慢速查找的细节。接下来的两个小节描述图33-3中这两个缓存查找程序的内部细节。它们的代码很相似,区别在于:

● 对ingress路由,ingress路由项的设备需要与ingress设备匹配,而egress设备还不知道,因此简单与空设备(0)比较。以上匹配条件反过来可应用于egress路由。

● 当缓存命中时,函数使用RT_CACHE_STAT_INC宏来更新in_hit和out_hit计数器。与路由缓存和路由表都相关的统计信息在第36章中描述。

● Egress查找需要考虑RTO_ONLINK标志(参见“Egress查找”小节)。

● Egress查找支持多路径缓存,该特性在第31章“缓存支持多路径”小节中描述。

33.3.5.1 Ingress查找

ip_route_input被用于路由ingress报文。下面给出函数原型和入参数的含义:

int ip_route_input(struct sk_buff *skb, u32 daddr, u32 saddr, u8 tos, struct net_device *dev) skb

触发路由查找的报文。这个报文本身可能不需要被路由。例如ARP出于某些原因使用ip_route_input来咨询local路由表,这时的skb应当是一个ingress ARP请求。

saddr daddr

第33章 路由缓存 Page 33-7

Understanding Linux Network Internals 第七部分——路由

用于查找的源地址和目的地址。 tos

IP包头中的TOS字段。 dev

接收该报文的设备。

ip_route_input首先根据输入条件选择应当包含该路由的哈希桶,然后一个接一个遍历哈希桶链表中的路由项,比较所有必须的字段,直到查找到匹配或到链表尾部时还没有找到匹配。

ip_route_input函数传递进来的输入参数被作为查找字段与路由缓存表项rtable中存储的fl字段*相比较,如下面的代码片段所示。通过输入参数的组合来选择哈希桶(hash变量),路由缓存表项用rth变量来表示。

hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos); rcu_read_lock( );

for (rth = rcu_dereference(rt_hash_table[hash].chain; rth; rth = rcu_dereference(rth->u.rt_next)) { if (rth->fl.fl4_dst == daddr && rth->fl.fl4_src == saddr && rth->fl.iif == iif && rth->fl.oif == 0 &&

#ifdef CONFIG_IP_ROUTE_FWMARK

rth->fl.fl4_fwmark == skb->nfmark && #endif

rth->fl.fl4_tos == tos) { rth->u.dst.lastuse = jiffies; dst_hold(&rth->u.dst); rth->u.dst.__use++;

RT_CACHE_STAT_INC(in_hit); rcu_read_unlock( );

skb->dst = (struct dst_entry*)rth; return 0; }

RT_CACHE_STAT_INC(in_hlist_search); }

rcu_read_unlock( );

如果在报文目的地址为多播时缓存查找失败,那么以下两个条件只要有一个满足,该报文就被送给多播处理函数ip_route_input_mc,否则该报文被丢弃:

● 目的地址是本地配置的多播地址,这是通过ip_check_mc来检查的。

●目的地址不是本地配置,但内核编译时支持多播路由(CONFIG_IP_MROUTE)。

下面代码给出了报文目的地址为多播时的处理:

if (MULTICAST(daddr)) { struct in_device *in_dev;

rcu_read_lock( );

if ((in_dev = __in_dev_get(dev)) != NULL) { *

flowi结构在第32章“主要的数据结构”小节中描述。

第33章 路由缓存 Page 33-8

Understanding Linux Network Internals 第七部分——路由

int our = ip_check_mc(in_dev, daddr, saddr, skb->nh.iph->protocol); if (our

#ifdef CONFIG_IP_MROUTE

|| (!LOCAL_MCAST(daddr) && IN_DEV_MFORWARD(in_dev)) #endif

) {

rcu_read_unlock( );

return ip_route_input_mc(skb, daddr, saddr, tos, dev, our); } }

rcu_read_unlock( ); return -EINVAL; }

最后,如果在目的地址不是多播情况下缓存查找失败,ip_route_input调用ip_route_input_slow来查找路由表:

return ip_route_input_slow(skb, daddr, saddr, tos, dev); }

33.3.5.2 Egress查找

__ip_route_output_key被用于路由本地生成的报文,与ip_route_input很相似:首先检查缓存,在缓存查找失败情况下调用ip_route_output_slow。当缓存支持多路径时,缓存命中需要更多的工作:在缓存内可能选择出多个合法表项,然后根据所使用的缓存算法选出一个正确的表项。选择过程是由multipath_select_route函数来实现,更多的细节可参见“多路径缓存”小节。

下面给出函数原型和入参数的含义:

int __ip_route_output_key(struct rtable **rp, const struct flowi *flp) rp

当程序返回成功时,*rp指向与搜索key flp相匹配的缓存表项。 flp

搜索key。

egress缓存查找成功需要匹配RTO_ONLINK标志,检查该标志是否设置:

!((rth->fl.fl4.tos ^ flp->fl4_tos) &

(IPTOS_RT_MASK | RTO_ONLINK)))

上面的条件只有在以下两个条件都满足时才为真:

● 路由缓存表项的TOS与搜索key中的TOS匹配。注意这个TOS字段被保存在8位tos变量的2, 3, 4, 5比特位(参见第18章中的图18-3)。*

● 当路由缓存表项和搜索key都设置了RTO_ONLINK标志,或者都没有设置。

在第18章图18-3中所示的TOS字段占用8比特,比特0被忽略,使用1到7比特位。但路由代码只用1, 2, 3, 4比特位。对egress路由不考虑precedence component(5, 6, 7比特位),这些比特位通过RT_TOS宏被屏蔽。

*

第33章 路由缓存 Page 33-9

Understanding Linux Network Internals 第七部分——路由

在第35章“搜索key初始化”小节中,你将看到RTO_ONLINK标志。该标志通过TOS变量被传递,但该变量与IP包头中的TOS字段无关。这里只是使用TOS字段的一个无用的比特位(参见第18章图18-1)。当该标志被设置时,表示目的地位于本地子网所以无须路由查找(或者换句话说,路由查找可能失败但这并不是一个问题)。该标志不是管理员在配置路由时所设置,但当路由查找时要使用该标志来指定搜索的路由类型的scope必须为

RT_SCOPE_LINK,表示目的地为直连。然后,在创建相关的路由缓存表项时保存该标志。查找时的RTO_ONLINK标志可通过下面协议来设置:

ARP

当管理员手工配置一个ARP映射时,内核确保该IP地址是本地配置子网内的地址。例如命令arp -s 10.0.0.1 11:22:33:44:55:66将10.0.0.1到11:22:33:44:55:66的映射添加到ARP缓存内。如果内核根据路由表判断IP地址10.0.0.1不属于本地配置子网内的地址,那么该命令执行将失败(参见arp_req_set和第26章)。

Raw IP和UDP

当通过一个套接字发送数据时,用户可以设置MSG_DONTROUTE标志。当应用程序从一个已知接口向直连目的地(不需要网关)发送报文时使用该标志,告诉内核不需要确定出设备。在路由协议和诊断应用程序中使用这种发送方式。

33.4 多路径缓存

在第31章“缓存支持多路径”小节中介绍了多路径缓存背后的概念。如果内核在编译时支持多路径缓存,查找代码会将多条路由添加到缓存内,这在第35章“多路径缓存”小节中描述。在本章,我们来讨论实现该特性的关键程序,以及缓存算法提供的接口。

33.4.1 注册一个缓存算法

缓存算法被定义为ip_mp_alg_ops数据结构的一个实例,该数据结构由一些函数指针组成。根据缓存算法的需要来初始化这些函数指针,并不是所有的函数指针都要被初始化,但其中的mp_alg_select_route函数指针初始化是强制性的。

向内核注册和注销缓存算法分别是用multipath_alg_register和multipath_alg_unregister。所有的缓存算法在net/ipv4/目录中按照模块来实现。

33.4.2 路由缓存与多路径之间的接口

内核在include/net/ip_mp_alg.h文件中,对ip_mp_alg_ops数据结构的每一个函数指针都定义了一个wrapper。下面给出每一个wrapper:

multipath_select_route

这是最重要的程序,它从缓存内满足一个查找条件的多个路由项中选择出正确的一项(因为它们与同一个多路径路由相关联)。这个程序被我们在前面看到的__ip_route_output_key查找函数调用。

multipath_flush

当缓存被flush时清空算法保持的任何状态。它被rt_cache_flush调用(参见“flush路由缓存”小节)。

multipath_set_nhinfo

当一个新的多路径路由被缓存时,更新算法保持的状态信息。

multipath_remove 第33章 路由缓存 Page 33-10

Understanding Linux Network Internals 第七部分——路由

当一个多路径路由被删除时,删除缓存内的相关路由(例如通过rt_free)。

并不是所有的算法都支持multipath_remove,只有加权随机算法使用multipath_flush和multipath_set_nhinfo。

在后面的小节中,我们将看到各种算法需要保持的状态信息,以及它们是如何来实现mp_alg_select_route程序的。

33.4.3 辅助程序

这里给出多路径代码中使用的一对辅助程序:

multipath_comparekeys

比较两个路由项的选择key。它主要被mp_alg_select_route算法中的回调函数调用,来查找与同一个多路径路由相关的所有缓存路由项。

rt_remove_balanced_routes

给定一个输入的缓存路由,删除它,并且从同一个哈希桶中删除与同一个多路径路由相关的所有其它的缓存路由项。rt_remove_balanced_routes的最后一个输入参数返回被删除的缓存路由项数目。这个函数的返回值为哈希桶链表中由输入路由项所指向的下一个rtable实例。调用方使用该返回值,从该位置继续扫描缓存表。当rt_remove_balanced_routes删除了哈希桶链表中最后一个相关的rtable实例后返回NULL。

33.4.4 算法之间的共同点

头脑中要始终记住以下三点要素,这将有助于你理解多路径缓存代码,尤其是缓存算法提供的mp_alg_select_route程序的实现:

● 识别与多路径路由相关的路由缓存表项需要归功于DST_BALANCED标志,该标志是在它们插入缓存之前被设置(参见第36章“dst_entry结构”小节)。我们将在第35章看到如何以及何时来设置该标志。该标志通常是在路由缓存代码中使用,根据缓存表项与一条多路径路由是否相关来采取不同的动作。

● dst_entry结构被用于定义缓存路由项,且包含一个上次使用的时间戳(dst->lastuse)。 每当缓存查找返回一个缓存路由项时,更新该缓存路由项的时间戳。与多路径路由相关的缓存项需要特殊处理。当缓存查找返回与一个多路径路由相关的缓存项时,与该多路径路由相关的所有其它缓存项也必须更新其时间戳,这是为了避免垃圾回收算法将这些路由缓存项清除。

● mp_alg_select_route程序输入的路由项是与查找key相匹配的第一个缓存项。元素被添加到路由缓存内的方式是,与同一个多路径路由相关的所有其它缓存项都位于同一个哈希桶内。由于这个原因,mp_alg_select_route将从输入缓存元素开始来遍历哈希桶链表,识别出其它的路由缓存项要归功于DST_BALANCED标志和multipath_comparekeys 程序。

33.4.5 随机算法

随机算法不需要保持任何状态信息,因此既不需要分配任何内存,也不会占用很多的CPU时间来决策。该算法所做的就是遍历输入路由项所在的哈希桶链表,计算合法的缓存项数目,利用本地程序random生成一个随机数,根据该随机数来选择相应的缓存项。

该算法的定义是在net/ipv4/multipath_random.c文件内。

第33章 路由缓存 Page 33-11

Understanding Linux Network Internals 第七部分——路由 33.4.6 加权随机算法

这是实现最复杂的算法。一条多路径路由的每一个下一跳指定有一个权值。该算法按照权值比例来随机选择正确的下一跳(即缓存内正确的路由项)。

对一条多路径路由的每一个下一跳,都有一个fib_nh数据结构实例来存储权值和其它参数。我们将在第34章看到这些数据结构在路由表中位于何处,可以参见该章中的图34-1。

在第31章“加权随机算法”小节内,解释了该算法背后的基本思想。为了能够快速决策,该算法构建一个本地信息数据库,用于访问fib_nh实例和读取下一跳的权值。图33-4给出了在配置了以下两条多路径路由后,数据库的可能样子:

#ip route add 10.0.1.0/24 mpath wrandom nexthop via 192.168.1.1 weight 1 nexthop via 192.168.2.1 weight 2

#ip route add 10.0.2.0/24 mpath wrandom nexthop via 192.168.1.1 weight 5 nexthop via 192.168.2.1 weight 1

该数据库不是在定义多路径路由时构建的,而是在路由查找时构建的。

记住mp_alg_select_route程序(此时为wrandom_select_route)的输入是与搜索key相匹配的第一个缓存路由项。所有其它的缓存路由项都位于同一个路由缓存哈希桶内。

通过mp_alg_select_route来选择路由分为两个步骤:

1. mp_alg_select_route首先遍历输入路由项所在的哈希桶链表,对每一个路由缓存,通过multipath_comparekeys程序来检查选择该缓存是否合法。同时,它创建一个局部链表保存合法的缓存路由项,主要目标是定义一条与第31章中的图31-4相类似的线。图33-5给出了本章所举的例子中链表的可能样子。添加到该链表的每一个路由使用图33-4中的数据库得到其权值,并初始化相应的power字段。

2. mp_alg_select_route生成一个随机值,针对合法路由项的链表,根据第31章“加权随机算法”小节中描述的机制从中选择一条路由。

我们来看如何从这个state数据库来查找。要记住缓存路由项(即rtable实例)包含了下一跳路由器和出设备。给定一条缓存路由,__multipath_lookup_weight首先根据出设备从state中选择出相应的哈希桶:state是以设备为索引。一旦选择出一个state桶,就开始扫描multipath_route元素链表,查找与网关和设备字段相匹配的项。一旦得到正确的

multipath_route实例,就开始扫描相关的multipath_dest结构链表,查找与输入查找key fl中的目的IP地址相匹配的表项。函数可以从匹配的multipath_dest实例中,通过nh_info指针来读取下一跳权值,该指针指向相应的fib_nh实例。

state数据库是由multipath_set_nhinfo程序来构建的,该程序在“路由缓存与多路径之间的接口”小节中描述。

该算法的定义是在net/ipv4/multipath_random.c文件内。

33.4.7 轮转算法

轮转算法不需要额外的数据结构来保持所需要的状态信息。所需要的全部信息可以从dst_entry结构的dstÆ__use字段得到,该字段表示缓存查找返回该路由项的次数。因此选择合适的路由项就是对输入的哈希桶链表中的路由项遍历,从合法路由项中选择__use值最小的项。

该算法的定义是在net/ipv4/multipath_rr.c文件内。

第33章 路由缓存 Page 33-12

Understanding Linux Network Internals 第七部分——路由

state MULTIPATH_STATE_SIZE(15) struct multipath_routelist oif gw=192.168.1.1 dests 头 锁

... list nh_info netmask=255.255.255.0network=0.10.0.1 prefixlen=24 list nh_info netmask=255.255.255.0network=0.10.0.2 prefixlen=24 ... 头 锁 struct multipath_routelist oif gw=192.168.2.1 dests struct multipath_dest ... struct multipath_dest ... list 头 锁 nh_info netmask=255.255.255.0network=0.10.0.1 prefixlen=24 list nh_info netmask=255.255.255.0network=0.10.0.2 prefixlen=24 struct multipath_bucket struct multipath_dest

图 33-4 加权随机算法创建的下一跳数据库

struct multipath_dest

第一个mpc 最后一个mpc struct struct struct struct multipath_candidate multipath_candidatemultipath_candidatemultipath_candidate next next next next power=10,000power=10,000power=10,000power=10,000 rt rt rt rt struct rtable struct rtablestruct rtablestruct rtable

图 33-5 为选择下一跳而创建的临时链表举例

第33章 路由缓存 Page 33-13

Understanding Linux Network Internals 第七部分——路由

33.4.8 设备轮转算法

设备轮转算法的目的和效果在第31章“设备轮转算法”小节中解释。该算法选择合适的出设备,因而就是对一个给定的多路径路由得到合适的缓存表项,drr_select_route程序的流程如下:

1. 全局向量state对每个设备保持一个计数器来表示该设备已经被选择的次数。

2. 对每个多路径路由,只考虑给定设备上的第一个下一跳。这加快了决策速度,但也暗示在共享同一个出设备的多个下一跳之间不会负载共享:对每一个设备,任何一个多路径路由项中只用一个下一跳。

3. 当遍历路由(也就是下一跳)计算最小使用次数时,那些还没被使用的设备相关的路由项被给予更高的优先级。当选择了一个新设备时,一个新表项被添加到state中。

4. 分析得到使用次数值最小的设备,该设备对应的第一条路由被选中。

该算法的定义是在net/ipv4/multipath_drr.c文件内。

33.5 DST与调用协议之间的接口

DST 缓存是一个子系统,例如它有自己的垃圾回收机制。作为一个子系统,它提供了一组函数,各种协议可以使用这些函数来改变或调整其行为。当外部子系统需要与路由缓存交互时,诸如通知它有一个事件发生或是读取某一个参数值时,是通过net/core/dst.c和include/net/dst.h文件中定义的一组DST程序来进行的。拥有路由缓存的L3协议通过初始化一个dst_ops VFT实例对外提供一组函数,DST程序是对这些函数的封装,如图33-6所示。

ARP, TCP, IPsec, IPIP等 IPv4 路由缓存

APIs IPv4 dst_link_failure等 DST

IPv6

路由缓存

IPv6

图 33-6 dst_ops接口

DST向更高层提供的关键结构是dst_entry,与协议相关的诸如rtable等结构只是对这个结构的封装。IP层拥有路由缓存,但其它协议通常保持到这些路由缓存元素的引用。所有这些引用都指向dst_entry,而不是封装该结构的rtable。sk_buff缓冲区也保持到dst_entry结构的一个引用,而不是到rtable结构的引用,这个引用被用于存储路由查找结果。

dst_entry和dst_ops结构在第36章相关小节中有详细描述。针对每个协议有一个dst_ops实例,例如IPv4使用的实例为ipv4_dst_ops,它在net/ipv4/route.c文件中被初始化:

struct dst_ops ipv4_dst_ops = { .family = AF_INET,

.protocol = _ _constant_htons(ETH_P_IP), .gc = rt_garbage_collect, .check = ipv4_dst_check, .destroy = ipv4_dst_destroy, 第33章 路由缓存 Page 33-14

ipv6_dst_ops ipv4_dst_ops Understanding Linux Network Internals 第七部分——路由

.ifdown = ipv4_dst_ifdown,

.negative_advice = ipv4_negative_advice, .link_failure = ipv4_link_failure, .update_pmtu = ip_rt_update_pmtu, .entry_size = sizeof(struct rtable), };

一旦DST子系统被通知有事件发生,或通过其中一个DST接口程序发起请求,则与受影响的dst_entry实例相关的协议得到通知,通过调用其dst_ops VFT实例来激活由dst_entry提供的相应的函数。例如如果ARP向上层协议通知一个给定的IPv4地址为不可达,则对相关的dst_entry结构调用dst_link_failure(记住缓存路由项是与IP地址相关联,而不是与网络相关联),这将调用由IPv4通过ipv4_dst_ops注册的ipv4_link_failure程序。

调用协议也可能直接介入DST的行为。例如当IPv4请求DST分配一个新的缓存表项时,DST可能意识到需要开始垃圾回收并激活rt_garbage_collect,该程序由IPv4自己提供。

当针对某个给定的通知类型所进行的处理对所有的协议都相同时,这个相同逻辑可能直接在DST API中实现,而不是在每个协议的处理钩子中都复制一份。

在DST的dst_ops结构中的一些虚函数是通过更高层的封装函数来激活的,没有被封装的函数通过dst->ops->function语法来直接激活。这里给出dst_ops虚函数的含义,并简要描述IPv4子系统中赋值给这些虚函数的程序(在前面代码片段中列出):

gc

进行垃圾回收。当子系统通过dst_alloc来分配一个新的缓存表项,当该函数发现内存不够时进行垃圾回收。IPv4程序rt_garbage_collect在“同步清理”小节中描述。

check

dst_entry被标记为dead的缓存路由项通常不再被使用,但当使用IPsec时该结论并不一定成立。这个程序检查一个obsolete dst_entry是否还有用。但是,ipv4_dst_check程序在删除dst_entry结构之前并不检查它是否还有用;而在相应的xfrm_dst_check程序中要对IPsec做“xfrm”转换。还可以参看诸如sk_dst_check(在第21章介绍)等程序是如何检查缓存路由项的状态。没有针对该函数的封装程序。

destroy

被dst_destroy调用,DST运行该程序来删除一个dst_entry结构,并将删除通知调用协议,以便调用协议先做一些必要的清理工作。例如IPv4程序ipv4_dst_destroy使用该通知来释放其它数据结构的引用。dst_destroy在“删除DST表项”小节中描述。

ifdown

被dst_ifdown调用,当一个设备被关闭或注销时,DST子系统激活该函数。对每一个受影响的缓存路由项都要调用一次(参见“外部事件”小节)。IPv4程序ipv4_dst_ifdown用一个指向loopback设备的指针来替换rtable中指向设备IP配置的idev指针,这是因为loopback设备总是存在。

negative_advice

被DST函数dst_negative_advice调用,它被用于向DST通知某个dst_entry实例出现问题。例如当TCP检测到一次写操作超时时使用dst_negative_advice。

IPv4程序ipv4_negative_advice使用该通知来删除缓存路由项。当这个dst_entry已经被标记为dead(通过其dst->obsolete标志,我们将在“删除DST表项”小节中看到),ipv4_negative_advice就释放到该dst_entry的rtable引用。

第33章 路由缓存 Page 33-15

Understanding Linux Network Internals 第七部分——路由

link_failure

被DST函数dst_link_failure调用,它是在发送报文时由于检测到目的地不可达而被激活。

下面举例描述该函数的使用。在IPv4和IPv6中分别使用邻居协议ARP和邻居发现(Neighbor Discovery),当激活该函数时表示它们从来没有接收到它们生成的solicitation请求的应答,该请求被用于解析L3地址到L2地址的关联(这通常是由于超时,例如可以参看net/ipv4/arp.c文件中处理ARP协议行为的arp_error_report)。对其它高层协议,诸如各种隧道(IP over IP等)协议,当到达隧道另一端出现问题时做相同的处理。注意距离隧道另一端可能有多跳。例如,可以参看在net/ipv4/ipip.c文件中用于IP-over-IP隧道协议的ipip_tunnel_xmit。

update_pmtu

更新缓存路由项的PMTU。通常是在处理所接收到的ICMP分片需求消息时调用,参见第31章“处理Ingress ICMP重定向消息”小节。没有针对该函数的封装程序。

get_mss

返回该路由使用的TCP最大段(MSS)。IPv4不初始化该程序,所以没有针对该函数的封装程序。参见“IPsec转换与dst_entry的使用”小节。

除了上面列出的这些虚函数的封装程序以外,DST也通过一些不与其它子系统交互的函数来操作dst_entry实例。例如“异步清理”小节中给出的dst_set_expires,第26章给出的如何使用dst_confirm来确认一个邻居是否可达。更多的细节可参见net/core/dst.c和include/net/dst.h文件。

33.5.1 IPsec转换与dst_entry的使用

在前面的小节中,我们看到dst_entry结构的常见用途:对一条缓存路由,存储与协议无关的信息,包括对接收到的或要发送的报文在路由查找之后处理该报文的input和output方法。

dst_entry结构的另一个用途是用于IPsec。IPsec是一组可以在IP层之上提供诸如认证和保密等安全服务的协议。IPsec使用dst_entry结构来构造它所称的transformation bundles。一个transformation是针对一个报文的一个操作,例如加密。一个bundle就是被定义为一连串操作的一组transformations。一旦IPsec协议决定将所有的transformations应用于与一个给定路由相匹配的流量,那么在路由缓存中存储的信息是一个dst_entry结构链表。

在一般情况下,一个路由项与一个dst_entry结构相关联,该结构的input和output字段描述了如何来处理匹配报文(转发、送往本地等等,参见第18章中的图18-1)。但IPsec创建一个dst_entry实例链表,只有最后一个实例中的input和output方法被实际应用于路由决策;前面实例中的input和output方法被应用于所要求的transformations,见图33-7所示(图中给出的是一个简化模型)。

在dst_entry结构中使用child指针来构造dst_entry链表。IPsec还使用另一个指针path,它指向链表的最后一个元素(即使不使用IPsec也将创建该元素)。

在链表中的其它dst_entry元素,即除了最后一个元素以外的每一个元素都要实现一个IPsec transformation。每个元素设置其path字段指向最后一个元素。而且,每个元素都设置其DST_NOHASH标志,以使DST子系统知道该元素不在路由缓存哈希表内,而由另一个子系统来处理它。

第33章 路由缓存 Page 33-16

Understanding Linux Network Internals 第七部分——路由

IPsec对路由查找的影响如下:输入和输出路由查找受到该数据结构布局的影响,见图33-7(b)中IPsec配置示意图。查找返回结果是一个指针,它指向第一个执行transformation的dst_entry,而不是最后一个表示实际路由信息的dst_entry。这是因为第一个dst_entry实例表示第一个要执行的transformation,且必须按照顺序来执行transformations。

你可以在好几个地方找到IP或路由层与IPsec之间的交互:

● 对egress流量,ip_route_output_flow函数(被“缓存查找”小节中介绍的

ip_route_output_key调用)内包含额外的代码(即调用xfrm_lookup)来与IPsec交互。

● 对送往本地的ingress流量,ip_local_deliver_finish调用xfrm4_policy_check来查找IPsec策略数据库。

● ip_forward对需要被转发的ingress流量做同样的检查。

IP代码有时直接调用一般的xfrm_xxx IPsec程序,有时调用名为xfrm4_ xxx的IPv4封装程序。

(a)

dst

sp input output

sk_buff dst_entry

b) (dst_entrydst_entrydst_entry dst child child child sp input input input output output output

sk_buffDST_NOHASHDST_NOHASHDST_NOHASH

path path path child input output

DST_NOHASH

path

dst_entry

图 33-7 dst_entry的使用 (a) 没有IPsec;(b) 有IPsec

33.5.2 外部事件

当dst_init初始化DST子系统时,向设备事件通知链netdev_chain注册,该通知链在第4章介绍。DST只对两类事件感兴趣,一类是当一个网络设备变为DOWN时

(NETDEV_DOWN),另一类是当设备被注销时(NETDEV_UNREGISTER)。你可以在in include/linux/notifier.h文件内找到完整的NETDEV_XXX 事件列表。

当一个设备变为不能被使用时,或是因为它不再存在(例如已经从内核中被注销),或是因为管理原因而被关闭(shut down),此时用到该设备的所有路由项也就不能被使用。这第33章 路由缓存 Page 33-17

Understanding Linux Network Internals 第七部分——路由

意味着路由表和路由缓存需要得到这类事件的通知并作出反应。我们将在第34章中看到路由表如何来处理这类事件。这里我们将看到如何来清理路由缓存。缓存路由项的dst_entry结构可以被插入到以下两个地方中的一个地方:

● 路由缓存。

● dst_garbage_list链表。只有当待删除的路由项的引用都被释放,才能够被垃圾回收过程视为可以被删除。

缓存内的表项被事件通知处理器fib_netdev_event(在第32章“对路由表的影响”小节中描述)接管,它flushes缓存。在dst_garbage_list链表中的表项被DST向netdev_chain通知链注册的程序接管。下面的代码片断是从net/core/dst.c文件内摘取,DST处理事件通知的处理器为dst_dev_event:

static struct notifier_block dst_dev_notifier = { .notifier_call = dst_dev_event, };

void _ _init dst_init(void) {

register_netdevice_notifier(&dst_dev_notifier); }

dst_dev_event遍历由dead dst_entry结构组成的dst_garbage_list链表,对每一项调用

dst_ifdown。dst_ifdown的最后一个输入参数为要处理的事件。这里给出它如何来处理这两类事件:

NETDEV_UNREGISTER

当设备被注销时,对该设备的所有引用都必须被删除。dst_ifdown将dst_entry结构和相关的neighbour实例中*到该设备的引用都替换为到loopback设备的引用。

NETDEV_DOWN

因为设备为down,所以不再能够向该设备发送流量。因而,dst_entry中的input和output程序被分别设置为dst_discard_in和dst_discard_out。这两个程序将送来的任何输入buffer(即它们被要求处理的任意帧)简单丢弃掉。

我们在“IPsec转换与dst_entry的使用”小节中看到,一个dst_entry结构能够与其它的dst_entry结构通过child 指针链接在一起。dst_ifdown 沿着每一个child指针来更新所有的dst_entry项。只对最后一项更新input和output程序,因为只有最后一项使用这些程序来接收或发送。

我们在第8章中看到注销一个设备不仅触发一个NETDEV_UNREGISTER通知,而且还触发一个NETDEV_DOWN通知,因为设备只有被关闭(shut down),才能够被注销。这意味着当一个设备被注销时,dst_dev_event要处理两类事件。这解释了为什么dst_ifdown检查其unregister参数,当该参数被设置时特意跳过部分代码而仅执行其它的部分。

33.6 Flush路由缓存

一旦系统中发生变化,使缓存中的一些信息变为过期,内核就flush路由缓存。在许多情况下只有一些表项过期,但内核为了简化而删除所有的表项。触发flush的主要事件包括:

设备变为UP或变为DOWN

*

参见第27章“L2头部缓存”小节。

第33章 路由缓存 Page 33-18

Understanding Linux Network Internals 第七部分——路由

通过一个给定设备在过去可达的一些地址,可能不再可达或者是由于有更好的路由而通过另一个设备可达。

向设备添加或删除一个IP地址

我们在第32章“添加一个IP地址”和“删除一个IP地址”小节中看到,Linux对每个本地配置的IP地址创建一个具体路由。当一个地址被删除时,缓存内相关的所有路由项也要被删除。被删除地址最有可能配置的网络掩码不是/32,所以在同一子网内与该地址相关的所有缓存项也应当被删除*。最后,如果同一子网内有一个地址被用作其它非直连路由的网关,与这些地址相关的所有缓存项也应当被删除。而Flush整个缓存要比跟踪所有这些情况更为简单。

全局转发状态或设备的转发状态发生变化

如果禁止转发,就需要删除可以转发流量的所有缓存路由项。参见第36章“使能和禁止转发”小节。

一条路由被删除

与被删除路由项相关的所有缓存表项需要被删除。

通过/proc接口请求进行flush

这在第36章“/proc/sys/net/ipv4/route目录”小节中描述。

用于flush缓存的程序是rt_run_flush,但它从来不被直接调用。请求对缓存flush是通过rt_cache_flush函数来实现,它根据调用方提供的输入参数delay的取值,决定是立即flush缓存还是启动一个定时器。当delay的取值:

小于0

缓存在内核参数ip_rt_min_delay指定的时间后被flush,该参数可以通过/proc来调整,参看第36章“/proc/sys/net/ipv4/route目录”小节中的描述。 0

缓存立即被flush。

大于0

缓存在指定的时间后被flush。

一旦递交了一个flush请求,在ip_rt_max_delay秒之内肯定进行一次flush,这个参数缺省值为8秒。当一个flush请求被递交且已经存在一个flush请求准备执行的情况下,重启定时器来反映这个新请求。但这个新请求不能让定时器迟于ip_rt_max_delay秒之后才过期,因为那时前一个定时器已经执行了。这是通过使用全局变量rt_deadline来完成的。

除此之外,缓存还被一个周期性定时器rt_secret_timer定期flush,该定时器每隔

ip_rt_secret_interval秒(该参数缺省值见第36章“/proc/sys/net/ipv4/route目录”小节)之后到期。当该定时器到期后,处理函数rt_secret_rebuild对缓存flush并重启该定时器。可以通过/proc来配置ip_rt_secret_interval参数。 当删除一个第二地址时这并不为真,参见第32章“删除一个IP地址”小节。

*

第33章 路由缓存 Page 33-19

Understanding Linux Network Internals 第七部分——路由 33.7 垃圾回收

就象在第30章“路由缓存的垃圾回收”小节中讲到的,存在两种垃圾回收:

● 当检测到内存不够时需要释放内存。这实际上分为两个任务,一个同步任务和一个异步任务。同步任务是由特殊条件在无规律的时间点触发,异步任务是在一个定时器到期时或多或少有规律地运行。

● 清理内核被要求删除的所有dst_entry结构,但它们可能因为还被某些项引用而不能被立即删除。

本小节讨论第一种垃圾回收中的同步任务和异步任务,另一种垃圾回收在“删除DST表项”小节中详细描述。

同步和异步垃圾回收使用一个公共程序来判断一个给定的dst_entry实例是否符合删除条件:rt_may_expire。这个程序接受两个参数(tmo1, tmo2)来表示候选者在符合删除条件之前必须在缓存中所处的最短时间。其中,tmo2用于那些被认为特别好的应当被删除的候选项,tmo1用于所有其它的候选项,就象在第30章“合格的缓存受害者举例”小节描述的那样。ip_rt_gc_timeout参数是指缓存中其它表项的时间。

这两个值越低,表项就越有可能被删除。这就是为什么在rt_check_expire中每当一个表项没有被删除,就将局部变量tmo减半,参见“异步清理”小节中的描述。我们还将在

“rt_garbage_collect函数”小节中看到,rt_garbage_collect对这两个门限值也是进行同样的处理。

33.7.1 同步清理

当DST子系统检测到内存不够时,触发一个同步清理。尽管是由DST来决定何时触发垃圾回收,但处理程序是由拥有缓存的协议提供的。所有的处理都通过dst_ops结构中的虚函数来控制,这些函数在“DST与调用协议之间的接口”小节中有描述。在那里我们看到dst_ops 有一个函数为gc,IPv4将它初始化为rt_garbage_collect。在以下两种情况下调用gc:

● 当添加一个新表项到路由缓存中但发现内存不够时。当添加一个表项时,

rt_intern_hash必须将路由与下一跳相关的neighbour数据结构相绑定(参见“将路由缓存绑定到ARP缓存”小节)。如果没有足够的内存来分配一个新的neighbour数据结构,则扫描路由缓存尝试去释放部分内存。可以这样做的原因是因为可能有一些缓存项暂时没有被使用,删除这些缓存项也可能删除相关的neighbour表项(这里用的是“可能”,因为一个数据结构只有在所有对它的引用都被删除后,该结构才能够被删除)。

●当添加一个新表项到路由缓存中,但表项总数将超过门限值gc_thresh时。分配表项的dst_alloc函数通过缓存容量为一个固定值来触发一个清理,以减少占用的内存。可以通过/proc来配置gc_thresh(参见第36章“通过/proc文件系统调整路由”小节中的描述)。

下面一个小节讨论rt_garbage_collect的内部细节。

33.7.2 rt_garbage_collect函数

rt_garbage_collect的逻辑流程在图33-8(a)和33-8(b)中描述。

rt_garbage_collect程序所做的垃圾回收需要花费大量的CPU时间。因此,如果该程序上次被调用的时间与现在的间隔小于ip_rt_gc_min_interval秒,则不做任何事而立即返回。除非缓存内的表项数目已经达到最大值ip_rt_max_size,这时要求立刻执行。

第33章 路由缓存 Page 33-20

Understanding Linux Network Internals 第七部分——路由

ip_rt_max_size是一个硬性。一旦达到这个门限值,dst_alloc函数将失败,除非rt_garbage_collect设法释放部分内存。

递增counter gc_total 距离上次gc超过 No 最小要求间隔? (ip_rt_gc_min_interval) No YesYes 缓存满? 估计要删除的缓存表更新counter 项数目(goal)gc_ignored End 对哈希表内的 每一个桶 Next 捕获桶的锁 对每一个 缓存路由项 Next 该路由项 释放桶的锁 No 可以被删除? (rt_may_expire) 使需求更激进 YesNo 1 已经达到goal? 删除该路由项 Yes 更新rover 更新goal (最后扫描的桶) 考虑多路径缓存特性 图33-8a rt_garbage_collect函数

这里给出rt_garbage_collect的逻辑结构:

● 首先计算可能删除的缓存表项数目(goal)。根据这个值及缓存内当前的表项数目(ipv4_dst_ops.entries),计算出一旦goal个表项被删除,还剩余的表项数目,这个数目被保存到equilibrium中。

● 遍历哈希表,尝试使最符合条件的表项过期,利用rt_may_expire检查它们是否符合过期条件。将符合删除条件的表项调用rt_free直接删除,如果它们与多路径路由相关则调用rt_remove_balanced_route来删除(参见“辅助程序”小节)。

● 一旦扫描完缓存哈希表,检测删除的表项数是否已经达到goal个。如果没有,则用更为激进的判断标准*来重新扫描哈希表。 * 译者注:即将expire减半

第33章 路由缓存 Page 33-21

Understanding Linux Network Internals 第七部分——路由

被删除的表项数(goal)依赖于哈希表内的表项数目。目的是当哈希表内的表项数目越多时,表项过期也就越快。

已经到达goal?

Yes 更新counter (gc_goal_miss)

Yes 需求可以更激进?

No

使需求更激进 2 可以对哈希表 No 再次循环扫描?

Yes 缓存满? Yes No (ip_rt_max_size)

缓存满? Yes (ip_rt_max_size)

返回成功(0) 更新counter gc_dst_overflow 返回失败(1)

图 33-8b rt_garbage_collect函数

利用图33-9的帮助,我们可以清晰地理解rt_garbage_collect中计算goal使用的一些门限值:

● 哈希表的大小为rt_hash_mask+1,或2rt_hash_log。当缓存内的表项数目超过gc_thresh时rt_garbage_collect被调用,gc_thresh的缺省值为哈希表的大小。

● 缓存内能够容纳的表项最大数目是ip_rt_max_size,其缺省值为哈希表的大小乘以16。

● 当缓存内的表项数目超过ip_rt_gc_elasticity*(2rt_hash_log)时,其缺省值为哈希表的大小乘以8,认为缓存有变大的风险,垃圾回收开始设置goal更为激进。

一旦确定好了各个门限值,rt_garbage_collect就开始遍历哈希表来寻找受害者。但哈希表的遍历并不是简单地从第一个桶开始到最后一个桶。rt_garbage_collect利用一个静态变量rover来记住前一次函数激活时扫描到的最后一个桶。这是因为不一定必须完整地扫描一遍该哈希表。通过记住上次扫描的哈希桶,程序就可以公平对待所有的桶,而不总是从第一个桶第33章 路由缓存 Page 33-22

Understanding Linux Network Internals 第七部分——路由

开始来选择受害者。受害者是通过rt_may_expire来识别,这个程序已经在“垃圾回收”小节中描述,它需要传递两个时间门限值来确定两类表项是否符合删除条件。当扫描一个桶的元素时,每当一个元素不符合删除条件,则降低(减半)第一个时间门限值。当扫描到每个桶链表的末尾时,函数将再次检查被删除表项的数目是否为函数开始时设置的数目goal。如果不是,函数将继续扫描下一个桶。该过程一直继续直到完成整张表的扫描。这时,函数将传递给rt_max_expire的第二个时间门限值降低(减半),以便更有可能寻找到符合条件的受害者。如果重新扫描哈希表不会花费太多时间的话,则重新开始扫描。如果程序是在软中断上下文中被调用,或者前一次扫描花费的时间超过一个jiffies(在x86平台上即为1/1000秒),则认为本次重新扫描哈希表将花费太多的时间,因而跳过不做。

(rt_hash_mask+1=2rt_hash_log) gcthresh

ip_rt_gc_elasticity*(2rt_hash_log) =8*(rthashmask+1) ip_rt_max_size=16*(rt_hash_mask+1)图 33-9 垃圾回收门限值

33.7.3 异步清理

同步垃圾回收被用于处理内存不够时的特殊情况。但在采取行动之前避免出现极端条件

更为可取:换句话说,极端条件出现的概率越低越好。这正是通过周期性定时器来异步清理所要做的。

当路由子系统初始化时,ip_rt_init启动定时器rt_periodic_timer,当它到期时激活处理函数rt_check_expire。每当rt_check_expire被激活时,它只扫描一部分缓存。它保持一个静态变量(rover)来记住前一次函数激活时扫描到的最后一个桶,每当该函数激活时就从下一个桶开始扫描。当rt_check_expire完成扫描整张哈希表或已经运行至少一个jiffies时,就重新启动该定时器,函数返回。

如果缓存内表项的时间已经过期,或者通过rt_may_expire判断出符合删除条件,则调用rt_free删除这些表项。当表项与一条多路径路由相关时,调用rt_remove_balanced_route删除该表项。

while ((rth = *rthp) != NULL) { if (rth->u.dst.expires) {

if (time_before_eq(now, rth->u.dst.expires)) { tmo >>= 1;

rthp = &rth->u.rt_next; continue; }

} else if (!rt_may_expire(rth, tmo, ip_rt_gc_timeout)) { tmo >>= 1;

rthp = &rth->u.rt_next; continue; }

/* Cleanup aged off entries. */

#ifdef CONFIG_IP_ROUTE_MULTIPATH_CACHED /* remove all related balanced entries if necessary */ if (rth->u.dst.flags & DST_BALANCED) { 第33章 路由缓存 Page 33-23

Understanding Linux Network Internals 第七部分——路由

rthp = rt_remove_balanced_route( &rt_hash_table[i].chain, rth, NULL); if (!rthp) break; } else {

*rthp = rth->u.rt_next; rt_free(rth); }

#else /* CONFIG_IP_ROUTE_MULTIPATH_CACHED */ *rthp = rth->u.rt_next; rt_free(rth);

#endif /* CONFIG_IP_ROUTE_MULTIPATH_CACHED */ } ...

if (time_after(jiffies, now))) break;

该定时器缺省每隔ip_rt_gc_interval秒到期,该参数缺省值为60,但可以通过

/proc/sys/net/ipv4/route/gc_interval文件来调整(参见第4章“通过/proc文件系统调整参数”小节)。当该定时器第一次到期时,设置下一次到期是在ip_rt_gc_interval和2*ip_rt_gc_interval(见ip_rt_init)之间的一个随机时间之后。使用随机值的原因是为了避免不同内核子系统的定时器可能在同一时间到期而花费大量CPU的可能性。如果在启动过程中许多子系统同时启动,并且都是按照相同的间隔时间来调度,那么就可能出现这种情况。

33.7.4 过期标准

缺省情况下,路由缓存表项永远不会过期,因为dst_entry->expires为0。* 当能够使缓存表项过期的事件发生时(参见第30章“使路由缓存表项过期的事件举例”小节),利用dst_set_expires函数†设置表项的dst_entry->expires时间戳字段为一个非零值来使表项能够过期:

●当接收到一个ICMP UNREACHABLE或FRAGMENTATION NEEDED消息时,所有相关路由项(缓存中目的地址与ICMP消息payload中携带有IP头的目的IP地址相同的路由缓存项)的PMTU必须被更新为ICMP头中指定的MTU。ICMP核心代码调用ip_rt_frag_needed来更新路由缓存。‡ 这些受影响的表项在可配置的时间ip_rt_mtu_expires之后被设置为过期,这个时间值缺省为10分钟,可以用/proc/sys/net/route/mtu_expires来改变。更多细节可参见第25章。

● 当TCP代码用路径MTU发现算法来更新一条路由的MTU时,调用ip_rt_update_pmtu函数,该函数将调用dst_set_expires。参见第18章对路径MTU发现算法的更多细节描述。

● 当一个目的IP地址被认为不可达时,通过直接或间接调用dst_ops数据结构中的

link_failure方法,将缓存内相关的dst_entry结构标记为不可达(参见“DST与调用协议之间的接口”小节)。

33.7.5 删除DST表项

在前面的小节中,我们看到rtable缓存表项是如何被同步清除或异步清除,以及被后台的垃圾回收所删除。在本小节内,我们将看到如何来处理rtable内嵌入的dst_entry结构,删除dst_entry的函数是dst_free。

在dst_alloc中调用memset来设置dst_entry->expires字段。

注意当使一个表项立即过期而调用dst_set_expires时,用1来代替输入值0,以便将这种情况与表示永远不过期的0区别开来。 ‡

译者注:ip_rt_frag_needed中是否应当用rcu_write_lock,而不是rcu_read_lock?

†*第33章 路由缓存 Page 33-24

Understanding Linux Network Internals 第七部分——路由

dst_hold和dst_release函数分别用于递增或递减一个dst_entry的引用计数。但是当调用dst_release释放最后一个引用时,该表项并不被自动删除。实际上,dst_entry是在rt_free和rt_drop删除相关的rtable结构时被间接删除。这两个函数通过dst_rcu_free调度执行dst_free,这涉及到RCU机制(参见“缓存锁”小节)。

我们在“IPsec转换与dst_entry的使用”小节中看到,dst_entry结构并不总是嵌入在rtable结构内。孤立的dst_entry实例可通过调用dst_free来直接删除。

一个dst_entry的删除并不复杂,但是要理解dst_free及其辅助程序是如何工作,就需要了解以下要点:

● 当一个表项仍然被引用时不能被删除时,设置其obsolete标志为2来标记它为dead(dst->obsolete的缺省值为0)。试图删除一个已经标记为dead的表项将失败。

● 我们在“IPsec转换与dst_entry的使用”小节中看到,一个dst_entry实例可能有

children。当删除一个链表内的第一个dst_entry时,路由子系统也必须删除所有其它的表项。但与此同时,需要记住只要表项还有引用就不能被删除。

清楚了上面两点,我们现在来看dst_free是如何工作的。

当调用dst_free删除一个引用计数为0的表项,则调用dst_destroy立即删除该表项。

dst_destroy函数也尝试去删除链接到该结构的任何children。当有一个children由于仍然被引用而不能被删除时,dst_destroy返回指向该child的一个指针,由dst_free来处理它。

当调用dst_free删除一个引用计数非0的表项,这包括刚才描述的dst_destroy不能删除一个child时的情况,则做以下处理:

● 通过设置其obsolete标志来标记该表项为dead。

● 用两个钩子函数dst_discard_in和dst_discard_out来替换该表项原来的input和output程序,以此来确保相关的路由不能够接收和发送报文(参见第36章“dst_entry结构”小节内对input和output的描述)。这种处理方式是在设备还没有处于运行或处于down状态(没有设置IFF_UP标志)时的典型做法。

我们在“外部事件”小节内看到当dst_dev_event处理的两类事件发生时,调用dst_ifdown来处理dst_garbage_list中的dst_entry结构。只有在特殊情况下才用

dst_discard_in和dst_discard_out来替换该结构内当前的input和output方法。这种做法并非多余,因为只有当释放dst_entry是与设备被关闭(shut down)相关时dst_free才这样做,而dst_dev_event处理的事件并不一定总是设备被关闭事件。

● 将dst_entry结构添加到dst_garbage_list全局链表内,该链表将所有应当被删除的,但由于引用计数非0而还没有被删除的表项链接在一起。

● 调整dst_gc_timer定时器在可配置的最小延迟时间(DST_GC_MIN)后到期,在该定时器还没有运行时激活它。

dst_gc_timer定时器周期性地遍历dst_garbage_list链表,利用dst_destroy来删除引用计数为0的表项。当定时处理函数dst_run_gc没有删除链表内所有的表项时,就再次启动该定时器但使它在稍后一点时间到期。更准确地说,该定时器的到期时间再延迟DST_GC_INC秒,最长延迟时间为DST_GC_MAX。但是当每次dst_free将一个新表项添加到dst_garbage_list链表中时,重新设置到期延迟时间为缺省最小值DST_GC_MIN。

图33-10(a)和33-10(b)给出了dst_free的逻辑流程图。

第33章 路由缓存 Page 33-25

Understanding Linux Network Internals 第七部分——路由

表项为dead?No Yes 表项仍然被 引用?No 释放所有对外部数据结构的引用 dst_destroyYes返回

更新counter dst->ops->entiesl3提供有destroy 处理钩子函数?No Yes运行该钩子函数释放dst_entry 数据结构 child结构?还有No返回NULL从链表中取出第一个child 将其引用计数减1Yes child仍然 被引用?No图33-10a dst_free函数

第33章 路由缓存 Page 33-26

Understanding Linux Network Internals 第七部分——路由

__dst_free 捕获dst_lock锁

Yes 该设备UP?NoYes 被设置?dst->devNoinput=dst_discard_inoutput=dst_discard_out 标记表项为dead(obsolete = 2) 将表项添加到dst_garbage_list链表内调整dst_gc_timer在最小延迟时间后到期释放dst_lock锁 图 33-10b dst_free函数

33.7.6 调整和控制垃圾回收的变量

下面给出控制DST垃圾回收任务所用到的全局变量和参数的含义:

dst_garbage_list

等待被删除的dst_entry结构组成的链表。当dst_gc_timer定时器到期时,执行定时处理钩子函数。只有引用计数__refcnt大于0的(不能被直接删除的)表项才被放入该链表内,以避免被直接删除。新表项被插入到链表首部。

dst_gc_timer_expires dst_gc_timer_inc

dst_gc_timer_expires是定时器在到期之前等待的秒数,取值范围在DST_GC_MIN和DST_GC_MAX之间,当定时处理钩子函数dst_run_gc运行而没能清空

dst_garbage_list链表时,等待时间增加dst_gc_timer_inc。但dst_gc_timer_inc必须在DST_GC_MIN到DST_GC_MAX的范围内。

第33章 路由缓存 Page 33-27

Understanding Linux Network Internals 第七部分——路由

在表33-1中列出了前面提到的三个常量的取值,这些常量在include/net/dst.h文件内定义。

表 33-1 DST_GC_XXX常量

变量名称 值 DST_GC_MIN HZ/10

DST_GC_INC HZ/2 DST_GC_MAX 120*HZ

33.8 Egress ICMP重定向限速

就象在第25章讨论的,当内核检测到路由不是最优时将生成ICMP重定向消息。这些ICMP消息被路由子系统处理,在RFC 1812的4.3.2.8小节中建议对它们进行限速。

限速所用的算法是一个很简单的指数回退算法(exponential backoff algorithm)。如果目的地持续忽略ICMP重定向消息,内核就持续发送ICMP重定向消息给它直到发送数目到达ip_rt_redirect_number,每发送一个消息就翻倍间隔时间。当发送的ICMP重定向数目达到ip_rt_redirect_number时,内核停止发送,直到ip_rt_redirect_silence秒过后还没有输入报文能够触发内核生成ICMP重定向消息。一旦ip_rt_redirect_silence秒过后有输入报文触发内核生成ICMP重定向消息,内核才重新开始发送ICMP重定向消息。

指数回退算法的初始延迟时间为ip_rt_redirect_load,所有三个ip_rt_redirect_xxx参数都可以通过/proc配置,这些参数的缺省值可参见第36章。

egress重定向消息的所有逻辑是在ip_rt_send_redirect中实现,当内核检测到需要发送一个ICMP重定向消息时,内核调用该函数来处理(参见第20章)。

dst_entry结构中有两个字段是用于实现ICMP重定向消息限速:

rate_last

上一个IMCP重定向消息送出的时间戳。

rate_tokens

已经向与该dst_entry实例相关的目的地发送的ICMP重定向消息的数目。所以,rate_tokens-1表示目的地连续忽略的ICMP重定向消息的数目。

第33章 路由缓存 Page 33-28

Understanding Linux Network Internals 第七部分——路由

第34章 路由表

由于路由在网络栈中的核心地位以及路由表的容量可以很大,因而设计高效的路由表来加快各种操作,尤其是查找操作就很重要。本章描述了Linux是如何来组织路由表,如何利用各种哈希表来访问路由表的各个数据结构,每一种哈希表都是为了不同方式的查找而专门设计的。

34.1 路由哈希表的组织

为了能够针对各种各样的操作而快速搜索到相关信息,Linux中采用了几种不同的指向相同数据结构的哈希表来描述路由表项:

● 一组基于网络掩码长度来访问路由的哈希表(在“Per-Netmask哈希表的组织”小节中描述)。

● 一组直接搜索fib_info结构的哈希表(在“fib_info结构的组织”小节中描述)。

● 一个以网络设备为索引,可以快速搜索路由项下一跳的哈希表(在“下一跳路由器结构的组织”小节中描述)。

● 一个可以快速标识由路由项下一跳所用网关的哈希表。(???)

34.1.1 Per-Netmask哈希表的组织

在路由表项的最高一级,是由基于网络掩码长度的哈希表组成。因为IPv4使用32比特地址,因而IPv4地址就有33个不同的网络掩码长度(范围从/0到/32,其中/0表示缺省路由)。路由子系统对每一个网络掩码长度维护一个不同的哈希表,路由表就是由这些哈希表与下面将提到的其他表共同组成。*

图34-1给出了一个路由表中主要数据结构之间的关系。所有这些主要数据结构的简要介绍可参见第32章,详细介绍可参见第36章,本章主要描述这些数据结构之间的关系。

34.1.1.1 哈希表组织的基本结构

路由表是由fib_table数据结构来描述。fib_table结构包含一个由33个指针组成的向量,每一个指针对应一个网络掩码长度并指向一个类型为 fn_zone 的数据结构。(术语zone是指掩码相同的一些网络)。fn_zone结构将路由表项组织成为一些哈希表,将目的网络的掩码长度相同的路由表项放在一个哈希表内。因而对任意的路由表项,可以根据其网络掩码长度快速得到其对应的哈希表。非空的fn_zone桶通过链表组织起来,该链表的头部被保存在fn_zone_list中。我们将在第35章中看到该链表的使用。

在这些每个网络掩码对应一个哈希表的组织结构中有一个例外,那就是缺省路由/0 zone对应的哈希表,它是由仅包含一个桶的简单链表组成。这样设计的原因是因为一台主机很少会维护多个缺省路由。

路由表项是由多个不同的数据结构来描述的,每个数据结构描述不同片段的路由信息。因为只通过部分字段可以区分多条路由表项,因而一条路由信息被分散到多个数据结构内。这样,不是维护一个庞大而臃肿的结构而是将路由表项分散为多个片段,路由子系统更容易在相似的路由表项之间共享公共片段信息,因而就可以出不同的功能,并在这些功能之间定义更加清晰的接口。

每个网段(subnet)对应一个fib_node实例,用变量fn_key来表示,它的值为网段。例如对子网10.1.1.0/24而言,fn_key为10.1.1。注意:fib_node结构(即该结构中的fn_key变量)与一 译者注:英文Per-Netmask tables或Per-Netmask hash tables的字面意思为“每个网络掩码对应一张哈希表”,但英文表达更为贴切、简要,所以这里不直译而用英文。

*第34章 路由表 Page 34-1

Understanding Linux Network Internals 第七部分——路由

个网段相关,而不是与一条路由表项相关;一定要记住该细节以避免后面混淆。这个细节的重要性是由于多条路由表项可能都有相同的网段。

有相同网段的路由表项(即fn_key相同)共享同一个fib_node。每一条路由表项有各自的fib_alias结构。例如:可能有一些路由表项,它们的网段相同而只是TOS值不同:每一个fib_alias实例因而有不同的TOS值。每个fib_alias实例与一个存储真正路由信息(即如何到达目的地)的fib_info结构相关联。

对于一个fib_node实例,相关的fib_alias实例链表按照IP TOS(即fa_tos字段)的递增顺序排列。fa_tos值相同的fib_alias实例按照相关的fib_info中fib_protocol字段的递增顺序排列。

本章前面提到路由子系统划分为多个数据结构,目的是优化它们的使用并使逻辑更加清晰。所以,fib_alias与fib_info之间的关联不是一对一,多个fib_alias结构可能共享一个

fib_info结构。当多条不同的路由表项碰巧与一个已存在的fib_info结构中的参数值相同时,它们就指向这同一个fib_info实例。通过fib_info结构中的一个引用计数来记住共享数目。

例如,有五条到不同目的网络的路由表项碰巧使用相同的下一跳网关,那么对所有这五个表项而言下一跳信息都是相同的,因而就可以共享同样的下一跳信息。这种情况下就有五个fib_node结构和五个fib_alias结构,但只有一个fib_info结构。

在图34-1中给出的一个配置例子中,展示出本章描述各种哈希表的不同数据结构之间的一些关系。在这个图中:

● 存在四条路由表项(即四个fib_alias实例)。

● 这四条路由表项是到三个不同的网段(即三个fib_node实例),这是由于有两个fib_alias实例共享一个fib_node实例。

● 四条路由表项中有两条共享同一个下一跳路由器。因而,这两个fib_alias结构中的fa_info字段指向同一个fib_info结构,见图的右下角。

图中数据结构字段左侧出现的钥匙标志是查找程序中所使用的字段,在第35章中将看到具体的过程。

struct fib_table Æ struct fn_hash Æ

struct fn_zone(掩码长度)Æ

struct fib_node(网段)Æ

struct fib_alias(TOS、type, scope, state)Æ

struct fib_info (protocol, prefsrc, priority, metrics, treeref)Æ

struct fib_nh(nh_dev, nh_flags, nh_scope, nh_oif, nh_gw)

第34章 路由表 Page 34-2

Understanding Linux Network Internals 第七部分——路由

struct fib_table * struct fib_table * struct fib_table * struct fib_table * struct hlist_head struct hlist_head … struct hlist_head … … struct hlist_head struct fib_table tb_id tb_stamp tb_lookup() … … … tb_select_default() fz_next fz_hash fz_divisor

RT_TABLE_MAX+1 缺省路由 struct fn_zone fz_next fz_hash fz_divisor tbdata…/0 struct fn_zone *struct fn_zone *33 struct fn_zone fz_next fz_hash fz_divisor struct fn_zone *…/32 struct fn_zone *struct fn_zone *fn_zone_liststruct fn_zone struct fib_infostruct fn_hash fib_hash fib_lhash fib_treeref=1fib_protocolfib_prefsrc fib_priority fib_metrics fib_nhs=1 … fib_nh *nh_hash*nh_parent… … …struct fib_infofib_hash fib_lhash fib_treeref=1fib_protocolfib_prefsrc fib_priority fib_metrics fib_nhs=1 … fib_nh *nh_hash*nh_parent… … …*nh_hash*nh_parent… … …struct fib_nodefn_hash fn_alias fn_key struct fib_nodefn_hash fn_alias fn_key fa_list fa_infofa_tos fa_typefa_scopefa_statestruct fib_aliasfn_hash fn_alias fn_key struct fib_nodefa_list fa_infofa_tos fa_typefa_scopefa_statestruct fib_aliasfa_list fa_infofa_tos fa_typefa_scopefa_statestruct fib_aliasfz_divisor struct fib_nh fa_list fa_infofa_tos fa_typefa_scopefa_statestruct fib_aliasstruct fib_info… 图 34-1 路由表的组织 第34章 路由表 Page 34-3

Understanding Linux Network Internals 第七部分——路由 34.1.1.2 Per-Netmask哈希表容量的动态变化

当元素数量超过一个给定的门限值时,增加哈希表的容量fz_hash。一个哈希表的容量可以不断增长直到一个给定的上限值。在“添加一条路由表项”小节中将解释新元素被插入到哈希表中时是如何触发哈希表容量的改变。

对于33个哈希表,由fn_hash*指针指向的的每一个哈希表的容量是变化的。当哈希表中元素数量超过桶数量的两倍时,改变哈希表的容量。桶数量存储在fz_divisor变量中,如图34-1所述。选择这种启发式方法主要是为了对哈希表的查找时间。保持哈希表中元素数量低于这个门限值可以使查找加快(假定元素分布均匀)。

一个哈希表的最大容量是从与具体架构中内存管理相关的参数得出的。在i386架构中,最大容量是8 MB。因为哈希表中每一个元素是由一个指针组成,而在32位处理器上每个指针占用4个字节,因而i386系统能够支持一个哈希表的容量超过2百万个桶。

当通过fn_new_zone接口第一次创建一个哈希表时,该哈希表的缺省容量为16个桶。(唯一一个例外是存储缺省路由表项的/0 zone,如前面的章节所述)。在哈希表容量扩展的前两次,容量首先为256,然后是1024。随后的容量扩展总是在当前容量基础上翻一倍。

目前还没有容量缩减机制,所以如果一个zone的哈希表中元素数量从280减少为10,哈希表的容量不会从1024减小为16。†

34.1.2 fib_info结构的组织

如图34-1所描述,每一个fib_info结构包含fib_hash与fib_lhash两个字段,用于将结构插入到两个更为复杂的哈希表中,如图34-2所示。这两个哈希表是:

fib_info_hash

所有的fib_info结构被插入到这个哈希表中,通过fib_find_info接口来查找该表。

fib_info_laddrhash

主要在路由表项有一个首选源地址时,才将fib_info结构插入到这个表中。在第35章“首选源地址的选择”中描述了首选源地址(preferred source address)的使用。这个地址通常是根据设备上配置情况自动得到的,但也可以配置它。

这个哈希表主要是便于删除由于本地配置IP地址的删除而影响到的路由表项(见fib_sync_down接口)。

在这两个哈希表中,都是通过fib_create_info接口将新元素添加到一个桶链表的首部。

34.1.2.1 全局哈希表容量的动态变化

在所有路由表中,fib_info结构的总数是存储在计数器fib_info_cnt中。当通过

fib_create_info接口创建一个fib_info实例,该计数器值增长;当通过free_fib_info接口删除一个fib_info实例,该计数器值减小。

当创建一个新的fib_info实例时,在fib_create_info中检查fib_info_cnt是否已经超过哈希表的容量fib_hash_size,如图34-2所示。当超过哈希表容量时,将fib_info_hash与 fib_info_laddrhash的容量都翻一倍。然后用fib_hash_free接口删除旧的哈希表,利用

fib_hash_alloc接口分配新的哈希表,利用fib_hash_move接口将旧hash表中的fib_info实例转移到新的哈希表中。

*† 不要将fn_hash与fz_hash混淆。

译者注:这里原文有误,原文是从526减小为16。

第34章 路由表 Page 34-4

Understanding Linux Network Internals 第七部分——路由

注意:本小节中讨论的容量变化与“Per-Netmask哈希表容量的动态变化”小节中讨论的容量变化无关。

fib_hash_size fib_info_hash fib_hash fib_hash fib_hash fib_lhashfib_lhashfib_lhash struct fib_infostruct fib_infostruct fib_info fib_hash fib_lhash fib_info_laddrhash struct fib_info

fib_hash_size 图 34-2 fib_info结构的组织

34.1.3 下一跳路由器结构的组织

如图34-1所示,每个fib_info结构可以包含一个或多个fib_nh结构,每个fib_nh表示一个下一跳路由器(next-hop router)。一个下一跳路由器的信息包括通过哪一个设备作为出接口。因而,当找到这个router时就很容易得到出接口设备,但这个结构并没有提供当出接口设备已知时如何快速查找到router的方法,而这种查找能力从两个方面讲是很重要的:

当出接口设备被关闭(shut down)时

网络子系统必须disable与该设备相关的所有路由表项,这是通过fib_sync_down接口来完成的,详细描述见第32章。

当出接口设备被使能或重新使能时

网络子系统必须使能或重新使能与通过该出接口设备可达的下一跳路由器相关的所有路由表项,这是通过fib_sync_up接口来完成的,详细描述见第32章。

还有另外一个小情况是与ICMP重定向消息有关。在第31章“处理Ingress ICMP重定向消息”小节中,我们看到只有当ICMP重定向消息中新建议的网关是本地已知的一个路由器时,内核才可能接受该消息。为了检查这个条件是否满足,内核只需要遍历与接收ICMP消息的设备相关的所有路由表项,查找是否有路由表项使用新建议的网关作为其下一跳路由器。这个逻辑是由ip_fib_check_default接口实现的,该接口被ip_rt_redirect调用,

ip_rt_redirect接口又被icmp_redirect调用,这正是ingress ICMP重定向消息的处理钩子函数。

这项需求是通过创建另一个以设备标识为索引的哈希表来解决的,这样就可以非常快速地查找到下一跳路由器。在图34-1中所示的nh_hash字段是用来将fib_nh结构插入fib_info_devhash哈希表的。该表是在net/ipv4/fib_semantics.c中静态分配的,容量是DEVINDEX_HASHSIZE(256)个桶。通过fib_create_info接口将新元素插入到fib_info_devhash表中哈希桶链表的首部。

第34章 路由表 Page 34-5

Understanding Linux Network Internals 第七部分——路由 34.1.4 两个缺省路由表:ip_fib_main_table与ip_fib_local_table

在系统初始化时总是创建下面两个路由表,它们与内核配置选项无关:

ip_fib_local_table

内核将到本地地址的路由表项放在该表中,包括到相关的网段地址以及网段广播地址的路由表项,详见第36章“内核插入路由项:fib_magic函数”小节。用户不能够直接配置该路由表。

ip_fib_main_table

所有其他的路由表项(包括用户配置的静态路由表项,路由协议生成的动态路由表项)都放在该表内。

在第30章“特殊路由”小节中,解释了这两个路由表之间的关系。在第35章中,还将看如何使用它们来查找路由。

34.2 路由表初始化

路由表的初始化是由net/ipv4/fib_hash.c文件中定义的fib_hash_init函数实现的,该函数被初始化IP路由子系统的ip_fib_init函数调用来创建ip_fib_main_table和ip_fib_local_table表(参见第32章“路由子系统初始化”小节)。

当fib_hash_init被首次调用时,创建用于分配fib_node数据结构的内存池fn_hash_kmem。

fib_hash_init首先分配一个fib_table数据结构,然后按照表34-1所示的钩子函数初始化其虚函数。接下来将该结构的下半部分(fn_hash)的内容清空,以基于网络掩码长度将路由表项分布到不同的哈希表内,如图34-1所示。

表 34-1 fib_table结构中虚函数的初始化

虚函数 钩子函数

tb_lookup fn_hash_lookup tb_insert fn_hash_insert tb_delete fn_hash_delete tb_flush fn_hash_flush tb_select_default fn_hash_select_default tb_dump fn_hash_dump

34.3 路由表项的添加与删除

在第36章中,我们将看到用户命令和路由守护进程是如何添加、删除、修改路由表项的,这都是通过内核路由子系统中的一组程序来实现的。本章我们将看到内核是如何对一个路由表来执行添加和删除操作的。如表34-1所示,fn_hash_insert与fn_hash_delete被用于添加和删除路由表项,我们将在下面的“添加一条路由表项”和“删除一条路由表项”小节中对此加以分析。fn_hash_insert有好几个相关的使用,都涉及路由表项的变化。

这里给出上述两个程序都用到的一些公共操作:

● 当添加或删除一条路由表项时,构造搜索key,用它来查找一个fib_node和一个

fib_alias。这些查找与路由数据报文所走的流程类似,但目的不同:这里的目的是为了检查待添加的路由项是否已经存在,或检查待删除的路由表项是否还存在。

● 生成(插入时)以及清除(删除时)相应的哈希表。

● 必要时flush路由缓存表。 第34章 路由表 Page 34-6

Understanding Linux Network Internals 第七部分——路由

● 当一条路由项已经添加到路由表中,或从路由表中删除一条路由项时,生成一条Netlink广播通知,告诉感兴趣的模块(参见第32章“Netlink通知”小节)。

34.3.1 添加一条路由表项

添加一条新的路由表项是通过fn_hash_insert接口来实现的,其逻辑可见图34-3(a)与34-3(b)。* 这个函数实际上被许多操作调用:除了插入新的路由项以外,还处理尾部追加

(appending)、首部追加(prepending)、变化(changing)和替换(replacing)。这些不同的操作是由传入的NLM_F_XXX flags参数来区分的。在第36章的表36-1中列出了与每一种操作相关联的flags组合。

不同操作的不同需求使得这个函数的逻辑变得复杂。例如,在“路由哈希表的组织”小节中提到,TOS值不同的多个路由表项可能有着同一个目的地。当内核添加一条新路由表项时,如果路由表中已经存在目的地与TOS都相同的路由表项,则函数返回错误。然而,该条件实际上是替换路由表项的前提条件。所以,由fn_hash_insert接口所进行的路由查找需要根据命令类型返回不同的结果。

正如在“Per-Netmask哈希表容量的动态变化”小节中所描述的,插入一条新路由表项可能触发一个zone哈希表容量的动态变化,这是通过fn_rehash_zone接口实现。正如在“下一跳路由器结构的组织”小节中所描述的,当路由表项指定一个首选源地址时,新的fib_info结构被添加到fib_info_devhash哈希表内。表示该路由表项下一跳的每一个fib_nh结构也被添加到fib_info_devhash哈希表内。

当一个替换(replace)操作用一条新的路由项来替换一条已存在的路由项时,内核flush路由缓存表以便不再使用旧的路由项。

无论是哪一种操作类型,都要生成一个Netlink消息来通知感兴趣的子系统。

34.3.2 删除一条路由表项

删除一条路由表项是通过fn_hash_delete接口实现的,其逻辑可见图34-4。因为只有一类操作,所以删除路由表项要比添加路由表项简单。

fn_hash_delete首先构造搜索key,然后用它来查找待删除的表项是否还存在。当查找到fib_alias结构时,就删除它并通过Netlink广播通知感兴趣的子系统,如果该路由表项已经被使用(即设置了FA_S_ACCESSED标志)则flush路由缓存表。

删除一个fib_alias实例可能删除一个fib_info实例和一个fib_node实例(参见图34-1):

● 当一个待删除的fib_alias为相关联的fib_node实例的最后一个实例时,也删除该fib_node实例。

● 当相关联的fib_info实例的引用计数fib_treeref为空时,由于不再需要而被删除。更具体地讲,fn_free_alias释放匹配的fib_alias实例时,调用fib_release_info接口递减与之相关联的fib_info实例中的引用计数fib_treeref。当该引用计数为0时,从所有的哈希表中摘除该fib_info实例,设置fib_dead标志标记该fib_info实例状态为dead,在

fib_info_put中通过free_fib_info接口释放它。从哈希表中也摘除与该fib_info实例相关联的下一跳信息,可参见“下一跳路由器结构的组织”小节。

对fa_list和fn_alias链表的操作受到fib_hash_lock锁的保护(见图34-1)。 *

该流程图与源代码并不是完全一致,但逻辑上是一致的。

第34章 路由表 Page 34-7

Understanding Linux Network Internals 第七部分——路由

fib_create_info

Zone存在吗?No创建新的zone (fn_new_zone) Yes 根据地址和网络掩码构造搜索key 根据key查找fib_info是否存在Yes增加使用的引用计数No 创建一个新的fibinfo实例提供了首选源地址?Yes将fib_info添加到hash表fib_info_laddrhash内 No End 对该fib_info的每一个下一跳Next将下一跳添加到hash表fib_info_devhash内 zone需要改变容量吗?Yes改变zone的容量 (fn_rehash_zone) No 图34-3a fn_hash_insert函数

第34章 路由表 Page 34-8

Understanding Linux Network Internals 第七部分——路由

创建新的fib_info实例 插入 fz_hash表 (fib_insert_node) 错误, 路由项已经存在 改变/替换 操作类型 错误 添加 Append Prepend 创建并初始化一个新的 fib_alias实例 将fib_alias实例链接到 其fib_node上 发送Netlink广播RTM_NEWROUTE

未发现 基于上面计算的key查找fib_node和fib_alaias 发现 不匹配 与优先级匹配TOS吗? 匹配 添加 改变/替换 操作类型链接新的fib_info并删除旧的 Append/Prepend End 对该fib_info的每一个下一跳Next TOS/优先级/type/scope匹配吗?不匹配 匹配 错误, 路由项重复 图 34-3b fn_hash_insert函数

34.3.3 垃圾回收

当路由表项由于配置改变或本地设备状态改变而无效时应当被删除。在路由子系统中提供了多个能够遍历路由表或部分路由表的函数。在一定的条件下,函数fib_sync_down设置RTNH_F_DEAD标志来标记可以合法删除的路由表项。当后来调用fib_flush时,再次遍历路由表并删除设置了该标志的路由表项。有周期性的函数来清理(clean up)路由缓存表中无效的表项,而针对路由表没有类似的函数。

对fib_sync_down程序的描述可参见第32章“辅助程序”小节。 第34章 路由表 Page 34-9

Understanding Linux Network Internals 第七部分——路由

根据地址和网络掩码构造搜索key

查找fib_node和第一个合法的fib_alaias未找到 找到 对每一个fib_alias实例Next End 返回-ESRCH 不匹配 添加 匹配? 匹配发送Netlink广播RTM_NEWROUTE 该路由已经被用?NO删除匹配的fib_alias(fn_free_alias) YesFlush路由缓存表 (rt_cache_flush) 删除的是最后一个 fib_alias实例?NO返回0 Yes删除fib_node (fn_free_node)图 34-4 fn_hash_delete函数

34.4 策略路由及其对路由表定义的影响

当内核编译时配置了支持策略路由的选项,那么管理员可以配置最多255个的路由表。为了支持这个功能选项,而又能在不使用策略路由的情况下保持路由子系统的代码简洁清爽,Linux开发人员对源代码添加了一些复杂处理,在阅读这些文件之前应当理解这部分代码的复杂性。

34.4.1 变量与结构定义

在支持策略路由情况下,指向255个路由表的指针被存储在fib_tables数组内,该数组在net/ipv4/fib_frontend.c文件中定义,如图34-1所示。

struct fib_table *fib_tables[RT_TABLE_MAX+1]; 第34章 路由表 Page 34-10

Understanding Linux Network Internals 第七部分——路由

注意:ip_fib_main_table与ip_fib_local_table这两个路由表分别被定义为fib_tables数组的元素,见include/net/ip_fib.h:

#ifndef CONFIG_IP_MULTIPLE_TABLES extern struct fib_table *ip_fib_local_table; extern struct fib_table *ip_fib_main_table; ... ... ... #else

#define ip_fib_local_table (fib_tables[RT_TABLE_LOCAL]) #define ip_fib_main_table (fib_tables[RT_TABLE_MAIN]) ... ... ... #endif

当向一个新路由表中添加第一条路由表项时,利用fib_hash_init接口来初始化该路由表。在没有配置策略路由的情况下,该函数只在系统启动时调用,因而带有__init宏。*但在配置策略路由的情况下,可以在任意时刻创建一个新路由表。所以fib_hash_init不带有__init宏。这可以解释函数原型定义在不同条件下存在差异的情况:

#ifdef CONFIG_IP_MULTIPLE_TABLES struct fib_table * fib_hash_init(int id) #else

struct fib_table * __init fib_hash_init(int id) #endif {

... ... ... }

即使在支持策略路由的情况下,除非明确给出了一个路由表的表ID,否则所有配置的路由表项都被添加到ip_fib_main_table表中。路由表ID只在新的ip命令中提供,而在传统的route命令中则没有提供。

34.4.2 每个函数有两个定义

策略路由特性并不是完全透明集成到路由代码内。例如内核要支持策略路由,必须用前置条件变量CONFIG_IP_MULTIPLE_TABLES来保护相应的变量、程序或代码片段。†

有一些全局变量和函数都有两个定义,一个用于不支持策略路由,另一个用于支持策略路由。在net/ipv4/fib_rules.c与include/net/ip_fib.h文件中定义了两个重要的函数:

fib_lookup

用于路由表的查找,在第35章中描述。

fib_select_default

用于转发报文且没有到达目的地的路由表项时选择一个缺省路由。

除了这两个函数以外,还有其它一些函数也有两个定义,例如fib_get_table(根据路由表ID返回表)和fib_new_table(创建一个新的路由表)。

在阅读源代码时,尤其是诸如TAGS和cscope等工具的源代码时,重要的是要知道这些函数存在有两个定义。否则,在分析代码时可能得出错误的代码流程。

*†

第7章描述了__init宏的使用和含义。

不要将多路由表与多路径混淆,这两个特性是完全不同的、相互的。

第34章 路由表 Page 34-11

Understanding Linux Network Internals 第七部分——路由

第35章 路由查找

在第33章,我们看到了入流量和出流量是如何触发查找的。总是首先搜索缓存,当缓存没有查找到时,通过ip_route_input_slow和ip_route_output_slow函数来查找路由表。在本章,我们将分析这些函数,我们尤其将涉及:

● ingress路由和egress路由有什么样的区别

● fib_lookup是如何搜索路由表的

● 策略路由查找与一般查找有什么样的区别

● 多路径路由是何时以及如何来处理的

● 如何选择一个缺省网关

35.1 查找函数的High-Level View

不管流量是哪个方向,都是利用fib_lookup来查找路由表。但是,就象在第34章“每个函数有两个定义”小节中提到的,fib_lookup有两个版本,一个是当内核支持策略路由时使用(net/ipv4/fib_rules.c),一个是当内核不支持策略路由时使用(include/net/ip_fib.h)。选择哪一个函数是在编译时决定的,所以当ip_route_input_slow和ip_route_output_slow调用fib_lookup时,它们透明地激活正确的查找函数。

我们简单看看在路由查找时用到的一些关键函数,在这里的讨论中,你将会发现参考第34章中的图34-1是很有帮助的。

fib_lookup程序是对每一个路由表所提供的查找函数的一个包装(wrapper)。当不支持策略路由时,查找函数版本是针对local表和main表。当支持策略路由时,逻辑更为复杂,需要查找由策略路由提供的路由表。

正如图35-1所示,fib_lookup激活查找函数fn_hash_lookup,它正是fib_table的函数指针tb_lookup的初始化程序(参见第34章“路由表初始化”小节),这个函数找出相应的fib_node实例,该实例的key与目的地址相匹配。接下来,fn_hash_lookup调用

fib_semantic_match来查找与匹配的fib_node相关联的fib_alias实例。如果找到相应的fib_alias实例,在配置多路径情况下fib_semantic_match还需要选择出正确的下一跳。

fib_lookup(选择路由表) fn_hash_lookup(fib_node查找)

fib_semantic_match(fib_alias查找)

图 35-1 主要的路由查找函数之间的关系

这里介绍的这些函数将在后面的小节内详细描述,这些小节主要的内容是:

● 对ingress流量和egress流量,如何使用fib_lookup来查找路由(“输入选路”小节和“输出选路”小节)

● 如何实现fn_hash_lookup函数(“路由表查找:fn_hash_lookup”小节)

● fib_semantic_match函数(“Semantic匹配其它标准”小节)

第35章 路由查找 Page 35-1

Understanding Linux Network Internals 第七部分——路由

● 支持策略路由的fib_lookup版本与基本查找函数*之间的区别是什么(“fib_lookup函数”小节)

35.2 辅助程序

下面给出本章中一些函数内所调用的辅助程序:

fib_validate_source

对从一个给定设备接收到的报文的源IP地址检验,检测企图的IP欺骗。而且还要在使能非对称路由情况下,确保报文的源IP地址通过该报文接收接口是可达的(参见第31章“反向路径过滤”小节)。另外,该程序也返回用于反方向的首选源地址spec_dst,详见下面“首选源地址选择”小节;并初始化路由标签(Routing tag),参见第31章“基于路由表的Classifier”小节。

inet_select_addr

给定一个设备dev,一个IP地址dst,和一个作用范围scope,返回作用范围为scope的第一个主地址,在通过出设备dev向地址dst发送报文时使用。

因为每个设备可能配置有多个地址,而且每个地址有各自的scope,所以需要该程序。

提供dst参数的原因在于,如果在设备dev上配置的不同IP地址属于不同子网,程序就可以返回与dst在同一子网的IP地址。

在第30章“Scope”小节内,我们看到每个设备可能配置有多个主IP地址和多个第二地址。inet_select_addr只返回主地址。

如果在dev上配置的地址都不满足由scope和dst限定的条件,程序则尝试其他设备,检验是否存在一个IP地址,配置有所要求的scope。因为loopback_dev是dev_baselist链中所插入的第一个设备,所以首先检查的就是它。

rt_set_nexthop

给定一个路由缓存项rtable和一个路由表查找结果res,完成rtable内各字段的初始化,诸如rt_gateway、所嵌入的dst_entry结构的metrics向量等等。这个函数还对在第31章“基于路由表的Classifier”小节内描述的路由标签初始化。

35.3 路由表查找:fn_hash_lookup

所有的路由表查找,不论路由表是否由策略路由提供,也不论流量方向如何,都是利用fn_hash_lookup来查找。在fib_hash_init(参见第34章“路由表初始化”小节)中,fn_hash_lookup被注册为fib_table结构中的tb_lookup函数指针所对应的处理钩子。

在第30章介绍了该函数在查找时所使用的LPM算法,该算法有效地利用了路由表的组织结构为每个网络掩码对应一个路由表,参见第34章中的图34-1。fn_hash_lookup搜索一个能够将报文路由到特定目的地的fib_node实例。

fn_hash_lookup函数的原型为:

static int fn_hash_lookup(struct fib_table *tb, const struct flowi *flp, struct fib_result *res) * 译者注:基本查找函数是指不支持策略路由的fib_lookup版本。

第35章 路由查找 Page 35-2

Understanding Linux Network Internals 第七部分——路由

下面给出各输入参数的含义: tb

搜索的路由表。因为fn_hash_lookup是一个通用的查找程序,每次只能够查找一张表。调用方根据是否支持策略路由和相关的因素,来决定搜索哪些路由表。 flp

搜索key。 res

查找成功时,利用路由信息来初始化res。

下面给出可能的返回值:

0:成功

已经根据转发信息初始化res(通过fib_semantic_match函数)。

1:失败

没有与搜索key匹配的路由项。

小于0:管理失败(Administrative failure)

这表示查找不成功,因为查找到的路由没有价值:例如相关的主机可能被标记为不可达。

LPM算法从表示最长网络掩码的zone开始遍历路由。这是因为网络掩码越长,就意味着路由更具体,也就意味着报文可能更接近最终目的地(例如/27网络掩码只能涵盖30台主机,而/24网络掩码中可以涵盖2台主机,所以首先检查/27网络掩码)。因而,搜索时遍历所有的active zone,且从网络掩码最长的zone开始。我们在第34章“路由哈希表的组织”小节中看到,所有的active zone按照网络掩码长度排序组成一张链表,fn_zone_list存储该链表的首部。

struct fn_hash *t = (struct fn_hash*)tb->tb_data; read_lock(&fib_hash_lock);

for (fz = t->fn_zone_list; fz; fz = fz->fz_next) { struct hlist_head *head; struct hlist_node *node; struct fib_node *f;

在fn_hash_lookup函数中,将目的IP地址与检查的active zone的网络掩码相与

(ANDs),与操作的结果作为搜索key。例如,如果正在检查/24 zone,目的地址flp->fl4_dst为10.0.1.2,则搜索key k为10.0.1.2 & 255.255.255.0,结果为10.0.1.0。这意味着接下来的代码要搜索到子网10.0.1.0/24的路由:

u32 k = fz_key(flp->fl4_dst, fz);

因为路由被存储在哈希表(fz_hash)内,所以首先通过一个哈希函数对搜索key k进行哈希,得到相应的哈希桶head。接下来是遍历与该哈希桶相关的路由链表(fib_node结构),查找与k匹配的路由项。

head = &fz->fz_hash[fn_hash(k, fz)];

hlish_for_each_entry(f, node, head, fn_hash) {

第35章 路由查找 Page 35-3

Understanding Linux Network Internals 第七部分——路由

if (f->fn_key != k)) { continue;

err = fib_semantic_match(&f->fn_alias, flp, res,

f->fn_key, fz->fz_mask, fz->fz_order);

if (err < 0) goto out; } }

err = 1; out:

read_unlock(&fib_hash_lock); return err; }

我们在第34章“路由哈希表的组织”小节中看到,一个fib_node覆盖了同一子网内的所有路由项,但这些路由项在诸如TOS等其他字段上可能不同。现在,如果fn_hash_lookup函数找到了与搜索key k匹配的fib_node,它还需要检查每一个潜在的路由项,来查找与输入参数flp中其他搜索字段相匹配的路由。这个详细检查是由fib_semantic_match来完成的,这将在下一小节描述。

fib_semantic_match也用查找结果来初始化输入参数res,如果它返回成功,

fn_hash_lookup就将该结果返回给调用方。fn_hash_lookup遍历路由表中所有的active zone,直到fib_semantic_match或者返回一个成功结果,或者发现该路由表中没有可用的路由(即这些路由项都不匹配)。

35.3.1 Semantic匹配其它标准(Subsidiary Criteria)

fib_semantic_match函数的目的是在与给定的fib_node相关的路由(fib_alias结构)中,查找与搜索key所有字段都匹配的路由项。我们在前一小节看到主要字段,即报文被路由到的目的IP地址,在调用fib_semantic_match函数之前已经在fn_hash_lookup中查找到匹配项。现在由fib_semantic_match函数来检查其他字段是否匹配。

一旦fib_semantic_match查找到相匹配的fib_alias实例,就从相关的fib_node中提取出路由信息。需要进行的额外工作就是选择下一跳,只有当匹配路由项使用多路径情况下才进行这项工作,这可以用两种方法来处理:

● 当搜索key中有egress设备时由fib_semantic_match来处理。

● 当搜索key中没有egress设备时由fib_select_multipath来处理,fib_select_multipath被ip_route_input_slow或ip_route_output_slow调用。

fib_semantic_match函数的逻辑流程见图35-2所示。

35.3.1.1 拒绝路由的标准

当fib_semantic_match函数遍历fib_alias结构时,拒绝以下路由:

● TOS不匹配。注意当路由项没有配置TOS时,可以路由任意TOS值的报文。

● scope比搜索key更窄的路由项。例如,如果路由子系统查找scope为

RT_SCOPE_UNIVERSE的路由,则不能使用scope为RT_SCOPE_LINK的路由项。

第35章 路由查找 Page 35-4

Understanding Linux Network Internals 第七部分——路由

而且,该函数必须检查路由项或所期望的下一跳是否无效(has gone away),此时路由子系统已经设置RTNH_F_DEAD标志来标记该路由项应当被删除。在第32章“辅助程序”小节中,描述了如何对一个路由或路由的下一跳来设置RTNH_F_DEAD标志。

一旦标识出一个合法的fib_alias实例,并假定相关联的fib_info结构可用(即没有设置RTNH_F_DEAD标志),如果搜索key中给出了egress设备,则fib_semantic_match函数就需要遍历所有下一跳的fib_nh实例,来查找与搜索key中egress设备相匹配的实例。但实际中可能没有一个下一跳能被使用,这可能是由两种原因引起的:

● 所有的下一跳都不可用(即它们都设置有RTNH_F_DEAD标志)。

● 搜索key指定的egress设备与任何一个下一跳的配置都不匹配。

如果不支持多路径,则只能有一个下一跳。

当fib_semantic_match遍历fib_alias实例时,对满足本小节前面提到的scope和TOS需求的实例设置FA_S_ACCESSED标志。该标志的设置与fib_alias是否被选中无关。当fib_alias实例被删除时,根据该标志来决定是否应当flush缓存。

第35章 路由查找 Page 35-5

Understanding Linux Network Internals 第七部分——路由

对每一个alias Next No Yes 配置了TOS? End 返回1 (没有路由) KEY-TOS与配置的 TOS匹配吗? Yes No No scope 大于或等于KEY-scope吗? Yes 路由类型 (fib_alias->fa_type)UNICAST LOCAL BROADCASTANYCAST MULTICASTYes fib_info为dead (RTNH_F_DEAD)?No End 对每一个下一跳Next BLACKHOLE UNREACHABLE PROHIBIT THROW 返回 <0 (NEGATIVE SUCCESS)Yes 下一跳为 dead Yes (RTNH_F_DEAD)?NoNo KEY-X:查找key的X字段 Yes 指定了KEY_OUT_DEV吗?NoKEY_OUT_DEV 匹配吗? 12Yes 初始化RESULT 返回0 (成功)图 35-2 fib_semantic_match函数

第35章 路由查找 Page 35-6

Understanding Linux Network Internals 第七部分——路由 35.3.1.2 fib_semantic_match函数的返回值

前面提到,fib_semantic_match函数的返回值可能为以下三个值:

● 1表示没有匹配的路由项。

● 0表示成功。这时候查找结果被保存在入参数res中,这个查找结果包含一个指针,指向匹配的fib_info实例。

● 负值表示管理失败(administrative failure)。

fib_semantic_match返回0值和负值是由查找到的匹配路由(fa)的类型(fa->fa_type)决定的。路由类型的例子有RTN_UNICAST和RTN_LOCAL。fib_semantic_match根据路由类型判断查找是成功还是失败,当失败时返回一个错误码以使内核采取相应的行动。

例如,当匹配路由类型为RTN_UNREACHABLE时,fib_semantic_match返回错误码-EHOSTUNREACH,这将使内核生成一个ICMP不可达消息。当匹配路由类型为

RTN_THROW时,fib_semantic_match返回错误码-EAGAIN,这将使net/ipv4/fib_rules.c文件内策略路由版的fib_lookup函数继续查找下一张路由表。

根据fa->fa_type类型字段得出返回值,返回的错误码放在fib_props数组中,该数组的定义和初始化在net/ipv4/fib_semantics.c文件内(参见第36章“rtable结构”小节)。该数组的每一个元素针对一种路由类型,每个元素包含一个相关的错误码和一个路由作用范围

RT_SCOPE_XXX。以fa->fa_type为索引,就可以从数组fib_props得出该路由类型对应的错误码和路由作用范围(scope)。

表35-1给出了如何来初始化fib_props数组。

表 35-1 fib_props数组的初始化

路由类型

RTN_UNSPEC RTN_UNICAST RTN_LOCAL RTN_BROADCAST RTN_ANYCAST RTN_MULTICAST

错误码 0 0 0 0 0 0

作用范围(scope)

RT_SCOPE_NOWHERE RT_SCOPE_UNIVERSE RT_SCOPE_HOST RT_SCOPE_LINK RT_SCOPE_LINK RT_SCOPE_UNIVERSE

RTN_BLACKHOLE -EINVAL RT_SCOPE_UNIVERSE RTN_UNREACHABLE -EHOSTUNREACH RT_SCOPE_UNIVERSE RTN_PROHIBIT -EACCES RT_SCOPE_UNIVERSE RTN_THROW -EAGAIN RT_SCOPE_UNIVERSE RTN_XRESOLVE -EINVAL RT_SCOPE_NOWHERE

注意前面几个元素的错误码error为0,在这些情况下fib_semantic_match返回成功。其它元素的错误码被路由代码用于正确地处理路由失败。

RTN_NAT -EAGAIN RT_SCOPE_NOWHERE

35.4 fib_lookup函数

在第30章“特殊路由”小节中提到,当不支持策略路由时内核缺省情况下使用两张路由表。对路由表的查找就是对这两张表的查找(调用两次fn_hash_lookup),所以在include/net/ip_fib.h文件内定义的fib_lookup函数就很简单:

第35章 路由查找 Page 35-7

Understanding Linux Network Internals 第七部分——路由

static inline int fib_lookup(const struct flowi *flp, struct fib_result *res) {

if (ip_fib_local_table->tb_lookup(ip_fib_local_table, flp, res) && ip_fib_main_table->tb_lookup(ip_fib_main_table, flp, res)) return -ENETUNREACH; return 0; }

搜索key是flp。函数首先检查ip_fib_local_table路由表,如果失败则检查ip_fib_main_table路由表。如果两张表都没有查找到,fib_loopkup返回-ENETUNREACH(目的网络不可达)。

35.5 为接收和发送设置函数

接收到的报文和本地生成的报文都需要被路由:对接收到的报文,要查找是否应当送交本地还是转发;对本地生成的报文,要查找是否应当送交本地还是发送出去。

在这两种情况下,给定一个需要被路由的报文skb,路由查找结果被保存在skb->dst中。该数据结构的类型为dst_entry,这在第36章“dst_entry结构”小节中有详细描述。该数据结构包含好几个字段:其中两个字段名为input和output,它们是根据路由查找结果来处理报文的函数指针。下一小节将详细描述这两个函数指针的初始化。

接下来,在“输入选路”和“输出选路”小节中,将详细描述ip_route_input_slow和ip_route_output_slow函数。这两个函数是在缓存查找失败后,分别针对ingress报文和egress报文来继续查找路由。由于这两个函数代码都比较长且使用了大量的宏、条件分支(诸如#ifdef)、以及对某些特性的处理(例如多路径),看上去可能令人有一些恐惧,但代码实际上要比看上去的简单。而且,那些不喜欢在源代码中使用goto语句的编程人员可能由于goto语句的大量使用而感到失望。我已经对每个函数都画出流程图,在阅读这些函数之前你可能想先看看这些高级描述(high-level descriptions)。

在本小节的后半部分,我们将看到如何来初始化这两个虚函数(dst->input与dst->output),并了解它们被激活时的更多细节。这两个函数的设置依赖于以下一些要素:

● 报文是正在被发送、接收还是转发

● 目的地址是单播地址还是多播地址

● 在路由查找时是否检测到错误

在表35-2和表35-3中,列出了dst->input和dst->output可以被初始化的程序。

在表35-2和表35-3中,并不是所有的函数都可以组合。图35-3总结了实际中应用的一些组合。这些组合不包括dst_discard_xxx程序,因为它们只在与路由查找无关的特殊情况下才出现(参见“特殊情况”小节)。

表 35-2 用于dst->input的程序

函数

ip_local_deliver ip_forward ip_mr_input ip_error dst_discard_in

描述

将报文送往本地。参见第20章。 转发一个单播报文。参见第20章。 转发一个多播报文。

处理目的地不可达的报文。参见“选路失败”小节。 简单丢弃任何输入报文。

第35章 路由查找 Page 35-8

Understanding Linux Network Internals 第七部分——路由

表 35-3 用于dst->output的程序

函数 ip_output ip_mc_output ip_rt_bug dst_discard_out

描述

对ip_finish_ouput的包装。参见第21章。 处理目的地址为多播的egress报文。 因为代码不应当被调用而打印告警信息。 简单丢弃任何输出报文。

图35-3展示了对ingress流量和egress流量,如何来初始化dst->input和dst->output函数指针。下面我们来一个个看。

图 35-3 dst->input和dst->output初始化

35.5.1 针对Ingress流量的函数指针初始化

我们在第19章看到ingress IP流量是由ip_rcv_finish处理。这个函数根据路由表来判断将报文送往本地还是丢弃。决策是由ip_route_input函数来完成,它首先检查路由缓存,当缓存查找失败时检查路由表(ip_route_input_slow)。ip_route_input_slow函数对dst->input和dst->ouput函数指针可以创建三对主要的组合:

● 如果报文被转发,函数将dst->input初始化为ip_forward,将dst->output初始化为ip_output。所以dst_input将调用ip_forward,而在ip_forward的结尾处间接调用dst_output,即ip_output。这是图35-3中的case (1)。

● 如果报文被送往本地,函数将dst->input初始化为ip_local_deliver。此时不需要初始化dst->output,但它还是被初始化为ip_rt_error*,当被调用时打印出一条错误消息,这样做有助于检测在处理送往本地报文时dst->output是否被错误调用。

● 如果根据路由表得出目的地址不可达,dst->input被初始化为ip_error,这将生成一个ICMP消息,消息类型依赖于路由查找返回的结果。因为ip_error将skb buffer释放,所以不需要初始化dst->output,因为即使在犯错情况下它也不会被调用。

*

译者注:在Linux 2.6.16版本ip_rt_error函数不存在,函数名已经修改为ip_rt_bug。

第35章 路由查找 Page 35-9

Understanding Linux Network Internals 第七部分——路由 35.5.2 针对Engress流量的函数指针初始化

我们在第21章看到在IP层有好几个不同的发送程序。图35-3使用ip_queue_xmit作为一个例子,但是不管该程序在什么情况下被调用,最终都是调用__ip_route_output_key来查找路由,当缓存查找路由失败时调用ip_route_output_slow。ip_route_output_slow函数对dst->input和dst->ouput函数指针可以创建四对主要的组合:

● 如果目的地是远程主机,函数将dst->output初始化为ip_output。这里不需要初始化dst->input。但是,我们可以用一个类似ip_rt_error的函数来初始化它,以便捕获bug,这种做法我们在“针对Ingress流量的函数指针初始化”小节中已经看到。

● 如果目的地是本地系统,函数将dst->output初始化为ip_output,将dst->input初始化为ip_local_deliver。这是一个可以形成循环的有趣组合。当dst_output调用ip_output时,ip_output将报文通过loopback设备送出,这导致ip_rcv和ip_rcv_finish的执行。

ip_rcv_finish检查ingress buffer,即skb,看到在skb->dst中已经存在路由信息,因此调用dst_input,而dst_input又将激活ip_local_deliver。这是图35-3中的case (2)。

● 如果目的地址是本地配置的一个多播IP地址,函数将dst->output初始化为ip_mc_output,接下来由多播代码来处理该报文。此时不初始化dst->input。

● 当内核编译支持多播路由时,在上述相同多播情况下,处理则稍有不同。此时,仍然将dst->output初始化为ip_mc_output,但同时将dst->input初始化为ip_mr_input。

35.5.3 特殊情况

当一个缓存路由dst不能被使用时,dst->output被初始化为dst_discard_out,dst->input被初始化为dst_discard_in。这两个程序都是简单地将传入的报文丢弃。这样初始化的一个例子就是:当一条缓存路由应当被删除(removed),但由于引用计数不为0还不能够被销毁(destroyed)时(参见第33章“删除DST表项”小节)。

当一条新路由表项已经分配出空间,但还没有准备好被使用时,也是用上述两个程序来初始化,因为该路由项此时还没有完全被初始化(参见dst_alloc)。

35.6 输入和输出选路程序的一般结构

我们在第33章“缓存查找”小节中看到当ingress路由和egress路由通过缓存而没有查找到路由时,分别调用ip_route_input_slow和ip_route_output_slow。

这两个程序都很长。为了使这两个程序的代码可读性更好,部分代码被移到两个内联*函数内,它们分别是ip_mkroute_input和ip_mkroute_output。当内核对多路径缓存特性支持或不支持时,这两个内联函数处理也就不同。当内核不支持多路径缓存时,这两个内联函数分别被定义为ip_mkroute_input_def和ip_mkroute_output_def。无论是否支持多路径缓存,路由缓存表项的分配和初始化都是由__mkroute_input和__mkroute_output来实现。当

ip_route_input_slow或ip_route_output_slow触发将新的路由表项插入到缓存内的操作时,都是调用rt_intern_hash函数来执行。

*

注意因为它们是内联程序,所以可以使用goto语句跳转到各自slow程序中定义的label。

第35章 路由查找 Page 35-10

Understanding Linux Network Internals 第七部分——路由

图35-4总结了本小节提到的内容,并且展示了这两个slow程序的流程框架是多么对称。

ip_route_input_slow

...... fib_lookup

ip_mkroute_input

不支持 支持 多路径缓存多路径缓存 ip_mkroute_input_def 对每一个下一跳 fib_select_multipath __mkroute_input

rt_set_nexthop __mkroute_input

rt_intern_hash

rt_set_nexthop

multipath_set_nhinfo rt_intern_hash

ip_route_output_slow

......

fib_lookup fib_select_multipath

ip_mkroute_output

不支持 支持

多路径缓存多路径缓存

ip_mkroute_output_def 对每一个下一跳

__mkroute_output

rt_set_nexthop__mkroute_output

rt_intern_hash rt_set_nexthop

multipath_set_nhinfo rt_intern_hash

图 35-4 ip_route_input_slow和ip_route_output_slow的框架

当支持多路径时存在一些差异,例如fib_select_multipath在ip_mkroute_input_def中被调用,而在ip_mkroute_output_def中没有被调用,其中的原因将在“多路径缓存”小节内解释。

35.7 输入选路

对还没有路由的Ingress IP报文查找路由,检查缓存表是利用ip_route_input,而检查路由表是利用ip_route_input_slow。这个slow程序在net/ipv4/route.c文件内定义,逻辑流程见图35-5(a)和35-5(b)。在这一小节,我们详细描述这个程序的内部细节。

第35章 路由查找 Page 35-11

Understanding Linux Network Internals 第七部分——路由

失败 对源 IP和目的IP地 址严格检查? OK 更新统计信息 (in_martian_source/in_martian_dest) Yes 目 的地址是 广播地址吗? 返回 -EINVAL No 失败 Yes 查找 Ingress 设备上 (fib_lookup) 使能转发吗? No 成功 更新统计信息 (in_slow_tot) 广播路由 远端路由 路由类型 No 本地路由 Ingress 设备上 使能转发吗? 通过 源地址合理性检查 (fib_validate_source) 返回-EINVAL 失败 更新统计信息 (in_martian_source) 返回-EINVAL ip_mkroute_input 2

图35-5a ip_route_input_slow函数

1更新统计信息 (in_no_route) 返回 -EINVAL 第35章 路由查找 Page 35-12

Understanding Linux Network Internals 第七部分——路由

brd_input: No 是三层 IP协议吗? Yes 源地址是 0.x.x.x吗?Yes1No 路由失败 转发 送往本地 2失败 对源地址 做合理性检查 OK 更新统计信息 (in_brd) 3返回-EINVAL local_input:分配一个 新缓存表项 (dst_alloc) OK 失败 返回-ENOBUFF 初始化 新路由缓存表项 初始化搜索key (flowi结构) 将表项添加到 路由缓存内(rt_intern_hash)3图 35-5b ip_route_input_slow函数

这个函数首先对源地址和目的地址进行一些合理性检查,例如源IP地址一定不能为多播地址。我在第31章“Verbose监控”小节中已经列出了大部分的检查,接下来还要进行更多的合理性检查。

第35章 路由查找 Page 35-13

Understanding Linux Network Internals 第七部分——路由

查找路由表是由fib_lookup来完成,在“fib_lookup函数”小节中已经介绍过这个函数。如果fib_lookup没有查找到一条匹配路由,则报文被丢弃。而且,如果接收接口被配置为使能转发,则向源地址回送一条ICMP_UNREACHABLE消息。注意:该ICMP消息不是由ip_route_input_slow来送出的,而是由slow的调用方送出,当调用方看到返回值为RTN_UNREACHABLE时送出该ICMP消息。

当fib_lookup成功查找到匹配路由,ip_route_input_slow需要区分以下三种情况:

● 报文目的地址为一个广播地址

● 报文目的地址为一个本地地址

● 报文目的地址为一个远端地址

在前两种情况下报文被送往本地,而在第三种情况下报文需要被转发。如何将报文送给本地以及如何转发报文的细节可以在“送往本地”和“转发”小节中找到。这里给出无论是送往本地还是转发都需要处理的一些任务:

合理性检查,尤其是源地址

再次检查源地址是否为非法值,并且通过fib_validate_source函数来检查spoofing企图。

创建和初始化一条新缓存表项(局部变量rth)

参见下面的“创建一条缓存表项”小节。

35.7.1 创建一条缓存表项

我在第33章“缓存查找”小节中曾经说过,只要是查找路由表就可以调用ip_route_input(所以当缓存查找没有匹配路由时将调用ip_route_input_slow),而不一定非得是要路由一个ingress报文。所以ip_route_input_slow并不总是需要创建新的缓存表项。当是从IP或L4协议(例如IP over IP)调用ip_route_input_slow函数时,则总是创建一条缓存表项。当前,其它可能调用该函数的地方就是ARP。由ARP生成的路由项只有在代理ARP有效时才被缓存,参见第28章“处理ARPOP_REQUEST报文”小节。

dst_alloc为新缓存表项分配空间,并对缓存项的一些字段初始化,尤其重要的是以下字段:

rth->u.dst.input

rth->u.dst.output

这两个虚函数分别被dst_input和dst_output调用,来完成ingress报文和egress报文的处理,参见第18章中的图18-1。我们也在“为接收和发送设置函数”小节中看到,根据报文是否应当被转发、送往本地或丢弃等情况,来决定如何初始化这两个程序。

rth->fl

这个flowi结构被用做缓存查找的搜索key。重要的是需要注意rth->fl的字段是利用ip_route_input_slow的输入参数来初始化的:这样做可以确保当下一次利用相同参数来查找时,ip_route_input查找缓存就可以得到结果。

rth->rt_spec_dst 第35章 路由查找 Page 35-14

Understanding Linux Network Internals 第七部分——路由

这是首选源地址。参见下面的“首选源地址选择”小节。

35.7.2 首选源地址选择

添加到路由缓存内的路由项是单向的,表示对于朝着被路由报文源IP地址的反方向上的流量,不能使用该路由项来进行路由。但是在一些情况下,接收到报文可能触发一个动作,要求本地主机选择一个源IP地址,以便在向发送方回送报文时使用*。这个地址,即首选(preferred)源IP地址†,必须与路由该ingress报文的路由缓存表项保存在一起。首选源IP地址被保存在rt_spec_dst字段内,下面是使用该地址的两种情况:

ICMP

当一个主机接收到一个ICMP ECHO REQUEST消息时(人们常用的ping命令通常生成这种消息),如果主机没有明确配置为不作出回应,则该主机返回一个ICMP ECHO REPLY消息。对该ingress ICMP ECHO REQUEST消息选择路由,路由项的rt_spec_dst被用作路由ICMP ECHO REPLY消息而进行路由查找的源地址。参见net/ipv4/icmp.c文件内的icmp_reply以及第25章。在net/ipv4/ip_output.c文件内的ip_send_reply函数做法与之类似。

IP选项

一些IP选项要求源地址和目的地址中间的主机将它们接收该报文接口的IP地址写入IP头内,Linux写入的地址是rt_spec_dst。参见第19章中对ip_options_compile的描述。

首选源地址是利用在“辅助程序”小节中提到的fib_validate_source函数来选择的,该选择函数被ip_route_input_slow调用。

ip_route_input_slow根据被路由报文的目的地址来初始化首选源IP地址rt_spec_dst:

报文目的地址为本地地址

这种情况下,报文被送往的本地地址为首选源地址(前面提到的ICMP例子就是这种情况)。

广播报文

广播地址不能被用作egress报文的源地址。所以在这种情况下,

ip_route_input_slow需要借助另外两个程序:inet_select_addr和fib_validate_source(参见“辅助程序”小节)。

当接收报文内没有设置源IP地址(即为全零)时,inet_select_addr在接收该报文的设备上选择scope为RT_SCOPE_LINK的第一个地址。这是因为报文源地址为空时目的地址为受限广播地址(limited broadcast address),而广播地址scope为RT_SCOPE_LINK。其中一个例子为DHCP发现消息。

当源地址不是全零时,由fib_validate_source来处理。

被转发的报文

这种情况下,由fib_validate_source来处理。(前面提到的IP选项例子就是这种情况)。

*

首选源IP地址在用于本地生成的流量时(即向本地发送报文不是由于收到另一个报文而触发或受到影响)可能不同。参见“选择源IP地址”小节。 † RFC 1122称之为“specific目的地”

第35章 路由查找 Page 35-15

Understanding Linux Network Internals 第七部分——路由

用户可以使用下面的类似命令,配置一条具体路由的首选源IP,当有报文匹配该路由项时使用配置的首选源IP:

ip route add 10.0.1.0/24 via 10.0.0.1 src 10.0.3.100

在这个例子中,当向10.0.1.0/24子网的主机发送报文时,内核将使用10.0.3.100作为源IP地址。当然,只有本地配置的地址才能生效:这意味着要使前面的命令生效,10.0.3.100就必须已经在本地某个接口上被配置,不需要一定是在到达10.0.1.0/24子网的设备上配置。(记住在Linux中,地址属于主机而不是属于设备。参见第28章“对多个接口作出响应”小节)。当管理员不想使用根据egress设备缺省选择的源地址时,可以通过上述命令提供一个源地址。

图35-6总结了如何来选择rt_spec_dst。

Yes 目的地址是 本地地址吗? 使用它 No

Yes 目的地址是

广播地址吗?

选择范围为

No RT_SCOPE_LINK的地址

(inet_select_addr) No Yes 用户配置了

首选源地址吗?

使用它 选择范围为

RT_SCOPE_LINK的地址 (inet_select_addr)

图 35-6 rt_spec_dst的选择

35.7.3 送往本地

我们在“针对Ingress流量的函数指针初始化”小节中已经看到,通过正确地初始化dst->input,以下类型的报文被送往本地:

● 报文目的地为本地配置的地址和多播地址

● 报文目的地为广播地址

ip_route_input_slow识别两种广播:

受限广播(Limited broadcasts)

第35章 路由查找 Page 35-16

Understanding Linux Network Internals 第七部分——路由

受限广播地址由全1组成:255.255.255.255*。即使不调用fib_lookup也很容易识别出,受限广播被送给链路上的所有主机,与主机所配置的子网无关。不需要查找路由表。 子网广播

这些广播被送给配置有特定子网的所有主机。如果主机配置为通过同一个设备可以到达不同的子网(参见第30章中的图30-4(c)),则只有正确的子网才能够接收子网广播报文。与受限广播不同,子网广播需要利用fib_lookup函数查找路由表才能够识别。例如地址10.0.1.127可能是10.0.1.0/25中的一个子网广播,而不是10.0.1.0/24中的子网广播。

ip_route_input_slow接受的广播报文只能由IP协议生成,你可能认为这是一项多余的检查,因为调用ip_route_input_slow就是为了路由IP报文。但是,就象在第33章“缓存查找”小节中提到的,ip_route_input的输入缓冲参数(当缓存查找没有匹配项时是ip_route_input_slow的输入参数)不一定是需要被路由的报文。

如果上述所有的一切都处理完毕,一条新的缓存表项rtable被创建,初始化并插入到路由缓存内。

注意对送往本地的报文不需要处理多路径。

35.7.4 转发

如果报文需要被转发,但ingress设备配置为禁止转发,则该报文不能被发送而必须被丢弃。利用IN_DEV_FORWARD检查设备的转发状态。图35-7给出了ip_mkroute_input的内部细节,尤其是给出了当不支持多路径缓存时函数的流程(即在ip_mkroute_input的结尾处调用ip_mkroute_input_def)。在“多路径缓存”小节中,你将看到这两种情况的区别。

如果fib_lookup返回的匹配路由项有多个下一跳,调用fib_select_multipath来选择其中一个。当支持多路径缓存特性时,有多种不同的选择方法。在“多路径对下一跳选择的影响”小节中描述了选择下一跳的算法。

调用fib_validate_source来验证源地址。然后根据我们在第31章“发送ICMP重定向消息”小节中看到的各种因素,内核可能决定向源发送一个ICMP_REDIRECT消息。这时不是直接在ip_route_input_slow函数内发送该ICMP消息,而是当ip_forward看到RTCF_DOREDIRECT标志后,向外发送该ICMP消息。

就象我们在“创建一条缓存表项”小节中看到的,路由查找结果并不总是需要被缓存。 *

已废弃的一种受限广播由全零组成:0.0.0.0。

第35章 路由查找 Page 35-17

Understanding Linux Network Internals 第七部分——路由

CONFIG_IP_ROUTE_MULTIPATH 路由有多个 下一跳吗?No Yes 指定了 出设备吗?YesNo

选择下一跳 (fib_select_multipath) 源地址 sanity检查 (fib_validate_source) 失败 更新统计信息 (in_martian_src) 通过 需要 ICMP 重定向吗?Yes 设置 RTCF_DOREDIRECT NoNo 将路由缓存吗?返回 -ENOBUF Yes失败 分配一个新 缓存表项 (dst_alloc) OK初始化新 路由缓存表项 返回 -ENOBUF 初始化搜索key (flowi结构) 设置下一跳 (rt_set_nexthop) __mkroute_input将表项添加到路由缓存内 图 35-7 ip_mkroute_input函数

35.7.5 选路失败

当一个报文由于主机配置或是由于没有路由匹配而不能被路由时,一条新路由项被插入到缓存内,该路由的dst->input被初始化为ip_error。这意味着所有匹配该路由的ingress报文将第35章 路由查找 Page 35-18

Understanding Linux Network Internals 第七部分——路由

由ip_error来处理。当ip_error被dst_input激活时,将根据报文不能被路由的原因生成相应的ICMP_UNREACHABLE消息,然后丢弃该报文。将错误路由添加到缓存内是有用的,因为这样可以对后续送给同样不正确地址的报文加快错误处理。

ICMP消息在ip_error中被限速。我们已经在第33章“Egress ICMP重定向限速”小节中看到,ICMP_REDIRECT消息也被DST限速。这里讨论的限速与ICMP_REDIRECT消息的限速是相互的,但是是用dst_entry中的相同字段来加强的。这种做法可行的原因是对任意给定的路由,这两种形式的限速是互斥的:一种是针对ICMP_REDIRECT消息,另一种是针对ICMP_UNREACHABLE消息。

下面给出ip_error中如何利用一个简单的token桶算法来实现限速。

每当ip_error被激活生成一条ICMP消息时,更新时戳dst.rate_last。dst.rate_tokens 指定了在限速生效之前,还可以发送多少ICMP消息,也即token的数目或多少预算,而新的

ICMP_UNREACHABLE发送请求将被忽略。每当一个ICMP_UNREACHABLE消息被送出时递减预算,而预算的递减和添加都是在ip_error中实现的。预算不能超过最大数

ip_rt_error_burst,就象从该变量名所看到的,该变量表示一个主机在1秒钟之内可以发送的ICMP消息的最大数(即一个burst值)。这个数值用Hz来表示,以便根据当前时间jiffies和dst.rate_last的差值,来很容易地添加tokens。

当ip_error被激活且至少有一个token可用时,就可以发送一个ICMP_UNREACHABLE消息。ICMP消息的子类型是根据dst.error得出,当fib_lookup无法查找到路由时由ip_route_input_slow来初始化dst.error。

第35章 路由查找 Page 35-19

Understanding Linux Network Internals 第七部分——路由

35.8 输出选路

对本地生成报文进行路由,调用我们在“针对Egress流量的函数指针初始化”小节中介绍的__ip_route_output_key,如果缓存查找没有找到匹配项则调用ip_route_output_slow。ip_route_output_slow函数的结构类似于ip_route_input_slow,在图35-8(a)和35-8(b)中给出了其逻辑流程(A high-level overview)。

(a)Yes失败 KEY对KEY的 指定了 返回 -EINVAL 源IP地址吗?源IP做合理性检查 No通过 No KEY的YesKEY 指定了 dest是多播或Yes 出设备吗?有限广播吗? No 选择与源IP No 失败 Yes相关的出设备 对出设备做 KEY 指定了 sanity检查 出设备吗? 通过 返回 -EINVAL cKEY 的dest是本地多No 播或有限广播吗?No KEY 指定了dest IP吗? YesNo KEY 指定了 Yes 源IP地址吗? YesKEY指定了 No KEY 指定了Yes 源IP地址吗?No源IP吗?KEY 的dest 是IP多播吗? No src IP=127.0.0.1 Yes dst IP=127.0.0.1 Yes Yes KEY用scope LINK从出设备 指定了dest IP吗? 上选择一个源IP地址设置dest IP = src IP (inet_select_addr) 用输入scope从出设备No 上选择一个源IP地址 (inet_select_addr) c fl.oif=loopback res.type=RTN_LOCAL 用scope HOST从出设flags |= RTCF_LOCAL 备上选择一个源IP地址 (inet_select_addr) c

图35-8a ip_route_output_slow函数

第35章 路由查找 Page 35-20

Understanding Linux Network Internals 第七部分——路由

(b) Yes失败OK NoKEYKEY 查找 指定了 指定了 (fib_lookup) 出设备吗?源IP吗? 用scope LINK从出设备NoYes 上选择源IP地址 (inet_select_addr) 返回 -ENETUNREACH Type=RTN_UNICAST CONFIG_ ROUTE_IP_MULTIPATH 本地 远端 (单播) (RTN_LOCAL) 匹配路由类型未指定 Egress设备且Yes 有多个下一跳可用? No 选择下一跳 No KEY 指定了 (fib_select_multipath) 源IP吗? Yes KEY:设置dest IP=src IP Yes 需要选择 缺省网关吗? No 出设备=loopback 选择缺省网关 flags |= RTCF_LOCAL (fib_select_default) No选择Preferred源地址KEY 指定了 (FIB_RES_PREFSRC)源IP吗? Yes (c) 创建路由并插入缓存 (ip_mkroute_output) 图 35-8b ip_route_output_slow函数

在接下来的一些小节中,我们将深入考察为了将报文送往本地或将它发送出去,

ip_route_output_slow需要做哪些事情。将报文送往本地和转发出去都需要执行以下任务,虽然这些任务可能是以不同的方式来执行的:

● 从匹配的路由项选择出使用的egress设备。

● 根据被搜索路由项的scope选择出将要使用的源IP地址。

● 创建及初始化一条新缓存表项,并将它插入到缓存内。 第35章 路由查找 Page 35-21

Understanding Linux Network Internals 第七部分——路由

图35-8用点线分隔出三部分。上面部分,即a部分,利用函数输入参数来填充搜索key的字段。中间部分,即b部分,查找路由表,当需要时要从一个多路径路由项中选择出下一跳或需要选择出缺省网关。下面部分,即c部分,创建新缓存表项。下面部分还根据函数中采用的转发决策结果来初始化dst->input和dst->output,函数主要通过一个局部变量flags来跟踪转发决策结果。

在一些情况下,不需要任何路由查找就可以路由一个报文(即无需调用图中中间部分的fib_lookup)。这主要有以下三种情况,它们在图35-8中都有描述:

当搜索key没有提供egress设备时,报文目的地为一个多播地址或受限的广播地址

这种情况是由于使用诸如vic和vat等多媒体工具而引发的一个问题,函数代码中有一段注释解释了这个问题,参见第26章“特殊情况”小节。

报文目的地为本地多播地址(即224.0.0.X)或受限的广播地址(即255.255.255.255),从一个给定的egress设备发送出去

因为调用方提供了egress设备,因而搜索key中也有了egress设备,而且因为多播或受限的广播是scope为RT_SCOPE_LINK的地址,下一跳就是目的地址本身。所以路由子系统已经拥有路由该报文的所有信息,不再需要查找路由*。参见第30章“路由的基本要素”小节中对多播地址的讨论。

报文目的地为未知地址(0.0.0.0†)

这些报文被送往本地,而不是被发送出去。

35.8.1 搜索Key初始化

下面是ip_route_input_slow如何来初始化搜索key,该搜索key作为fib_lookup的参数用于路由表的查找。该搜索key将与新的被缓存的路由项一起被保存起来,以用于后续的缓存查找。

u32 tos = RT_FL_TOS(oldflp); struct flowi fl = { .nl_u = { .ip4_u = { .daddr = oldflp->fl4_dst, .saddr = oldflp->fl4_src,

.tos = tos & IPTOS_RT_MASK, .scope = ((tos & RTO_ONLINK) ? RT_SCOPE_LINK :

RT_SCOPE_UNIVERSE), #ifdef CONFIG_IP_ROUTE_FWMARK .fwmark = oldflp->fl4_fwmark #endif

} },

.iif = loopback_dev.ifindex, .oif = oldflp->oif };

源IP地址、目的IP地址和防火墙标记是直接从函数的输入参数拷贝而来。但TOS和scope的设置需要一些解释:

TOS

注意L3-to-L2地址映射也是自动进行的,参见第26章“特殊情况”小节。 译者注:原文中这里就缺少注释。

*†

第35章 路由查找 Page 35-22

Understanding Linux Network Internals 第七部分——路由

调用方可以将fl4_tos字段的两个最低位(two least significant bits)用于存储flags,ip_route_output_slow可以使用该flags来确定待搜索路由项的scope。因为TOS字段不

需要占用整个八位,所以这种方法是可行的。参见第33章“Egress查找”小节以及第18章中的图18-3。

在net/ipv4/route.c文件中定义了RF_FL_TOS宏:

#define RF_FL_TOS(oldflp) \\

((u32)(oldflp->fl4_tos & (IPTOS_RT_MASK | RTO_ONLINK)

Scope

当RTO_ONLINK标志被设置时,设置待搜索的路由项的scope为

RT_SCOPE_LINK;否则设置为RT_SCOPE_UNIVERSE。参见第33章“Egress 查找”小节中涉及ARP的例子。

因为调用ip_route_output_slow只是为了路由本地生成的流量,所以搜索key fl中的源设备被初始化为回环设备。我们将看到,如果目的地址也是本地地址,那么egress设备也被初始化为回环设备。

图35-8(a)给出了当搜索key的一些基本字段没有由输入key提供时,如何来初始化这些字段。

35.8.2 选择源IP地址

搜索key中的源IP地址也是发送报文IP头中的源IP地址。所以在ip_route_output_slow函数的一开始,如果搜索key的fl.fl4_src存在,则选择它为源IP地址,后面代码中将rth->rt_src初始化为相同的值。

当搜索key没有提供源IP地址时*,则以目的地址类型作为inet_select_addr的输入参数来选择一个源IP地址†。ip_route_output_slow还专门使用以下的scope作为inet_select_addr的输入参数来处理特殊情况:

● RT_SCOPE_HOST 当报文被送往本地(参见“送往本地”小节)。

● RT_SCOPE_LINK 当报文被送给只在本地链路上有意义的地址,诸如广播、受限广播和本地多播。当fib_lookup查找失败但报文还要送出时也使用该scope,因为搜索key提供了egress设备,因而假定目的地址为链路地址(参见“发送给其它主机”小节)。

当需要被路由的报文不符合上面列出的两种特殊情况时,ip_route_output_slow将

fib_lookup查找到的路由结果res作为输入参数,调用FIB_RES_PREFSRC来选择源IP地址。FIB_RES_PREFSRC使用多种方法来选择首选源IP地址:如果用户为该路由项明确配置了首选源地址则返回它,否则用匹配路由的作用范围(res->scope)为输入参数,调用inet_select_addr来得到首选源IP地址。

如果egress设备已知,则ip_route_output_slow将该设备上配置的地址给予更高的优先级,将该地址作为inet_select_addr的第一个输入参数。但是也可以选择其它设备的地址。

图35-9总结了选择源IP地址的逻辑流程。 *†

参见“首选源地址选择”小节中当搜索key提供了源IP地址时的例子。 该函数在“辅助程序”小节中有介绍。

第35章 路由查找 Page 35-23

Understanding Linux Network Internals 第七部分——路由

用户配置了 首选源地址吗? No Yes 目的地址是 本地地址吗? Yes 使用它 No 用范围RT_SCOPE_HOST 来选择地址 目的地址是 链路地址吗? Yes No 用范围RT_SCOPE_LINK 来选择地址 用范围RT_SCOPE_UNIVERSE 来选择地址 图 35-9 源IP地址选择

35.8.3 送往本地

当fib_lookup看到报文的目的地址是本地配置地址,或者当报文中没有提供目的地址时(即搜索包含了未知地址0.0.0.0),该报文被送往本地。在这种情况下:

● egress设备被设置为回环设备。这表示该报文不会离开本地主机,该报文被发送出后,将重新回到IP输入栈。

● dst->input被初始化为ip_local_deliver,这在第20章“送往本地”小节中描述。正是由于这一操作,当报文重新回到IP输入栈时,ip_rcv_finish调用dst_input,即调用ip_local_deliver函数来处理该报文。

图35-10给出了这两个操作的效果:在内核网络代码中,报文从输出函数被转移到输入函数。

图 35-10 处理本地生成并送往本地的报文

第35章 路由查找 Page 35-24

Understanding Linux Network Internals 第七部分——路由

当搜索key中源IP地址和目的IP地址都没有被设置时,报文被送往本地,源地址和目的地址被设置为缺省回环地址127.0.0.1(INADDR_LOOPBACK),该地址scope为RT_SCOPE_HOST。

35.8.4 发送给其他主机

与送往本地的报文不同,被发送出去的报文需要执行另外两个任务:

● 当查找返回的路由是一条多路径路由项时,需要选出下一跳。这由fib_select_multipath函数来执行。

● 当查找返回的路由是缺省路由时,需要选择使用的缺省网关。这由fib_select_default函数来执行(当res.prefixlen字段为0时表示是缺省路由,这表示“前缀长度”,即与该地址相关的网络掩码长度为0)。

这两个任务在接下来的小节中讨论。

即使fib_lookup查找路由失败,但还是有可能成功地将报文发送出去。当搜索key提供了egress设备,ip_route_output_slow假定通过该egress设备可以直接到达目的地。这时,如果还没有源IP地址,则还需要设置一个scope为RT_SCOPE_LINK的源IP地址,可能的情况下用的是该egress设备上的一个地址。

35.8.5 多路径选择与缺省网关选择之间的交互

从ip_route_output_slow中摘录的下面代码表明:两个关键函数fib_select_multipath和fib_select_default分别用于处理多路径选择和缺省网关选择。res是fib_lookup返回的路由结果。

#ifdef CONFIG_IP_ROUTE_MULTIPATH if (res.fi->fib_nhs > 1 && fl.oif == 0) fib_select_multipath(&fl, &res); else #endif

if (!res.prefixlen && res.type == RTN_UNICAST && !fl.oif) fib_select_default(&fl, &res);

注意当搜索key指定了要使用的egress设备(fl.oif)时,不需要调用这两个程序。这时res已经包含了最终的转发决策。所以,执行fib_lookup以及接下来调用fib_semantic_match函数(参见图35-1)的主要任务是:

● 当匹配路由是多路径路由时选择出下一跳。fib_semantic_match选择匹配egress设备的第一个下一跳路由器(参见“Semantic匹配其它标准”小节)。只有当内核在编译时支持多路径,才执行该条件代码。

● 当匹配路由是缺省路由时选择出缺省路由。fib_semantic_match选择匹配egress设备的第一个缺省路由。fib_semantic_match不区分网络掩码长度不同的路由项,这意味着它并不特殊对待缺省路由项,这种情况由fib_semantic_match透明地处理。

多路径特性在“多路径对下一跳选择的影响”小节中描述,缺省网关选择在“缺省网关选择”小节中描述。

如果不考虑前后关系,本小节开始处给出的代码可能会引起以下两方面的误解:

● 它暗示多路径不能用于缺省路由,因为在代码片断中的逻辑表明fib_select_multipath的执行不会调用接下来的fib_select_default函数。

第35章 路由查找 Page 35-25

Understanding Linux Network Internals 第七部分——路由

但是,多路径特性实际上可以用于缺省路由。IPROUTE2软件包提供的ip命令(需要配置多路径特性)允许对缺省路由项配置多个下一跳。所以对路由项调用fib_select_multipath足以完成路由决策。

net-tools的route工具允许管理员配置多条缺省路由,每一条缺省路由有一个下一跳。这种情况下就不存在多路径(fib_nhs总是为1)。所以fib_select_default足以完成路由决策。

● 它暗示管理员不能在一个egress设备上配置多个下一跳,因为只有在egress设备为空时才调用fib_select_multipath。

但是,配置一条有多个下一跳的多路径路由项,所有这些下一跳使用相同的egress设备是可能的。用egress设备(fl.oif)非空的搜索key来查找路由表是由

fib_semantic_match处理,它简单返回匹配该设备的第一个可用的下一跳。fib_select_multipath不处理这种选择。

35.8.6 缺省网关选择

选择正确的缺省网关是由fib_select_default来执行的,当以下两个条件都满足时它被ip_route_output_slow函数激活:

fib_lookup返回的路由项网络掩码为/0(res.prefixlen为0)

缺省路由匹配任意目的地址,但它最后被检查的原因是由于网络掩码为/0,这是最短的网络掩码。如果配置的路由项没有一项匹配目的地址,那么只有缺省路由才匹配。但是因为所有的缺省路由项都匹配,所以fib_lookup总是返回检查的第一个缺省路由项。这就是为什么调用fib_select_default来在多个可用的缺省路由项中作出最佳选择。

fib_lookup返回的路由项的类型为RTN_UNICAST

本地路由、广播路由和多播路由都不需要网关,对这些路由项使用网关是无意义的。

就象我们在第34章“每个函数有两个定义”小节中提到的,fib_select_default函数有两个版本。下面是不支持策略路由时使用的版本(定义在include/net/ip_fib.h*文件中):

static inline

void fib_select_default(const struct flowi *flp, struct fib_result *res) {

if (FIB_RES_GW(*res) && FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK) ip_fib_main_table->tb_select_default(ip_fib_main_table, flp, res); }

flp是搜索key,res是在ip_route_output_slow中调用fib_lookup而返回的查找结果。

注意当执行tb_select_default所需要的条件不满足时,调用方不会得到任何错误或警告信息,fib_select_default只是简单地将作为输入参数的fib_result实例返回。

tb_select_default被初始化为fn_hash_select_default,它在net/ipv4/fib_hash.c文件中定义,在下一小节对其描述。注意只有当res路由项的下一跳网关的scope为RT_SCOPE_LINK时,fib_select_default才查找ip_fib_main_table表,其中的缘由在第30章“scope的使用”小节中描述。

* 另一个版本参见“策略路由时选择缺省网关”小节。

第35章 路由查找 Page 35-26

Understanding Linux Network Internals 第七部分——路由 35.8.7 fn_hash_select_default函数

fn_hash_select_default接收一个结构为fib_result的res作为输入参数,该参数是前面调用fib_lookup而返回的路由查找结果。fn_hash_select_default使用该结构作为缺省路由搜索的起点。

为了被选中,缺省路由的scope必须与res->scope相同,优先级要小于或等于res->fi->fib_priority,并且下一跳的scope为RT_SCOPE_LINK(即必须直连)。

选择路由项也要考虑下一跳的状态是否可达。fib_detect_death函数将路由项中L3地址已经被解析为L2地址的下一跳(即状态为NUD_REACHABLE)给予更高的优先级。该检查可以确保如果当前所用的缺省路由的下一跳网关不可达而使该路由项不可用,则需要选择新的路由项。

前面选择出的缺省路由被保存在全局变量fn_hash_last_dflt中。

整个程序运行时需要持有fib_hash_lock锁。

35.9 多路径对下一跳选择的影响

在ip_route_input_slow和ip_route_output_slow中,只有在以下情况下才调用fib_select_multipath:

● 内核支持多路径特性(CONFIG_IP_ROUTE_MULTIPATH)。

● 利用fib_lookup查找路由表返回的路由项有多个下一跳(fib_nhs> 1)。

● 搜索key没有提供egress接口。

● 目的地址不是本地地址、广播地址或多播地址。

下面代码给出了如何调用fib_select_multipath来选择下一跳:

#ifdef CONFIG_IP_ROUTE_MULTIPATH if (res.fi->fib_nhs > 1 && fl.oif == 0) fib_select_multipath(&key, &res); #endif

我们已经在第31章“选择下一跳”小节中看到,当一条路由项有多个下一跳时可用时,Linux是如何从中选择一个作为下一跳。我们现在来看该算法的实现。

我们在第34章“路由哈希表的组织”小节中看到,一条路由是由密切相关的数据结构fib_node和fib_info来表示,每个fib_info包含结构类型为fib_nh(每个fib_nh表示路由项中的一个下一跳)的一个数组。

首先,我们需要清楚fib_info和fib_nh数据结构中的哪些字段被用于决定在一组可用的下一跳中,是否必须选择某个下一跳,如果是,那么选择哪一个。

下面是用于保存多路径配置的一些字段:

fib_info->fib_nhs

路由项定义的下一跳的个数。

fib_info->fib_nh

fib_nh结构数组,数组的大小为fib_info->fib_nhs。

第35章 路由查找 Page 35-27

Understanding Linux Network Internals 第七部分——路由

下面字段被用于实现加权随机轮转算法:

fib_info->fib_power

该字段被初始化为fib_info实例的所有下一跳权值(fib_nh->nh_weight)的总和,但不包含由于某些原因而不能使用的下一跳(带有RTNH_F_DEAD标志)。每当调用fib_select_multipath来选择一个下一跳时,fib_power的值递减。当该值递减为零时被重新初始化。

fib_nh->nh_weight

下一跳的权值。当用户没有明确配置时被设置为缺省值1。我们将看到,这个值可以使fib_select_multipath对每个下一跳的选择机会是与该下一跳的权值成比例(相对于fib_info->fib_power而言)。

fib_nh->nh_power

使该下一跳被选中的tokens。这个值是在初始化fib_info->fib_power时,首先被初始化为fib_nh->nh_weight。每当fib_select_multipath选中该下一跳时就递减该值。当这个值递减为零时,不再选中该下一跳,直到nh_power被重新初始化为fib_nh->nh_weight(这是在重新初始化fib_info->fib_power值时进行的)。

现在,我们将看到代码实现的工作机理。

当选择算法开始运行时,所有下一跳的tokens数(nh_power)与其权值相同。我们在前面看到,下一跳的权值缺省为1。change_nexthops循环设置下一跳的nh_power字段,同时将所有下一跳的权值累加到函数的局部变量power上。

spin_lock_bh(&fib_multipath_lock); if (fi->fib_power <= 0) { int power = 0;

change_nexthops(fi) {

if (!(nh->nh_flags&RTNH_F_DEAD)) { power += nh->nh_weight;

nh->nh_power = nh->nh_weight; }

} endfor_nexthops(fi);

fib_info->fib_power被初始化为所有下一跳权值的总和。因为这个值在每次

fib_select_multipath作出决策时都要被递减(见本小节稍后的代码),所以每个下一跳被选中的次数等于该下一跳的权值,直到fib_power递减为0。这也保证了当fib_info->fib_power被递减为0时,每个下一跳已经被选中的次数是与其权值成比例。

fi->fib_power = power; if (power <= 0) {

spin_unlock_bh(&fib_multipath_lock); res->nh_sel = 0; return; } }

fib_select_multipath选中一个下一跳是伪随机的:每当fib_select_multipath被调用时,它生成一个取值范围为零到fib_info->fib_power-1的随机数w,然后遍历所有的下一跳,直到发现某个下一跳的tokens数(fib_nh->nh_power)大于或等于w。注意每次循环时递减w,以使每一次循环都更有可能找到匹配条件的下一跳。

第35章 路由查找 Page 35-28

Understanding Linux Network Internals 第七部分——路由

w = jiffies % fi->fib_power; change_nexthops(fi) {

if (!(nh->nh_flags&RTNH_F_DEAD) && nh->nh_power) { if ((w - = nh->nh_power) <= 0) { nh->nh_power--; fi->fib_power--; res->nh_sel = nhsel;

spin_unlock_bh(&fib_multipath_lock); return; } }

} endfor_nexthops(fi); res->nh_sel = 0;

spin_unlock_bh(&fib_multipath_lock);

35.9.1 多路径缓存

图35-4展示了对ingress流量和egress流量,何时调用前一小节描述的fib_select_multipath程序,以及对多路径缓存的支持是如何影响到由ip_mkroute_input和ip_mkroute_output向路由缓存内加入表项的方式。我们对ingress和egress情况分别进行分析。

Ingress流量

先考虑内核不支持多路径缓存时的情况。当前一小节列出的条件满足时

ip_mkroute_input调用fib_select_multipath,根据前面描述的逻辑选出一个下一跳。

接下来考虑内核支持多路径缓存时的情况。此时不利用fib_select_multipath来选择下一跳。相反,它对多路径路由项的所有下一跳循环,对每个下一跳都向缓存内添加一条表项。对每个路由项还调用multipath_set_nhinfo,该函数在第33章“路由缓存与多路径之间的接口”小节中描述。缓存算法使用该函数来更新本地信息以选择下一跳。例如加权随机算法使用该函数来组织下一跳的数据库(参见第33章“加权随机算法”小节)。

Egress流量

正如图35-4所示,egress情况与ingress情况非常类似。唯一的区别在于即使当内核支持多路径缓存时也调用fib_select_multipath,代码后面激活ip_mkroute_output来覆盖由fib_select_multipath选出的下一跳。

在上面两种情况下,下一跳选择结果res->nh_sel被初始化为多路径路由项的最后一个下一跳。对后续报文而言,在缓存查找时就可以选择出下一跳。参见第33章“多路径缓存”小节。

35.10 策略路由

当内核支持策略路由时,路由查找需要考虑到可能存在多个路由表。下面两个小节描述fib_lookup和fib_select_default的策略路由版本与本章前面介绍的基本版本之间有何区别。

35.10.1策略路由时的fib_lookup

当内核配置了策略路由时,fib_lookup函数包含另外一个步骤:需要根据配置的策略找出使用哪一个路由表。

我们在第32章“主要的数据结构”小节中看到,路由策略是用fib_rule数据结构来定义。所有的fib_rule实例被链接到全局链表fib_rules中。这个链表按照priority字段的递增顺序排列,这使得配置时就确定了路由规则被检查的顺序,从而缩短查找时间。更常用的匹配规则或最重要的规则(由管理员根据具体情况来定义)更靠近链表头部。priority字段占用32比第35章 路由查找 Page 35-29

Understanding Linux Network Internals 第七部分——路由

特,这表示一台主机理论上可以配置多达232个策略。当然,因为策略被存储在一个排序的、扁平的链表内,策略数目过多将显著降低路由查找性能。

即使用户没有配置任何规则,fib_rules也包含三个缺省的fib_rule实例,这三个实例在net/ipv4/fib_rules.c文件中定义,如图35-11所示:

local_rule

这是优先级最高的规则,因而处于链表头部。该规则总是匹配,其目的是迫使第一个查找的是ip_fib_local_table路由表。这是因为目的地址为本地系统的报文只需要查找该表,而不需要其它更进一步的路由决策。

main_rule

这是检查的第二个路由表(除非管理员在中间插入一些用户定义的路由表)。该规则同样总是匹配,它对主路由表ip_fib_main_table进行搜索。

default_rule

这是缺省规则,位于链表的尾部。 local_rule main_ruledefault_rule

fib_rules r_next r_next r_next preference=0 preference=0x7FFE preference=0x7FFF tabel=RT_TABLE_LOCAtabel=RT_TABLE_MAINtabel=RT_TABLE_DEFAUL LT 新规则将插入到这里 图 35-11 缺省规则

图35-12给出了fib_lookup的实现逻辑。它一个接一个地遍历路由策略,直到查找到与路由报

文匹配的策略或到达策略链表尾部还没有找到任何匹配。当查找到匹配策略时,接下来的动作依赖于策略类型(参见第31章“策略路由查找”小节)。特别地,策略动作

RTN_UNREACHABLE,RTN_BLACKHOLE和RTN_PROHIBIT将返回一个错误,fib_lookup的调用方根据该错误值来生成相应的ICMP消息。策略动作RTN_UNICAST将调用tb_lookup来查找,它实际上是调用在“路由表查找:fn_hash_lookup”小节中描述的fn_hash_lookup函数,该函数可以返回多种结果。除了在专门章节中已经描述的错误值以外,还需要注意到:

● 当查找成功时,res->r被初始化为匹配策略。

● 当查找失败时,如果错误类型为-EAGAIN则fib_lookup继续在循环内查找策略。返回该错误值的原因是因为与fn_hash_lookup查找到的匹配路由相关的动作类型为RTN_THROW(参见第30章“路由类型与动作”小节)。

35.10.2策略路由时选择缺省网关

当支持策略路由时选择缺省路由与不支持策略路由时选择的工作机理相同。唯一的区别在于定义在net/ipv4/fib_rules.c 文件中的fib_select_default利用匹配策略(res->r)来得到使用的路由表。

void fib_select_default(const struct flowi *flp, struct fib_result *res) {

if (res->r && res->r->r_action == RTN_UNICAST &&

FIB_RES_GW(*res) && FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK) { struct fib_table *tb; 第35章 路由查找 Page 35-30

Understanding Linux Network Internals 第七部分——路由

if ((tb = fib_get_table(res->r->r_table)) != NULL) tb->tb_select_default(tb, flp, res); } } 捕获锁 fib_rules_lock Next End 对每一个rule 释放锁 fib_rules_lock Yes No源IP/ 目的IP/掩码 匹配吗? 返回 -ENETUNREACH CONFIG_IP_ROUTE_FWMARK YesYes 匹配吗? tag匹配吗? 设备匹配吗?NoTOS防火墙Ingress Yes No No RTN_UNREACHABLE -ENETUNREACH 返回释放锁 fib_rules_lock RTN_BLACKHOLE等 匹配规则对应的 释放锁 返回 -EINVAL fib_rules_lock动作 RTN_PROHIBIT 释放锁 返回 -EACCESS RTN_UNICASfib_rules_lock 根据匹配规则的ID 得到路由表 table->tb_lookup OK (fn_hash_lookup) 路由类型为失败 Yes THROW? No 释放锁 释放锁 fib_rules_lockfib_rules_lock 根据table->lookup返回0 返回错误码 图 35-12 fib_lookup函数的策略路由版本

第35章 路由查找 Page 35-31

Understanding Linux Network Internals 第七部分——路由 35.11 基于源站寻路

我们在第18章中看到IP报文可以基于源站被路由。因为它是由IP协议直接处理而不涉及路由子系统,所以是在本书的IP部分讨论。这里我们感兴趣的是基于源站寻路(source routing)所要涉及的路由查找。

我们用第18章中的图18-1作为一个参考。当一个ingress报文到达ip_rcv_finish时触发第一次路由查找。如果不是基于源站寻路,那么这是唯一的一次路由查找。但是在ip_rcv_finish调用dst_input之前,它检查IP头部是否指定了基于源站寻路,如果是则进行处理。

基于源站寻路在这里是由ip_options_rcv_srr来处理。它从IP头部提取出要使用的下一跳,并调用ip_route_input来进行第二次路由查找。第二次路由查找使用更新的查找结果覆盖原来的skb->dst。参见图35-13中的调用顺序。

当本地生成的流量携带有基于源站寻路的IP选项时,只触发一次路由查找,因为在此次路由查找之前已经选出正确的下一跳(参见ip_queue_xmit)。

ip_rcv_finish

ip_route_input

ip_options_rcv_srr

ip_route_input

dst_input

ip_forward

(检查strict source routing)

图 35-13 对ingress流量基于源站寻路

35.12 策略路由与基于路由表的Classifier

我们在第31章“基于路由表的Classifier”小节中看到,实现网络QoS层的流量控制子系统可以根据路由子系统计算的一个标签(tag)来分类报文。同样是在这一节中,我们看到如何配置realms,以及根据这些配置得出路由标签的逻辑流程。在本小节中,我们将看到如何将realm配置存储在路由表中,以及如何根据路由代码来计算路由标签。因为流量控制话题超出了本书的范围,所以这里我们不讨论如何使用路由标签。

35.12.1存储Realms

内核将策略realm和路由realm分别存储在fib_rule->r_tclassid和fib_nh->nh_tclassid字段内。

fib_rule->r_tclassid

源realm和目的realm都是8比特值(范围为0到255),但在r_tclassid中,它们都占用16比特。当配置源realm时它被保存在高16比特位,当配置目的realm时它被保存在低16比特位,参见图35-14。 16比特 16比特

源realm(可选)目的realm

图 35-14 r_tclassid字段结构

第35章 路由查找 Page 35-32

Understanding Linux Network Internals 第七部分——路由

fib_nh->nh_tclassid

一般情况下,只使用目的realm来计算路由标签,根据目的地址来选择匹配路由。但是,就象我们在第31章“计算路由标签”小节中看到的,内核有时侯需要反向路径查找。当这种情况发生时,路由项的目的realm是从反向路由的源realm得出的。nh_tclassid是一个32比特变量。

35.12.2辅助程序

在查看如何初始化dst.tclassid之前,我们首先来看为了完成这一任务而用到的一些辅助程序:

fib_rules_tclass

它用于从一个fib_rule数据结构内提取r_tclassid字段。因为fib_lookup返回的结果内包含一个指针指向匹配的fib_rule实例,所以在查找之后使用fib_rules_tclass来提取匹配规则。注意只有当内核支持策略路由时才调用该函数,此时fib_rule结构才有意义。

fib_combine_itag

图35-15给出了这个函数的逻辑流程,它是在进行反向路径查找时来辅助查找realms。

当内核没有使能策略路由时,它简单地将源路由realm和目的路由realm交换。

当内核使能策略路由时,这个函数将策略源realm(图35-15中的S2)作为目的realm。而且,如果目的路由realm(D1)存在则将D1作为源realm,否则将目的策略realm(D2)作为源realm。

结果是用输入参数itag来返回,当调用方激活rt_set_nexthop时使用该返回结果(参见“计算路由标签”小节)。

在反向路径查找之后,这个函数被fib_validate_source调用。fib_validate_source的输入参数有源IP地址和目的IP地址,fib_validate_source将它们交换,然后调用fib_lookup进行反向路径查找。因而,fib_lookup返回的结果中源realm和目的realm也已经被交换。因为这两个realm字段都占用16比特空间,而且fib_lookup返回的两个realm已经被交换,所以fib_combine_itag中使用16比特移位来进行调整。

set_class_tag

给定一条路由(因而也就可以得到相关的dst_entry.tclassid)和一个已经被调用方初始化了的标签,set_class_tag使用第二个参数来填充dst_entry.tclassid中还没有被初始化的realm。

源realm 目的realm

nh_tclassidS1 D1

r_tclassid S2D2

没有使能 使能

策略路由 策略路由

如果没有设置D1如果设置D1

D1 0 D2S2D1S2

图 35-15 fib_combine_itag函数

第35章 路由查找 Page 35-33

Understanding Linux Network Internals 第七部分——路由 35.12.3计算路由标签

路由标签必须由我们在本章前面讨论的ip_route_input_slow和ip_route_output_slow函数来计算。使用的逻辑流程在第31章“计算路由标签”小节中描述。

计算一个路由标签所需要的信息是要路由的skb报文和路由查找结果skb->dst。路由标签被保存在skb->dst.tclassid中。一旦ip_route_input_slow和ip_route_output_slow成功找到转发信息,它们就创建并初始化一条新的路由缓存表项,包括对路由标签的初始化,然后将该表项添加到缓存内。缓存表项的部分初始化是用rt_set_nexthop函数来完成的,其中的部分代码用于处理路由标签dst_entry.tclassid。图35-4展示了rt_set_nexthop被调用时的内部情况。

static void rt_set_nexthop(struct rtable *rt, struct fib_result *res, u32 itag) {

struct fib_info *fi = res->fi;

if (fi) { ... ... ...

#ifdef CONFIG_NET_CLS_ROUTE

rt->u.dst.tclassid = FIB_RES_NH(*res).nh_tclassid; #endif }

... ... ...

#ifdef CONFIG_NET_CLS_ROUTE

#ifdef CONFIG_IP_MULTIPLE_TABLES set_class_tag(rt, fib_rules_tclass(res)); #endif

set_class_tag(rt, itag); #endif ... ... ... }

前面的代码片断表现了当内核支持基于路由表的classifier时,首先用目的路由的realm来初始化tclassid(否则不需要做)。注意根据内核是否支持策略路由,调用set_class_tag时所使用的输入参数不同:

支持策略路由时

根据策略realms来填充dst.tclassid中还没有被初始化的字段。

不支持策略路由时

使用调用方先前计算的itag输入参数来填充dst.tclassid中还没有被初始化的字段:

● ip_route_input_slow(通过调用__mkroute_input)传递的itag值是用fib_combine_itag计算的。

● ip_route_output_slow(通过调用__mkroute_output)传递的itag值为0,因为它路由的报文是本地生成的,因而内核不用做任何反向查找来填充还没有被初始化的realms。

第35章 路由查找 Page 35-34

Understanding Linux Network Internals 第七部分——路由

第36章 相关主题

在前面的章节中,我们看到各种路由特性是如何工作,它们之间如何相互作用以及它们如何与其它内核子系统相互作用。在本章,我们描述这些子系统是如何与配置路由的用户空间命令相互作用,来结束本书对路由部分的讨论。我不会详细描述这些命令,因为管理话题超出了本书讨论的范围。我们还将看到/proc目录下可用于调整路由的各种文件。在本章最后,我们将详细描述在第32章中已经介绍过的数据结构。

36.1 用户空间配置工具

路由的配置可以使用net-tools包和IPROUTE2包,它们分别使用ioctl和Netlink接口。下面的小节将对这两个包详细描述,但主要关心的还是IPROUTE2包中的ip命令,这是在Linux上更新、功能更强大的路由配置方法。

如果你了解这两组工具的局限性并正确使用,那么它们可以共存而不会出现问题。net-tools包不能够配置任何高级路由特性,例如多路径路由与策略路由。在net-tools包各个命令的显示结果中也看不到这些高级路由特性。但IPROUTE2包提供的路由配置向后兼容net-tools包中的命令。

图36-1汇总了我们将在下面小节内所看到的内容。这张图展示了通过这两个内核接口来操作路由表的主要函数,ioctl命令是由net-tools使用。(IPROUTE2还允许你配置诸如策略规则等其它对象,但图中为了简洁而没有画出)。

IPROUTE2 net-tools ioctl Netlink SIOCDELRTRTM_DELROUTERTM_NEWROUTE SIOCADDRT ip_rt_ioctl inet_rtm_delrouteinet_rtm_newroute 捕获锁(rtnl_lock) 解析配置数据结构解析配置数据结构 (inetcheckattr)(inetcheckattr) 将配置数据结构 得到表tb 得到/创建表tb 转换为Netlink模式 (fibgettable)(fibnewtable)(fib_convert_rtentry) SIOCDELRT SIOCADDLR删除路由 插入路由 命令 tb->tb_delete tb->tb_insert (fnhashdelete)(fnhashinsert) 得到表得到/创建表tb tb (fibget(fibnewtable) table) 删除路由 插入路由 tb->tb_delete tb->tb_insert delete) (fnhash(fnhashinsert) 释放锁(rtnl_unlock) 图 36-1 基于ioctl和基于Netlink的路由表操作

第36章 相关主题 Page 36-1

Understanding Linux Network Internals 第七部分——路由

需要注意以下要点:

● 这两种工具添加路由和删除路由最终都是调用相同的程序:fn_hash_insert和fn_hash_delete,我们在第34章已经看到这两个程序。

● 由于前一要点,在调用公共的fn_hash_xxx程序之前,必须将从这两个用户空间工具接收到的输入信息保存到相同的数据结构内。因为这两个工具与内核之间是通过不同的消息类型来通信,而且因为Netlink是新型的更为首选的接口,所以利用

fib_convert_rtentry将ioctl命令的输入信息转换为Netlink格式。这个转换函数会将用户输入的串转换为本章后面描述的内核数据结构,由于考虑了解析该请求的操作,所以不需要再调用解析程序inet_check_attr(它在inet_rtm_xxx程序中被悄悄调用)。

● 为了使路由配置的变更依次操作而使用了一个锁。图36-1没有显示与这两个

net_rtm_xxx程序相关的锁,因为在它们被调用之前,路由Netlink套接字代码已经捕获到该锁。(详见rtnetlink_rcv)。

36.1.1 利用IPROUTE2包配置路由

IPROUTE2包提供了各种不同的工具。我们在本章感兴趣的是ip命令,尤其是它的两个子命令:ip route与ip rule,这两个命令分别被用于配置路由和策略路由规则。

利用IPROUTE2包,不仅可以添加路由或删除路由,还可以修改路由、后向追加路由(append)和前向追加路由(prepend)。这些操作并没有提供更多的特性,而只是在处理大型路由表时使管理操作更为容易。

36.1.1.1 IPROUTE2用户命令与内核函数之间的对应关系

表36-1和表36-2给出了对主要的ip route和ip rule命令,由IPROUTE2设置的操作码和标志。了解这些就可以更容易地阅读图36-1中的程序和表中“内核处理钩子”列中给出的处理钩子函数。“CLI关键字”列包含的是在命令行中触发相应操作的关键字。

表36-1中有一个关键字需要解释:flush。ip route flush命令可以使管理员来定义删除什么样的路由。通常的操作是flush所有类型的路由,但这个命令可以使管理员通过诸如设备和目的网络等标准,来被flush的路由项。

内核没有用于flush操作的处理函数。而在IPROUTE2中,首先发布一个list命令得到路由表的一份拷贝,将那些不符合flush条件的路由项过滤掉,然后对剩余的每一条路由项发布一个RTM_DELROUTE命令。这种做法对较少的配置工作很好,但处理大型路由表时将需要花费大量的处理。如果能够将flush条件发送给内核,由内核来处理过滤,该操作将能够更容易,更快速。

表 36-1 在IPROUTE2包的iproute.c文件中,由do_iproute设置的参数

Flags CLI关键字 操作 内核处理钩子

add RTM_NEWROUTE NLM_F_EXCL NLM_F_CREATE inet_rtm_newroute change RTM_NEWROUTE NLM_F_REPLACE inet_rtm_newroute

NLM_F_CREATE

replace RTM_NEWROUTE inet_rtm_newroute

NLM_F_REPLACE

prepend RTM_NEWROUTE NLM_F_CREATE inet_rtm_newroute

NLM_F_CREATE

append RTM_NEWROUTE inet_rtm_newroute

NLM_F_APPEND

test RTM_NEWROUTE NLM_F_EXCL inet_rtm_newroute delete RTM_DELROUTE None inet_rtm_delroute 第36章 相关主题 Page 36-2

Understanding Linux Network Internals 第七部分——路由

list/lst/show RTM_GETROUTE None inet_dump_fib get RTM_GETROUTE NLM_F_REQUEST inet_rtm_getroute flush RTM_GETROUTE None None

表 36-2 在IPROUTE2包的iprule.c文件中, 由do_iprule设置的参数

Flag CLI关键字 操作 内核处理钩子

add RTM_NEWRULE None inet_rtm_newrule delete RTM_DELRULE None inet_rtm_delrule list/lst/show RTM_GETRULE None inet_dump_rules

从“CLI关键字”列中可以看出,一些内核处理钩子需要处理多个用户命令类型,内核通过操作和flags参数的组合来区分不同的命令。

如图36-1所示,处理路由的内核处理钩子是inet_rtm_newroute和inet_rtm_delroute,它们在下一小节中描述。那么,在表36-1中的这两个处理钩子是如何实现的呢,我把它留作一个练习。(可以参考第35章“策略路由”小节)。

对研究IPROUTE2工具代码有好奇心的读者来说,图36-2给出了在IPROUTE2包中,对各种ip route和ip rule命令请求进行解析,并将请求发送给内核过程中相关的文件和程序。例如,如果你输入ip route add ...等命令,在ip.c中的程序main将调用iproute.c文件中定义的do_iproute来处理该命令。因为该操作为add,所以do_iproute将利用iproute_modify来处理该命令。

main

route rule (ip.c)

do_iproute do_iprule

(iproute.c) (iprule.c)

add

list getaddlist change

flush delete replace prepend append test delete iprule_list iproute_modifiproute_getiprule_modify

iproute_list_or_flush

图 36-2 IPROUTE2包用于路由的文件和函数

36.1.1.2 inet_rtm_newroute和inet_rtm_delroute函数

当内核收到用户利用IPROUTE2工具发送的路由请求时,最终调用这两个程序,它们分别处理添加路由和删除路由,如图36-1和表36-1所示。

这两个程序都调用inet_check_attr来填充一个kern_rta结构,该结构存储对用户命令解析后的结果。kern_rta的所有字段都是指针:它们直接指向从用户空间接收到的数据结构的字段。空指针表示相关的字段没有配置。

第36章 相关主题 Page 36-3

Understanding Linux Network Internals 第七部分——路由

我们在本小节考察inet_rtm_newroute。inet_rtm_delroute的操作与此相反。

int inet_rtm_newroute(struct sk_buff *skb, struct nlmsghdr* nlh, void *arg) {

struct fib_table * tb; struct rtattr **rta = arg;

struct rtmsg *r = NLMSG_DATA(nlh);

if (inet_check_attr(r, rta)) return -EINVAL;

tb = fib_new_table(r->rtm_table);

if (tb)

return tb->tb_insert(tb, r, (struct kern_rta*)rta, nlh, &NETLINK_CB(skb)); return -ENOBUFS; }

函数首先利用inet_check_attr解析输入消息nlh,并将结果保存到rta中。当添加一条路由时,用户可以指定将该路由项添加到哪一个路由表中。多路由表的概念在第31章“策略路由背后的概念”小节中有更详细的描述。如果指定的路由表还不存在,则利用fib_new_table创建并初始化该表。现在有了该路由表的引用,就调用其虚函数tb_insert来执行插入操作。我们在第34章“路由表项的添加与删除”小节中看到,tb_insert调用的是fh_hash_insert,其内部流程在第34章“添加一条路由表项”小节中描述。

36.1.2 利用net-tools包配置路由

在net-tools包中的route命令在大多数Unix系统中都可见到,这是最常见的配置路由和dump路由表及路由缓存内容的方法。

route add和route del命令分别向内核发送ioctl命令SIOCADDRT和SIOCDELRT,以添加路由和删除路由。但dump路由表和路由缓存是用另一种方法:route简单dump出/proc/net/route文件和/proc/net/rt_cache文件的内容。*

处理这两个ioctl命令的内核处理钩子是ip_rt_ioctl,它在net/ipv4/fib_frontend.c文件中定义。图36-1给出了其内部的部分流程。

只有权限为网络管理员的用户(CAP_NET_ADMIN)才可以使用route命令。调用

capable来加强这一规则。† 接下来,因为携带着路由删除或添加信息的数据结构属于用户空间,所以需要利用copy_from_user将它拷贝到内核空间的一个地址处。

... ... ...

if (!capable(CAP_NET_ADMIN)) return -EPERM;

if (copy_from_user(&r, arg, sizeof(struct rtentry))) return -EFAULT; ... ... ...

36.1.3 路由变化通知

我们在第3章中看到Netlink定义了多播组,当出现特定类型事件时送出通知,用户程序可以注册到这些组来接收事件通知。在这些多播组内有一个为RTMGRP_IPV4_ROUTE组,用于在IPv4路由表发生变化时向外通知。将这些变化发送给多播组RTGRM_IPV4_ROUTE是利用rtmsg_fib程序。

*†

参见net-tools包中的文件lib/inet_gr.c。

关于用户权限和处理能力的更多细节,可参见Understanding the Linux Kernel (O'Reilly)一书。

第36章 相关主题 Page 36-4

Understanding Linux Network Internals 第七部分——路由

当其它守护进程或用户手工配置将路由项添加或删除时,对这些事件感兴趣的监听方,例如各路由守护进程需要得到通知。用户也可以使用IPROUTE2的ip monitor route命令来测试该特性。图36-3给出了一个例子:当一个终端上的操作使路由表发生变化时,事件通知被输出到ip monitor route命令正在执行的另一个终端上。* 终端和内核通过一个Netlink套接字通信。

监听

RTMGRP_IPV4_ROUTE

组的通知

终端1 终端2

# ip monitor route # ip route add 10.0.0.0/24 via 192.168.1.2

10.0.0.0/24 via 192.168.1.2 dev eth1

# ip route del 10.0.0.0/24 via 192.168.1.2

Deleted 10.0.0.0/24 via 192.168.1.2 dev eth1

图 36-3 ip monitor route命令使用举例

36.1.4 内核插入路由项:fib_magic函数

我们在图36-1中看到可以用Netlink套接字在用户空间和内核之间交换数据。但在某些情况下内核中的各个部分也使用Netlink消息相互通信。这样,利用通常对用户生成事件作出响应的相同代码,就能够很容易对内核生成事件作出响应。

例如,我们在第32章“添加一个IP地址”小节中看到,当在一个接口上配置一个新的IP地址时,可以生成一组路由表项。添加这些路由项的一个简单方法是模拟接收到来自用户空间请求插入新路由项的命令。这是利用fib_magic程序来实现,该程序将创建一条Netlink消息,该消息与通过route add或ip route add命令而添加路由所生成的消息相同。

fib_add_ifaddr和fib_del_ifaddr是调用fib_magic的两个很好的例子,关于这两个函数的更多细节可参见第32章“IP配置改变”小节。

36.2 统计

路由代码对各种不同的路由特性都进行统计,例如路由查找和垃圾回收。路由统计信息是针对每个处理器进行统计。在第32章“路由子系统初始化”小节中描述的ip_rt_init函数,为每个CPU分配一个rt_cache_stat数据结构的拷贝,每个CPU各自维护本CPU上的路由统计。rt_cache_stat字段是利用RT_CACHE_STAT_INC宏来递增,这个宏透明地更新相应CPU的计数值。“rt_cache_stat结构”小节中详细描述了rt_cache_stat中的各个字段。

这些统计内容可以通过dump出/proc/net/stat/rt_cache文件的内容来读取(参见“/proc/net和/proc/net/stat目录”小节)。但得到的输出信息没有被格式化,因而不容易阅读。为了得到格式化输出信息,可以使用IPROUTE2包中的lnstat工具。

36.3 通过/proc文件系统调整路由

IPv4路由子系统使用/proc文件系统将一些内部数据结构以只读方式输出(例如路由缓存),而将其它数据结构以读写方式输出以便调整路由。

*

当前并不是所有的路由变化都生成通知。例如当一个设备变为Down时而删除相关的IPv4路由就不会通知IPv4协议。但这种行为可能会发生变化,例如IPv6中同样的操作已经发送通知。

第36章 相关主题 Page 36-5

Understanding Linux Network Internals 第七部分——路由

图36-4给出了这些文件的位置以及注册这些文件的程序。不是由创建程序注册的文件则是在系统启动时在sysctl_init函数中静态定义。

/proc/sys/net/ipv4/

/proc/sys/net/ipv4/conf

/proc/sys/net/ipv4/route

这些目录下的文件输出内部数据结构用于调整路由,所以这些目录下的文件是可写的。后面的章节列出这些目录下的文件,相关的内核变量、以及这些变量的缺省值。*

/proc/net/

/proc/net/stat

这些目录下的文件不是用于调整路由,而是执行内核程序以得到某种信息。参见“/proc/net目录 和/proc/net/stat目录”小节。

图 36-4 IPv4路由子系统使用的/proc文件

注意由内核设置的缺省值可能与Linux系统启动时得到的缺省值不同,原因在于每一个Linux分销商可以在Linux系统启动时,利用初始化文件和脚本来随意改变每个sysctl变量的缺省值。例如,可以参见/etc/sysctl.conf文件。而且,不同版本的内核使用的缺省值可能不同。

* 第36章 相关主题 Page 36-6

Understanding Linux Network Internals 第七部分——路由 36.3.1 /proc/sys/net/ipv4目录

这个目录下有许多文件,但被路由子系统使用的文件为:

ip_forward

包含了一个用于全局使能和禁止IP转发的布尔类型flag,这个全局的IP转发flag值可以被每个设备上的IP 转发flag值所覆盖(参见“使能和禁止转发”小节)。

icmp_echo_ignore_broadcasts

一个ICMP调整参数。在第30章“定向广播”小节中介绍过该参数,它被路由代码用于确定如何来处理定向广播。广播过滤只能在这里使能和禁止,并且只能够是全局生效(不是对每个设备都有各自的广播过滤参数)。

参见表36-3对这些文件的总结。

表 36-3 /proc/sys/net/ipv4/目录下用于调整路由子系统的文件

内核变量名

ipv4_devconf.forwarding*

sysctl_icmp_echo_ignore_broadcasts

文件名 ip_forward

icmp_echo_ignore_broadcasts

缺省值 0 0

36.3.2 /proc/sys/net/ipv4/route目录

IPv4路由子系统使用这个目录下的所有文件。下面按照功能分组来描述这些文件:

error_burst

error_cost

用于实现对ICMP_UNREACHABLE消息的限速。参见第35章“选路失败”小节。

max_size

gc_thresh

gc_min_interval

gc_timeout

gc_elasticity

gc_interval

用于路由缓存垃圾回收算法,参见第33章中的描述。 flush

min_delay

max_delay

用于控制路由缓存的flush。

参见第19章对ipv4_devconf数据结构的描述。

*

第36章 相关主题 Page 36-7

Understanding Linux Network Internals 第七部分——路由

与该目录下的其它文件不同,只有flush是可写的*并触发一个动作;它不是一个简单的调整参数。当用户将n写入该文件时,函数 ipv4_sysctl_rtcache_flush被激活,将在n秒后对路由表缓存进行一次flush。当写入一个负值到flush中时,内核调度在缺省延迟min_delay之后进行一次flush。max_delay是在用户调度进行flush和内核实际flush缓存之间经过的最长时间。参见第33章“Flush路由缓存”小节。

min_adv_mss

这个值与TCP最大报文段长度(MSS)参数相关。每个路由有一个相关的MSS值。当一个dst_entry的下一跳(利用rt_set_nexthop)被初始化时,在调用rt_intern_hash将它添加到路由表缓存之前,MSS被初始化为出设备的MTU或min_adv_mss,哪一个值大就选取哪一个,参见tcp_advertise_mss中的注释和rt_set_nexthop中对该变量的初始化。

min_pmtu

mtu_expires

当与一条路由缓存表项相关的PMTU变化时,路由缓存被调度在mtu_expires秒后过期。参见第30章“使路由缓存表项过期的事件举例”小节。

min_pmtu是路径MTU发现协议可以为路由项设置的最小PMTU值。

redirect_load

redirect_number

redirect_silence

用于实现对ICMP_REDIRECT消息的限速。参见第33章“Egress ICMP重定向消息限速”小节。

secret_interval

路由缓存每隔secret_interval/HZ秒被定期flush。参见第33章“Flush路由缓存”小节。

表36-4列出了内核变量和缺省值。†

表 36-4 /proc/sys/net/ipv4/route目录下用于调整路由子系统的文件

内核变量名 ip_rt_error_burst ip_rt_error_cost flush_delay

ip_rt_gc_elasticity ip_rt_gc_interval

ip_rt_gc_min_interval ipv4_dst_ops.gc_thresh

文件名 error_burst error_cost flush

gc_elasticity gc_interval

gc_min_interval_msb gc_thresh

缺省值 5 * HZ HZ N/Aa 8

60 * HZ HZ / 2

依赖于内存大小c

RT_GC_TIMEOUT (300 * HZ) 2 * HZ

ip_rt_gc_timeout gc_timeout ip_rt_min_delay min_delay *†

这个文件实际上有读取权限,但如果试图读取文件内容,内核则发出抱怨。

大多数表示周期性时间的参数在配置时是以秒为单位,但存储时以jiffies(秒数 * HZ)为单位。当通过dump文件内容来读取这些值时,你得到的值可能是以秒为单位,也可能以jiffies为单位,这依赖于内核是用什么程序来dump这些值(例如proc_handler程序)。例如,proc_dointvec输出的就是内核中的值(假设为正值),而proc_dointvec_jiffies把以jiffies为单位的值(即ticks)转换为秒来显示。

第36章 相关主题 Page 36-8

Understanding Linux Network Internals 第七部分——路由

ip_rt_max_delay max_delay ip_rt_max_size max_size ip_rt_min_advmss min_adv_mss ip_rt_min_pmtu min_pmtu ip_rt_mtu_expires mtu_expires ip_rt_redirect_load redirect_load ip_rt_redirect_number redirect_number ip_rt_redirect_silence redirect_silence ip_rt_secret_interval secret_interval a

见本小节前面对flush的描述。

b

10 * HZ

Depends on RAM 256

512+20+20 d 10 * 60 * HZ HZ / 50 9

((HZ/50)<<(9+1)) 10 * 60 * HZ

与该内核变量相关联还有另外一个文件gc_min_interval,但这个文件已经废弃并且将被删除。

c

启动时根据哈希表容量来初始化,而哈希表容量依赖于系统所拥有的RAM数量。

d

依据RFC 793和RFC 1112,TCP使用的MSS缺省值是512;后面的20+20是IP报头大小和TCP报头大小,此时IP报头和TCP报头都没有选项。

36.3.3 /proc/sys/net/ipv4/conf目录

这个目录下包含的文件可以用于调整IPv4,IPsec和ARP协议,以及控制每一个设备的路由。与协议相关的参数在相关章节中已经涉及到,所以在本章中,我们只讨论用于调整路由的参数。

在/proc/sys/net/ipv4/conf目录下,每个注册的网络设备(包括loopback设备)都有各自的子目录,在这些子目录下包含有每个调整参数对应的文件。这使用户可以对每个设备来配置各种协议的路由参数。每个目录下包含的文件相同。所有这些参数在内核中被组合到一个类型为ipv4_devconf的数据结构内,该结构在include/linux/inetdevice.c文件内定义,并在表36-5中列出。各个参数的缺省值就是/proc/sys/net/ipv4/conf/default目录下对应文件中的值。

表 36-5 /proc/sys/net/ipv4/conf目录下用于调整路由子系统的文件

内核变量名(ipv4_devconf的字段) accept_redirects accept_source_route forwarding mc_forwarding rp_filter

secure_redirects shared_media send_redirects log_martians tag(未使用)

文件名

accept_redirects accept_source_route forwarding mc_forwarding rp_filter

secure_redirects shared_media send_redirects log_martians tag

缺省值 1 1 0 0 0 1 1 1 1 0

36.3.3.1 特殊子目录

在/proc/sys/net/ipv4/conf目录下,除了每个设备对应一个目录以外,还有两个特殊子目录:

default

用户没有明确配置的所有参数是用这个目录下的缺省值来初始化,内核将这些缺省值维护在另一个ipv4_devconf实例:ipv4_devconf_dflt内(参见表36-5)。 all 第36章 相关主题 Page 36-9

Understanding Linux Network Internals 第七部分——路由

这个目录被用于全局配置(即用户在这里所配置的值应用于所有设备)。内核也是将这些值维护在另一个数据结构ipv4_devconf内,该变量名称与其结构类型相同。

default目录和每个设备对应的目录是在调用devinet_sysctl_register时创建的。all目录是静态定义的(参见net/ipv4/devinet.c文件中devinet_sysctl_table的定义)。

当系统启动时,路由代码调用devinet_init来初始化路由,devinet_init又调用

devinet_sysctl_register来注册default目录(参见第32章“路由子系统初始化”小节)。因为这个函数被inetdev_init调用,所以当每个设备初始化时都要调用一次该函数(当设备上配置第一个IPv4地址时)。

36.3.3.2 特殊子目录的使用

将每个设备上的配置值和全局配置值进行组合时,以及将all目录下变量的改变广播给所有设备时,不同的特性呈现出不同的行为。例如:

● 对某一些字段,将设备上配置的值和全局配置值做与(AND)操作。这时,只有当全局配置和设备配置的值都为使能时,该特性才使能。

● 对某一些字段,将设备上配置的值和全局配置值做或(OR)操作。这时,只要一个文件中的值为使能则该特性就使能。

● 对某一些字段,不考虑全局值。

那么,要使某一个特性使能,是如何来利用这些文件呢?

对每个参数有一个对应的宏IN_DEV_XXX,这个宏在include/linux/inetdevice.h文件内定义,对一个给定的设备使用这个宏得出当前的操作状态。这些宏将设备上的IPv4配置块作为输入参数,这里指的设备是结构类型为in_device的实例。可以查看这些宏得出每个参数是用什么标准(相与、相或、或什么操作都不做)来组合设备配置和全局配置。这里针对每一种情况分别给出一个例子:

#define IN_DEV_RPFILTER(in_dev) \\

(ipv4_devconf.rp_filter && (in_dev)->cnf.rp_filter)

#define IN_DEV_PROXY_ARP(in_dev) \\

(ipv4_devconf.proxy_arp || (in_dev)->cnf.proxy_arp)

#define IN_DEV_MEDIUM_ID(in_dev) ((in_dev)->cnf.medium_id)

前面例子中使用的逻辑并不是IN_DEV_XXX宏实现的唯一方式。例如

IN_DEV_RX_REDIRECTS宏就更为复杂,它是对多个参数的封装,而不只是对两个值做相与或相或的操作。

还有一个情况需要考虑。对一些参数,all目录下的文件变化立即通知所有设备对应的子目录(不是由于调用IN_DEV_XXX宏引起)。这种情况下,相关的IN_DEV_XXX宏不需要检查全局配置值。参见“使能和禁止转发”小节中的例子。

36.3.3.3 文件描述

这里简要描述在表36-5中列出的文件:

accept_redirects

send_redirects

第36章 相关主题 Page 36-10

Understanding Linux Network Internals 第七部分——路由

在第31章描述了ICMP重定向消息,该消息由路由器发送给主机,通知主机路由不是最优。accept_redirects是一个布尔类型flag,可用于在一个接口上使能或禁止ICMP重定向处理。* send_redirects被用于反方向:当值为真且检测到出现非最优路由时,允许系统生成ICMP重定向消息。

accept_source_route

可以利用这个flag来使能和禁止IP源站选路(Source Routing)选项。当为禁止时,ip_rcv_finish丢弃所有携带有该选项的IP包。IP选项在第18章中讨论。

forwarding

mc_forwarding

这些是布尔类型flag,分别用于使能和禁止单播与多播的转发。当使用mc_forwarding时,内核编译时必须使用必要的多播选项。

rp_filter

当这个flag为真时,如果某个ingress报文的源IP地址通过非对称路由(asymmetric route)可达(依据本地主机的路由表),则丢弃该报文。参见第31章“反向路径过滤”小节。

secure_redirects

shared media

当secure_redirects被设置时,当接收到一个ICMP_REDIRECT消息时,只有当消息中建议的网关在本地是一个已知网关时才接受该消息。

一般来讲,ICMP_REDIRECT消息中建议使用一个新的下一跳,如果该下一跳的IP地址与当前被拒绝的下一跳不在同一网段内则被拒绝,参见RFC 1122中的描述。但是,在一些情况下接受不在同一网段内的新下一跳是有意义的。当

shared_media为真时就接受这些ICMP_REDIRECT消息。RFC 1620清晰地解释了为什么在某些情况下该选项是有意义的。

该特性的更多信息可参见第31章“ICMP重定向消息”小节。

log_martians

当该flag被设置时,当内核接收到的IP包中含有非法地址时生成日志消息。参见第31章“Verbose监控”小节。

36.3.4 /proc/net目录和/proc/net/stat目录

/proc/net目录下有一些文件,当试图dump这些文件内容时执行内核处理钩子函数。下面一个个来描述这些文件:

route

rt_cache

可以读取这两个文件的内容,分别dump出路由表(ip_fib_main_table)和路由缓存。它们并不显示用户定义的路由表的内容,这些路由表是当内核支持策略路由时才创建的。IP地址以十六进制格式输出。

* 也可以参见“使能和禁止转发”小节。

第36章 相关主题 Page 36-11

Understanding Linux Network Internals 第七部分——路由

stat/rt_cache

一组统计信息。参见“统计”小节和“rt_cache_stat结构”小节。 rt_acct

由在第31章介绍的基于路由表的callisifier所收集的计费信息。要想得到格式更佳的输出信息,可使用IPROUTE2包中的rtacct命令。

ip_mr_cache

ip_mr_vif

由多播路由使用(本书中不讨论)。

表36-6汇总了这些文件和相关内核处理钩子函数之间的关联。

表 36-6 /proc/net目录下由路由子系统使用的文件及对应的内核处理钩子函数

文件名 定义所在的内核文件

route net/ipv4/fib_hash.c (fib_proc_init) rt_cache net/ipv4/route.c (ip_rt_init) rt_acct net/ipv4/route.c (ip_rt_init) ip_mr_cache net/ipv4/ipmr.c (ip_mr_init) ip_mr_vif net/ipv4/ipmr.c (ip_mr_init) stat/rt_cache net/ipv4/route.c (ip_rt_init)

如图36-4所示,/proc/net目录和/proc/net/stat目录下的文件都是由inet_init在诸如

ipv4_proc_init和ip_init等程序的帮助下间接创建的。inet_init标记有module_init宏,所以它是在系统启动时被调用(参见第7章)。

36.4 使能和禁止转发

本章前面提到,内核通过/proc输出用于使能和禁止IP转发的参数,这些参数不仅有全局参数,而且对每个设备也有一个参数。我们在本章中只讨论IPv4转发。

虽然管理员可以改变全局的转发状态,但实际上并没有全局转发状态。路由代码只使用每个设备上的转发状态:改变全局配置只是一次性将该改变应用到所有设备的一种简便方法。尤其是当内核接收到一个目的地址不属于本地系统的IP报文时,内核根据接收接口所配置的转发状态,决定该报文是转发还是丢弃。这个决策不依赖于全局转发状态,也不依赖于该报文朝着其目的地被送出的出设备的转发状态。

理解每个设备上的配置和全局配置之间的关系是很重要的,以便在你改变这些配置时能够清楚系统相应的行为。下面列出/proc目录下与IPv4转发相关的文件:

/proc/sys/net/ipv4/conf/device_name/forwarding

对名为device_name的设备使能和禁止转发。值为零时表示禁止,其它值表示使能。

/proc/sys/net/ipv4/conf/all/forwarding

对这个文件的改变将应用到所有的网络设备(包括不是为UP状态的设备),但不影响以后注册的设备的转发状态。

/proc/sys/net/ipv4/conf/default/forwarding 第36章 相关主题 Page 36-12

Understanding Linux Network Internals 第七部分——路由

对没有明确配置转发状态的设备,这是其缺省状态。与前面的文件不同,它的值只影响以后注册的设备的转发状态(而不是已经存在的设备)。

/proc/sys/net/ipv4/ip_forward

改变这个文件与改变/proc/sys/net/ipv4/conf/all/forwarding文件有相同的效果。可以将前面那个文件看作是这个文件的别名。

forwarding文件的变化是由devinet_sysctl_forward来处理,它在内部区分三种情况。ip_forward文件的变化是由ipv4_sysctl_forward来处理。每当至少有一个设备的转发状态发生变化时,利用rt_cache_flush来flush路由缓存。

改变/proc/sys/net/ipv4/conf/all/forwarding文件或是/proc/sys/net/ipv4/ip_forward文件,都将触发执行inet_forward_change,该函数的处理流程为:

1. 更新ipv4_devconf.accept_redirect配置参数。

这样做是为了增强只有主机才能接收ICMP重定向消息,而路由器不能够接收这一规则。如果全局转发状态变为使能,意味着系统现在被视为一台路由器,因而必须禁止可敬的(honouring)ICMP重定向的缺省配置(管理员可以根据需求重新使能该配置参数)。

2. 更新缺省转发状态。

注意改变全局转发配置将强行改变缺省转发配置,但反之不成立。

3. 更新所有设备的转发状态。

第36章 相关主题 Page 36-13

Understanding Linux Network Internals 第七部分——路由

36.5 在本书路由部分中重要的数据结构

在第32章“主要的数据结构”小节中简要介绍了主要的数据结构,在第34章中的图34-1有助于读者理解这些数据结构之间的关系。这一小节详细描述每一种数据结构类型。图36-5给出了定义各种数据结构的文件。

根目录

(通常为usr/src/linux)

include net linux netipv4 route.h dst.h fib_hash.c struct rtentry struct dst_entry struct fib_node struct dst_ops struct fn_zone

route.h struct fn_hash struct rtable fib_rule.c struct ip_rt_acct struct fib_rule

struct rt_cache_stat fib_lookup.h ip_fib.h struct fib_alias struct fib_info multipath_drr.c

struct fib_result struct mltipath_device

struct fib_table multipath_wrandom.c struct fib_nh struct multipath_candidate inetpeer.h struct multipath_dest

struct inet_peer struct multipath_bucket

flow.h struct multipath_route struct flowi

ip_mp_alg.h struct ip_mp_alg_ops

图 36-5 内核文件中的数据结构分布

36.5.1 fib_table结构

对每个路由表实例创建一个fib_table结构,这个结构主要由一个路由表标识和管理该路由表的一组函数指针组成:

unsigned char tb_id

路由表标识。在include/linux/rtnetlink.h文件中可以找到预先定义的类型为rt_class_t的值,例如RT_TABLE_LOCAL。

unsigned tb_stamp

未被使用。

int (*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res)

这个函数被第35章中描述的fib_lookup程序调用。

第36章 相关主题 Page 36-14

Understanding Linux Network Internals 第七部分——路由

int (*tb_insert)(struct fib_table *table, struct rtmsg *r,struct kern_rta *rta, struct nlmsghdr *n, struct netlink_skb_parms *req)

int (*tb_delete)(struct fib_table *table, struct rtmsg *r, struct kern_rta *rta, struct nlmsghdr *n, struct netlink_skb_parms *req);

tb_insert被inet_rtm_newroute和ip_rt_ioctl调用,处理用户空间的ip route

add/change/replace/prepend/append/test 命令和 route add 命令。类似地,tb_delete被inet_rtm_delroute(对ip route del ... 命令作出的响应)和ip_rt_ioctl(对route del ... 命令作出的响应)调用,用于从路由表中删除一条路由。这两个函数指针也被fib_magic(参见“内核插入路由项:fib_magic函数”小节)调用。

int (*tb_dump)(struct fib_table *table, struct sk_buff *skb,

struct netlink_callback *cb)

Dump出路由表的内容。在处理诸如“ip route get...”等用户命令时被激活。

int (*tb_flush)(struct fib_table *table)

将设置有RTNH_F_DEAD标志的fib_info结构删除,参见第33章“垃圾回收”小节。

void (*tb_select_default)(struct fib_table *table, const struct flowi *flp, struct fib_result *res)

选择一条缺省路由,参见第35章“缺省网关选择”小节。

unsigned char tb_data[0]

指向该结构的尾部。当该fib_table结构是另一个更大结构的一部分时,这种做法是很有用的,因为可以在该结构结束时紧接着指向另一个数据结构,参见第34章中的图34-1。

36.5.2 fn_zone结构

一个zone是一组有着相同网络掩码长度的路由项。路由表中的路由项按照zone来组织,参见第32章中的描述。Zone是用fn_zone结构来定义的,该结构包含以下字段:

struct fn_zone *fz_next

将活动zones(active zones)(即至少有一条路由项的zones)链接在一起的指针。该链表的头部由fn_zone_list来跟踪,fn_zone_list是fn_hash数据结构的一个字段。

struct hlist_head *fz_hash

指向存储该zone中路由项的哈希表。

int fz_nent

在该zone中路由项的数目(即在该zone的哈希表中fib_node实例的数目)。这个值可以用于检查是否需要改变该哈希表的容量(参见第34章“Per-Netmask 哈希表容量的动态变化”小节)。

int fz_divisor

哈希表fz_hash的容量(桶的数目),参见第34章“Per-Netmask 哈希表容量的动态变化”小节。 第36章 相关主题 Page 36-15

Understanding Linux Network Internals 第七部分——路由

u32 fz_hashmask

这为fz_divisor-1,提供该字段的理由是可以用操作数更少的按位与操作,而不是用操作数更多的取模操作来计算一个值对fz_divisor取模操作。因为n%fz_divisor的结果与n&fz_hashmask的结果相同(例如100%16 = 100&15),而后一个按位与操作所花费的CPU时间要少。

int fz_order

在网络掩码fz_mask中(所有连续)的比特数目,在代码中也用prefixlen来表示。例如,网络掩码255.255.255.0所对应的fz_order为24。

u32 fz_mask

用fz_order构造的网络掩码。例如设fz_order值取3,则生成的fz_mask的二进制表示为11100000.00000000.00000000.00000000,其十进制表示为224.0.0.0。

在fn_zone结构中还定义了两个宏,分别用于访问fz_hashmask和fz_mask字段:

#define FZ_HASHMASK(fz) ((fz)->fz_hashmask) #define FZ_MASK(fz) ((fz)->fz_mask)

36.5.3 fib_node结构

内核路由项中每一个唯一的目的网络对应一个fib_node实例。目的网络相同但其它配置参数不同的路由项共享同一个fib_node实例。下面描述该结构中的每一个字段:

struct hlist_node fn_hash

fib_node元素是用哈希表来组织的。这个指针用于将分布在一张哈希表中的一个桶内所有的fib_node元素链接在一起。

struct list_head fn_alias

每个fib_node结构与包含一个或多个fib_alias结构的链表相关联。fn_alias指针指向该链表的头部。

su32 fn_key

这是路由项的前缀(或网络地址,用路由项的netmask字段来表示)。该字段被用作查找路由表时的搜索key。参见第34章“哈希表组织的基本结构”小节。

36.5.4 fib_alias结构

fib_alias实例是用来区分目的网络相同但其它配置参数(除了目的地址以外)不同的路由项。下面描述该结构中的每一个字段:

struct list_head fa_list

将与同一个fib_node结构相关联的所有fib_alias实例链接在一起。

struct fib_info *fa_info

该指针指向一个fib_info实例,该实例存储着如何处理与该路由相匹配报文的信息。

第36章 相关主题 Page 36-16

Understanding Linux Network Internals 第七部分——路由

u8 fa_tos

路由的服务类型(TOS)比特位字段(bitfield)。当该值为零时表示还没有配置TOS,所以在路由查找时任何值都可以匹配。不要将fa_tos字段与fib_rule结构中的r_tos字段相混淆。fa_tos字段是用户对每一条路由表项配置的TOS,而fib_rule结构中的r_tos字段是用户对策略规则配置的TOS。

u8 fa_type

参见“rtable结构”小节中对rt_type字段的描述。

u8 fa_scope

路由表项的作用范围。参见第30章“Scope”小节。

u8 fa_state

一些标志的比特位图(Bitmap of flags)。迄今只定义了下面这一个标志:

FA_S_ACCESSED

当查找时访问到fib_alias实例,就设置该标志来标记。当一个fib_node数据结构改变时这个标志很有用:它用于决定是否应当flush路由缓存。如果fib_node已经被访问,那么就可能意味着:在该路由发生变化时需要清理(clear)路由缓存内的表项,从而触发一次flush。

36.5.5 fib_info结构

在前面小节描述到,定义一条路由项的参数包含在fib_node结构和fib_alias结构的组合体内。诸如下一跳网关等重要的路由信息则存储在一个fib_info结构内。下面描述该结构中的每一个字段:

struct hlist_node fib_hash

struct hlist_node fib_lhash

用于将fib_info数据结构插入到第34章“fib_info结构的组织”小节中描述的两个哈希表中。

int fib_treeref

atomic_t fib_clntref

引用计数。fib_treeref是持有该fib_info实例引用的fib_node数据结构的数目,fib_clntref是由于路由查找成功而被持有的引用计数。

int fib_dead

标记路由项正在被删除的标志。当该标志被设置为1时,警告该数据结构将被删除而不能再使用。参见第34章“删除一条路由表项”小节。

unsigned fib_flags

该字段为表36-7中列出的RTNH_F_XXX标志的组合。当前使用的唯一标志是RTNH_F_DEAD。当与一条多路径路由项相关联的所有fib_nh结构都设置了RTNH_F_DEAD标志时,设置该标志(参见第32章“常用的辅助程序与宏”小节)。 第36章 相关主题 Page 36-17

Understanding Linux Network Internals 第七部分——路由

表 36-7 fib_nh结构中nh_flags字段的取值

标志

RTNH_F_DEAD

描述

该标志主要在多路径代码中用于跟踪dead下一跳。参见第32章“常用的辅助程序与宏”小节中对fib_sync_down的描述。

该标志期望用来标记需要递归查找的表项,但是目前还未被使用。最新的IPROUTE2发布版还没有接受pervasive关键字。

当该标志被设置时,告诉内核不要检查下一跳地址是否相连(即不检查该下一跳地址通过出设备是否可达)。例如,当定义在隧道虚设备上的路由时,就可以设置onlink关键字。

RTNH_F_PERVASIVE

RTNH_F_ONLINK

int fib_protocol

设置路由的协议。该字段的可能取值RTPROT_XXX被定义在

include/linux/rtnetlink.h文件内。在表36-8和表36-9中列出了这些值(这两个表中没有ROUTED,因为它不使用Netlink接口与内核通信)。参见第31章“路由协议守护进程”小节中对这些协议的概述。

fib_protocol取值大于RTPROT_STATIC的路由项不是由内核生成(即由用户空间路由协议生成)。

使用该字段的一个例子是使路由守护进程(Daemon)在与内核通信时,操作只能局限于它们自己生成的路由项。更多细节可参见第31章“Interaction between daemons and kernel”小节。*

表 36-8 内核用到的fib_protocol的取值

RTPROT_UNSPEC RTPROT_REDIRECT RTPROT_KERNEL RTPROT_BOOT RTPROT_STATIC

表 36-9 用户空间用到的fib_protocol的取值

RTPROT_GATED RTPROT_RA

描述

由GateD添加的路由。

由RDISC(IPv4)和ND(IPv6)路由器通告添加的路由。在RFC 1256中定义的ICMP路由器发现协议(Router Discovery Protocol)可以使主机找到相邻的路由器。rdisc是iputils软件包的一部分,它是实现ICMP路由器发现协议的一个用户空间工具。

由多线程路由工具包(Multi-Threaded Routing Toolkit——MRT)添加的路由。 描述

表示该字段无效。

由ICMP重定向设置的路由,当前的IPv4不使用该标志。 由内核设置的路由。参见“内核插入路由项:fib_magic函数”小节。

由诸如ip route和route等用户空间命令设置的路由。 由管理员设置的路由。不再被使用。

RTPROT_MRT

RTPROT_ZEBRA 由Zebra添加的路由。 *

没有该小节,是否应当为32.9.3小节?

第36章 相关主题 Page 36-18

Understanding Linux Network Internals 第七部分——路由

RTPROT_BIRD RTPROT_DNROUTED RTPROT_XORP

由BIRD添加的路由。

由DECnet路由守护进程添加的路由。 由XORP路由守护进程添加的路由。

u32 fib_prefsrc

首选源IP地址。参见第35章“选择源IP地址”小节。

u32 fib_priority

路由优先级。值越小则优先级越高。它的值可以用IPROUTE2包中的

metric/priority/preference关键字来配置。当没有明确设定时,内核将它的值初始化为缺省值0。

u32 fib_metrics[RTAX_MAX]

当配置路由时,ip route命令还可以指定一组metrics。fib_metrics是存储这一组metrics的一个向量。没有明确设定的Metrics在初始化时被设置为0。参见第30章“路由的基本要素”小节中列出的可用的metrics。表36-10给出了在那一小节中列出的metrics与在include/linux/rtnetlink.h文件中定义相关的内核符号RTAX_XXX的之间的关系。

表 36-10 路由metrics

Metric 不是一种metric 路径MTU 最大通告窗口 往返时间(RTT) RTT方差 慢启动门限值 拥塞窗口 最大段Size 最大Reordering 缺省生存时间(TTL) 初始拥塞窗口

内核符号 RTAX_LOCK RTAX_MTU RTAX_WINDOW RTAX_RTT RTAX_RTTVAR RTAX_SSTHRESH RTAX_CWND RTAX_ADVMSS RTAX_REORDERING RTAX_HOPLIMIT RTAX_INITCWND

RTAX_FEATURES 不是一种metric

int fib_power

当内核编译支持多路径时,该字段才是fib_info数据结构的一部分。参见第31章“多路径路由背后的概念”小节。

struct fib_nh fib_nh[0]

int fib_nhs

fib_nh是结构类型为fib_nh的一个可变长向量,fib_nhs是该向量的size。只有当内核支持多路径特性时,fib_nhs才可能大于1。参见第31章“多路径路由背后的概念”小节和第34章中的图34-1。

第36章 相关主题 Page 36-19

Understanding Linux Network Internals 第七部分——路由

u32 fib_mp_alg

多路径缓存算法。在第31章“缓存支持多路径”小节介绍的多路径缓存算法的标识IP_MP_ALG_XXX在include/linux/ip_mp_alg.h文件中定义。只有当内核编译支持多路径缓存时,该字段才是fib_info数据结构的一部分。

#define fib_dev fib_nh[0].nh_dev

这个宏用于访问fib_nh向量中第一个fib_nh实例的nh_dev字段。参见第34章中的图34-1。

#define fib_mtu fib_metrics[RTAX_MTU-1]

#define fib_window fib_metrics[RTAX_WINDOW-1]

#define fib_rtt fib_metrics[RTAX_RTT-1]

#define fib_advmss fib_metrics[RTAX_ADVMSS-1]

这些宏用于访问fib_metrics向量中指定的元素。

36.5.6 fib_nh结构

对每一个下一跳,内核需要跟踪的信息要比IP地址信息更为丰富。fib_nh结构在下面字段中存储这些额外的信息:

struct net_device *nh_dev

这是与设备标识nh_oif(后面描述)相关联的net_device数据结构。因为设备标识和指向net_device结构的指针都需要利用(在不同的上下文内),所以这两项都存在于fib_nh结构中,虽然利用其中任何一项就可以得到另一项。

struct hlist_node nh_hash

用于将fib_nh数据结构插入到在第34章“下一跳路由器结构的组织”小节中描述的哈希表中。

struct fib_info *nh_parent

该指针指向包含该fib_nh实例的fib_info结构,参见第34章中的图34-1。

unsigned nh_flags

在include/linux/rtnetlink.h文件中定义的一组RTNH_F_XXX标志,在本章前面的表36-7中列出。

unsigned char nh_scope

用于获取下一跳的路由scope。在大多数情况下为RT_SCOPE_LINK。该字段由fib_check_nh来初始化。

int nh_weight

int nh_power

只有当内核编译支持多路径时,这两个字段才是fib_nh数据结构的一部分,在第31章“多路径路由背后的概念”小节对它们有详细描述。nh_power是由内核初始化,nh_weight是由用户利用关键字weight来设置。 第36章 相关主题 Page 36-20

Understanding Linux Network Internals 第七部分——路由

__u32 nh_tclassid

只有当内核编译支持基于路由表的classifier时,该字段才是fib_nh数据结构的一部分。它的值是利用realms关键字来设置。参见第35章“策略路由与基于路由表的Classifier”小节。

int nh_oif

egress设备标识。它是利用关键字oif和dev来设置的。

u32 nh_gw

下一跳网关的IP地址,它是利用关键字via来设置的。注意在NAT情况下,它表示NAT路由器向外部世界通告的地址,以及在NAT路由器向内部网中的主机发送回应之前而向外部世界回应的地址。例如,命令ip route add nat 10.1.1.253/32 via 151.41.196.1将设置nh_gw为151.41.196.1。注意在2.6内核中路由代码已经不再支持NAT,即原来众所周知的FastNAT。

36.5.7 fib_rule结构

利用ip rule命令来配置策略路由规则(也称为策略——policies)。如果你的Linux系统上安装了IPROUTE2软件包,就可以输入ip rule help来查看该命令的语法。策略被存储在fib_rule结构内,下面描述该结构的每一个字段:

struct fib_rule *r_next

将这些fib_rule结构链接到一个包含所有fib_rule实例的全局链表内(参见第35章中的图35-8)。

atomic_t r_clntref

引用计数。该引用计数的递增是在fib_lookup函数(只在策略路由版的函数中)中进行的,这解释了为什么在每次路由查找成功后总是需要调用fib_res_put(递减该引用计数)。

u32 r_preference

路由规则的优先级。当管理员利用IPROUTE2软件包添加一个策略时,可以使用关键字priority,preference和order来配置。如果没有明确配置,内核为其分配一个优先级,该值比用户添加的最后一个规则的优先级小1(参见inet_rtm_newrule)。优先级0,0x7FFE和0x7FFF是预留给由内核添加的特定规则(参见第35章“策略路由时的fib_lookup”小节,以及在net/ipv4/fib_rules.c文件中定义的三个缺省规则local_rule,main_rule和default_rule)。

unsigned char r_table

路由表标识,范围从0到255。当用户没有指定路由表标识时,IPROUTE2按照以下方法来选择路由表:当用户命令是添加一条规则时使用RT_TABLE_MAIN,其它情况使用RT_TABLE_UNSPEC(例如删除一条规则)。

第36章 相关主题 Page 36-21

Understanding Linux Network Internals 第七部分——路由

unsigned char r_action

该字段允许的取值是在include/linux/rtnetlink.h文件中定义的rtm_type枚举值(RTN_UNICAST等)。这些值的含义在“rtable结构”小节中描述。

当用户配置一条规则时,使用type关键字来设定该字段。如果用户没有明确配置,IPROUTE2在添加规则时设置该字段的值为RTN_UNICAST,在其它情况下设置为RTN_UNSPEC(例如当删除规则时)。

unsigned char r_dst_len

unsigned char r_src_len

目的IP地址与源IP地址的长度,单位为比特。它们被用于计算r_srcmask和 r_dstmask。如果这两个字段未被初始化则设置为0。

u32 r_src

u32 r_srcmask

表示只有从该IP地址和网络掩码组成的源网络发送的报文才能被接受。

u32 r_dst

u32 r_dstmask

表示只能向该IP地址和网络掩码组成的目的网络发送报文。

u32 r_srcmap

该字段是利用用户空间关键字nat和map-to来设置,在路由NAT实现代码使用。由于不再支持路由NAT,所以该字段也不再被使用。参见第32章“最近废弃的选项”小节。

u8 r_flags

一组标志。当前未使用。

u8 r_tos

IP头中的TOS字段。包含该字段的原因是规则的定义中可以包含一个条件,该条件放在IP头部的TOS字段。

u32 r_fwmark

当内核编译支持“使用Netfilter MARK值作为路由key“特性时,可以根据防火墙标签来定义规则。该字段是管理员定义一条策略规则时利用fwmark关键字指定的标签。

int r_ifindex

char r_ifname[IFNAMSIZ]

r_ifname是策略应用的设备的名称。给定r_ifname,内核可以得到相关的net_device实例,将该实例的ifindex字段拷贝到r_ifindex中。r_ifindex值取-1表示禁止该规则(参见第32章“对策略数据库的影响”小节)。

第36章 相关主题 Page 36-22

Understanding Linux Network Internals 第七部分——路由

__u32 r_tclassid;

当内核编译支持基于路由表的classifier时,该字段才被包含在fib_rule数据结构内。它的含义在第35章“策略路由与基于路由表的Classifier”小节中描述。

int r_dead

当一个规则可用时该字段为0。当利用inet_rtm_delrule删除规则时该字段被设置为1。每当调用fib_rule_put删除到fib_rule数据结构的一个引用时,递减引用计数,当引用计数为0时就要释放该结构。但此时如果r_dead没有设置,则表示发生了某种错误(例如代码对引用计数的设置不正确)。

36.5.8 fib_result结构

fib_result结构被fib_semantic_match初始化为路由查找结果,更多细节可参见第33章和第35章(尤其是“Semantic匹配其它标准”小节)。在该结构中的字段为:

unsigned char prefixlen

匹配路由的前缀长度。参见“fn_zone结构”小节内对fz_order的描述。

unsigned char nh_sel

多路径路由是由多个下一跳来定义的。该字段标识已经被选中的下一跳。

unsigned char type

unsigned char scope

这两个字段被初始化为相匹配的fib_alias实例的fa_type和fa_scope字段的取值。

__u32 network

__u32 netmask

只有当内核编译支持多路径缓存时,这两个字段才包含在fib_result数据结构内。参见第33章“加权随机算法”小节中,加权随机多路径缓存算法是如何使用这两个字段。

struct fib_info *fi

与匹配的fib_alias实例相关联的fib_info实例。

struct fib_rule *r

与前面字段不同的是,该字段由fib_lookup来初始化。只有当内核编译支持策略路由时,该字段才包含在fib_result数据结构内。

36.5.9 rtable结构

IPv4使用rtable数据结构来存储缓存内的路由表项。* 为了dump出路由缓存的内容,可以查看/proc/net/rt_cache文件(参见“通过/proc文件系统调整路由”小节),或是发布一个ip route list cache 或 route –C命令。下面描述该结构的每一个字段: * IPv6使用rt6_info,DECnet(本书中不讨论)使用dn_route。

第36章 相关主题 Page 36-23

Understanding Linux Network Internals 第七部分——路由

union {...} u

这个联合用来将一个dst_entry结构嵌入到rtable结构中(参见第33章“哈希表组织”小节)。该联合中的rt_next字段被用于链接分布在同一个哈希桶内的rtable实例。

struct in_device *idev

该指针指向egress设备的IP配置块。注意对送往本地的ingress报文的路由,设置的egress设备为loopback设备。

unsigned rt_flags

在该比特图中可以设置的标志为在include/linux/in_route.h文件内定义的RTCF_XXX值,在表36-11中列出了这些值。

表 36-11 rt_flags的可能取值

标志

RTCF_NOTIFY

描述

路由表项的所有变化通过Netlink通知给感兴趣的用户空间应用程序。该选项还没有完全实现。利用诸如ip route get 10.0.1.0/24 notify 等命令来设置该标志。

对接收到的ICMP_REDIRECT消息作出响应而添加一条路由表项(参见ip_rt_redirect及其调用方)。

当必须向源站送回ICMP_REDIRECT消息时,ip_route_input_slow设置该标志。ip_forward依据该标志和其它信息,决定是否需要发送ICMP重定向消息,这在第20章有详细描述。例如,如果报文是基于源站的路由,不应当生成ICMP重定向消息。

该标志主要用于告诉ICMP代码,不应当对地址掩码请求消息作出回应。每当调用fib_validate_source检查到接收报文的源地址通过一个本地作用范围(RT_SCOPE_HOST)的下一跳是可达时,就设置该标志。更多细节可参见第25章和第35章。

这些标志不再被IPv4使用。它们以前被FastNAT特性使用,该特性在2.6内核中已经被删除(参见第32章“最近废弃的选项”小节)。

RTCF_REDIRECTED RTCF_DOREDIRECT

RTCF_DIRECTSRC

RTCF_SNAT

RTCF_DNAT

RTCF_NAT

RTCF_BROADCAST RTCF_MULTICAST RTCF_LOCAL

路由的目的地址是一个广播地址。 路由的目的地址是一个多播地址。

路由的目的地址是一个本地地址(即本地接口上配置的某个地址)。对本地广播地址和本地多播地址也设置该标志(参见ip_route_input_mc)。

未被使用。依据IPROUTE2软件包的ip rule命令的语法,在该命令中有一个关键字reject,但该关键字还未被接受。 未使用。 未使用。

未使用。该标志已经被废弃,设置该标志是用于标记一条路由对快速交换(Fast Switching)合法。快速交换特性已经在2.6内核中被废弃。

不再被IPv4使用。该标志是用于标记报文来自于masqueraded源地址。

RTCF_REJECT RTCF_TPROXY RTCF_DIRECTDST RTCF_FAST

RTCF_MASQ

第36章 相关主题 Page 36-24

Understanding Linux Network Internals 第七部分——路由

unsigned rt_type

路由类型。它间接定义了当路由查找匹配时应采取的动作。该字段可能的取值是在include/linux/rtnetlink.h文件中定义的RTN_XXX宏,在表36-12中列出了这些值。

表 36-12 rt_type可能的取值

路由类型

RTN_UNSPEC RTN_LOCAL RTN_UNICAST

描述

定义一个未初始化的值。例如,当从路由表中删除一个表项时使用该值,这是因为删除操作不需要指定路由表项的类型。 目的地址被配置为一个本地接口的地址。

该路由是一条到单播地址的直连或非直连(通过一个网关)路由。当用户通过ip route命令添加路由但没有指定其它路由类型时,路由类型缺省被设置为RTN_UNICAST。 目的地址是一个多播地址。

目的地址是一个广播地址。匹配的ingress报文以广播方式送往本地,匹配的egress报文以广播方式发送出去。

匹配的ingress报文以广播方式送往本地,匹配的egress报文以单播发送出去。不被IPv4使用。

这些值与特定的管理配置而不是与目的地址类型相关联。参见第30章“路由类型与动作”小节。

RTN_MULTICAST RTN_BROADCAST RTN_ANYCAST RTN_BLACKHOLE

RTN_UNREACHABLE

RTN_PROHIBIT

RTN_THROW RTN_NAT RTN_XRESOLVE

源IP地址和/或目的IP地址必须被转换。因为相关的特性FastNAT已经在2.6内核中被废弃,所以该类型不再被使用。 有一个外部解析器来处理该路由。目前尚未实现该功能。

__u16 rt_multipath_alg

多路径缓存算法。该字段是根据相关路由项上配置的算法来初始化(参见“fib_info结构”小节中的fib_mp_alg)。

__u32 rt_dst

__u32 rt_src

目的IP地址和源IP地址。

int rt_iif

Ingress设备标识。这个值是从ingress设备的net_device数据结构中得到。对本地生成的流量(因此不是从任何接口上接收到的),该字段被设置为出设备的ifindex字段。不要将该字段与本章后面描述的flowi数据结构fl中的iif字段混淆,对本地生成的流量,fl中的iif字段被设置为零(loopback_dev)。

__u32 rt_gateway

当目的主机为直连时(即在同一链路上),rt_gateway表示目的地址。当需要通过一个网关到达目的地时,rt_gateway被设置为由路由项中的下一跳网关。

第36章 相关主题 Page 36-25

Understanding Linux Network Internals 第七部分——路由

struct flowi fl

用于缓存查找的搜索key,参见“flowi结构”小节中的描述。

__u32 rt_spec_dst

RFC 1122中指定的目的地址,参见第35章“首选源地址选择”小节中的描述。

struct inet_peer *peer

在第19章介绍的inet_peer结构存储有关IP peer的long-living信息,IP peer是该缓存路由项的目的IP地址对应的主机。与本地主机在最近一段时间通信的每个远端IP地址都有一个inet_peer结构。

36.5.10dst_entry结构

数据结构dst_entry被用于存储缓存路由项中于协议的信息。L3协议在另外的结构中存储本协议的更多的私有信息(例如,IPv4使用rtable结构)。下面描述该结构的每一个字段:

struct dst_entry *next

用于将分布在同一个哈希桶内的dst_entry实例链接在一起。参见第33章中的图33-1。

struct dst_entry *child

unsigned short header_len

unsigned short trailer_len

struct dst_entry *path

struct xfrm_state *xfrm

这些字段被IPsec代码使用。

atomic_t __refcnt

引用计数。参见第33章“删除DST表项”小节。

int __use

该表项已经被使用的次数(即缓存查找返回该表项的次数)。不要将这个值与rt_cache_stat[smp_processor_id( )].in_hit混淆:后者(在“统计”小节中描述)表示针对某个CPU的全局缓存命中次数。

struct net_device *dev

Egress设备(即将报文送达目的地的发送设备)。

short obsolete

用于定义该dst_entry实例的可用状态:0(缺省值)表示该结构有效而且可以被使用,2表示该结构将被删除因而不能被使用,-1被IPsec和IPv6使用但不被IPv4使用。

第36章 相关主题 Page 36-26

Understanding Linux Network Internals 第七部分——路由

int flags

标志集合(Set of flags)。DST_HOST被TCP使用,表示主机路由(即它不是到网络或到一个广播/多播地址的路由)。DST_NOXFRM,DST_NOPOLICY和DST_NOHASH只用于IPsec。

unsigned long lastuse

用于记录该表项上次被使用的时间戳。当缓存查找成功时更新该时间戳,垃圾回收程序使用该时间戳来选择最合适的应当被释放的结构。

unsigned long expires

表示该表项将过期的时间戳。参见第33章“过期标准”小节。

u32 metrics[RTAX_MAX]

metrics向量,主要被TCP使用。该向量是用fib_info->fib_metrics向量的一份拷贝来初始化(如果fib_metrics向量被定义),当需要时使用缺省值。参见函数

rt_set_nexthop和第35章,参见表36-10中列出的该向量所有可能取值的描述。

需要解释一下值RTAX_LOCK。RTAX_LOCK不是一个metric,而是一个比特位图(bitmap):当位置n的比特位被设置时,表示已经利用lock选项/关键字配置了值为n的metric。换句话说,一个类似于ip route add ... advmss lock ... 的命令将设置1<unsigned long rate_last

unsigned long rate_tokens

这两个字段被用于对两种类型的ICMP消息限速。参见第33章“Egress ICMP重定向限速”小节和第35章“选路失败”小节。

short error

当fib_lookup API(只被IPv4使用)失败时,错误值被保存在error(用一个正值)中,在后面的ip_error中使用该值来决定如何处理本次路由查找失败(即决定生成哪一类ICMP消息)。

struct neighbour *neighbour

struct hh_cache *hh

neighbour是包含下一跳三层地址到二层地址映射的结构,hh是缓存的二层头。详见第六部分的各章节。

int (*input)(struct sk_buff*)

int (*output)(struct sk_buff**)

分别表示处理ingress报文和处理egress报文的函数。参见第33章“缓存查找”小节。

__u32 tclassid

基于路由表的classifier的标签。参见第35章“策略路由和基于路由表的Classifier”小节。

第36章 相关主题 Page 36-27

Understanding Linux Network Internals 第七部分——路由

struct dst_ops *ops

该结构内的虚函数表(VFT)用于处理dst_entry结构。

struct rcu_head rcu_head

处理互斥。

char info[0]

该字段用作dst_entry数据结构尾部的指针很有用。它只是用于占位(a placeholder)。

36.5.11dst_ops结构

dst_ops结构是使用路由缓存的三层协议与于协议的缓存之间的接口。参见第33章“DST与调用协议之间的接口”小节。下面描述该结构的每一个字段:

unsigned short family

地址系列。参见include/linux/socket.h文件中的AF_XXX。

unsigned short protocol

协议ID。参见include/linux/if_ether.h文件中的ETH_P_XXX。

unsigned gc_thresh

该字段用于垃圾回收算法,指定了路由缓存的容量(即哈希桶的数目)。其初始化是在ip_rt_init(IPv4路由子系统初始化函数)中完成的。

int (*gc)(void) atomic_t entries

当协议已经分配的dst_entry实例数目(entries)已经超过或等于门限值gc_thresh时,由dst_alloc激活垃圾回收函数gc。

struct dst_entry * (*check)(struct dst_entry *, _ _u32 cookie)

void (*destroy)(struct dst_entry *)

void (*ifdown)(struct dst_entry *, struct net_device *dev, int how)

struct dst_entry * (*negative_advice)(struct dst_entry *)

void (*link_failure)(struct sk_buff *)

void (*update_pmtu)(struct dst_entry *dst, u32 mtu)

int (*get_mss)(struct dst_entry *dst, u32 mtu)

参见第33章“DST与调用协议之间的接口”小节。

int entry_size

三层路由缓存结构(例如针对IPv4的结构为rtable)的大小。

kmem_cache_t *kmem_cachep

分配路由缓存元素的内存池。 第36章 相关主题 Page 36-28

Understanding Linux Network Internals 第七部分——路由 36.5.12flowi结构

利用flowi数据结构,就可以根据诸如ingress设备和egress设备、L3和L4协议报头中的参数等字段的组合对流量进行分类。它通常被用作路由查找的搜索key,IPsec策略的流量选择器以及其它高级用途。下面描述该结构的每一个字段:

int oif

int iif

Egress设备ID和ingress设备ID。

union {...} nl_u

该联合的各个字段是可用于指定L3参数取值的结构。目前支持的协议为IPv4,IPv6和DECnet。

__u8 proto

L4协议。

__u8 flags

该变量只定义了一个标志,FLOWI_FLAG_MULTIPATHOLDROUTE,它最初用于多路径代码,但已不再被使用。

union {...} uli_u

该联合的各个字段是可用于指定L4参数取值的主要结构。目前支持的协议为TCP, UDP,ICMP,DECnet和IPsec协议套件(suite)。

由于flowi数据结构不是扁平式的(flat),而是包含了许多联合和结构,所以内核提供了一组宏用于访问某些字段。

36.5.13rt_cache_stat结构

rt_cache_stat结构存储了在“统计”小节中介绍的统计信息的counter。下面描述这些counters:

in_hit

out_hit

分别表示已经查找路由缓存成功而被路由的接收报文和本地生成报文的数目(参见ip_route_input和ip_route_output_key)。

in_slow_tot

in_slow_mc

in_slow_tot是由于缓存查找失败而需要查找路由表的报文数目(参见

ip_route_input_slow),只对查找路由表成功的报文计数。该counter被称为慢(slow)是因为查找路由表与查找路由缓存相比是非常地慢。该counter也对广播报文计数,但不对多播流量计数。多播流量是用in_slow_mc变量来计数。

第36章 相关主题 Page 36-29

Understanding Linux Network Internals 第七部分——路由

out_slow_tot

out_slow_mc

out_slow_tot和out_slow_mc所起的作用分别与in_slow_tot和in_slow_mc相同,但它们用于egress流量的计数。

in_no_route

由于路由表不知道如何到达目的IP地址(只可能在缺省网关没有配置或不可用的情况下才发生)而不能被转发的ingress报文的数目。参见ip_route_input_slow。没有counter被用于跟踪由于查找路由失败而不能被送出的本地生成报文的数目。

in_brd

被正确接收(即合理性检查都没有失败)的广播报文的数目。没有counter用于送出的广播报文数目。

in_martian_dst

in_martian_src

这两个counters分别表示由于目的IP地址或源IP地址没有通过合理性检查而被丢弃的报文数目。合理性检查的例子包括源IP地址不能为多播或广播,目的地址不能属于所谓的零网段,即地址不能为0.n.n.n。

gc_total

gc_ignored

gc_goal_miss

gc_dst_overflow

这四个字段被第33章“rt_garbage_collect函数”小节中描述的rt_garbage_collect函数更新。

gc_total跟踪rt_garbage_collect函数被激活的次数。

gc_ignored跟踪rt_garbage_collect函数刚被调用不久因而立即退出的次数。

gc_goal_miss是rt_garbage_collect已经扫描完缓存但没能满足函数开始时所设定的目标的次数。

gc_dst_overflow是gc_garbage_collect函数由于没有将缓存表项数目减少到ip_rt_max_size门限值以下而失败的次数。

in_hlist_search

out_hlist_search

这两个字段分别由缓存查找程序ip_route_input和__ip_route_output_key更新。它们表示已经测试但没有找到匹配的缓存元素数目(不是缓存查找失败次数)。

第36章 相关主题 Page 36-30

Understanding Linux Network Internals 第七部分——路由

36.5.14ip_mp_alg_ops结构

ip_mp_alg_ops表示路由缓存与多路径缓存特性之间的接口,该结构由以下函数指针组成:

void (*mp_alg_select_route) (const struct flowi *flp, struct rtable *rth, struct rtable **rp)

void (*mp_alg_flush) (void)

void (*mp_alg_set_nhinfo) (_ _u32 network, _ _u32 netmask, unsigned char prefixlen, const struct fib_nh *nh)

void (*mp_alg_remove) (struct rtable *rth)

这些函数被第33章“路由缓存与多路径之间的接口”小节中描述的于算法的wrapper函数激活。

36.6 在本书路由部分中重要的函数和变量

表36-13汇总了在本书描述的路由子系统的章节中介绍或引用的主要函数、变量和数据结构。更多的函数和变量可以参见第32章中“常用的辅助程序与宏”小节和“辅助程序”小节,以及在第35章中的两个“辅助程序”小节。

第36章 相关主题 Page 36-31

Understanding Linux Network Internals 第七部分——路由

表 36-13 在路由子系统中用到的函数、变量和数据结构

描述

函数

for_ifa, endfor_ifa

for_primary_ifa, endfor_ifa FIB_RES_XXX

这些宏用于遍历一个网络设备上配置的IPv4地址。参见第32章“主IP地址与第二IP地址”小节。

这组宏用于访问fib_result结构的各个字段。参见第32章“常用的辅助程序与宏”小节。

这些宏用于识别特殊IP地址。参见第32章“常用的辅助程序与宏”小节。

LOOPBACK

ZERONET

MULTICAST

LOCAL_MCAST/BADCLASS fib_hash_lock

fib_info_lock

fib_rules_lock

rt_flush_lock

fib_multipath_lock

alg_table_lock ip_rt_init

ip_fib_init

devinet_init

fib_rules_init

fib_hash_init

dst_init dst_alloc

用于保护各种数据片段的锁。参见第32章“全局锁”小节。

各种初始化程序。参见第32章“路由子系统初始化”小节。

rt_periodic_timer

rt_secret_timer fib_netdev_event

fib_inetaddr_event fib_add_ifaddr

fib_del_ifaddr fib_magic fib_rules_detach

fib_rules_attach rtmsg_fib

为路由缓存分配一个表项。参见第33章“缓存表项的分配与引用计数”小节。

定时器。参见第33章“垃圾回收”和“Flush路由缓存”小节。

netdev_chain和inetaddr_chain通知链的钩子处理函数。参见第32章“外部事件”小节。

当在本地网络设备上通过配置添加或删除一个IP地址时,分别使用这两个函数来更新路由表。参见第32章“添加一个IP地址”和“删除一个IP地址”小节。

在特定条件下被内核用于插入路由表项。参见本章“内核插入路由项:fib_magic函数”小节。

分别在网络设备注册和注销时使能和禁止路由策略。参见第32章“对策略数据库的影响”小节。

当路由已经被添加或删除时向一个特定的Netlink多播组发送通知。参见第32章“Netlink通知”小节。

第36章 相关主题 Page 36-32

Understanding Linux Network Internals 第七部分——路由

ip_route_input

__ip_route_output_key

ip_route_output_flow

ip_route_output_key

ip_route_connect

ip_route_newports ip_route_input_slow

ip_route_output_slow ip_route_input_mc ip_mkroute_input

ip_mkroute_input_def

ip_mkroute_output

ip_mkroute_output_def

fib_select_default

fib_select_multipath fib_lookup

fn_hash_lookup

fib_semantic_match fn_hash_insert fn_hash_delete rt_intern_hash

multipath_alg_register

multipath_alg_unregister multipath_select_route

multipath_flush

multipath_set_nhinfo

multipath_remove rt_free

dst_free

rt_garbage_collect

rt_may_expire dst_input

dst_output

前面两个函数是路由缓存查找程序,其它函数都是针对它们进行的封装。参见第33章“缓存查找”小节。

路由表查找程序。参见第35章。

用于多播目的地址的查找程序。

在ip_route_input_slow和 ip_route_output_slow中用到的各种支持程序。参见第35章。

在路由表查找的不同阶段调用的程序。参见第35章“查找函数的High-Level View”小节。

向路由表添加一条新路由表项。参见第34章“添加一条路由表项”小节。

从路由表删除一条路由表项。参见第34章“删除一条路由表项”小节。

向路由缓存添加一条表项。参见第33章“添加元素到缓存中”小节。

注册和注销一个多路径缓存算法。参见第33章“注册一个缓存算法”小节。

这些程序用于管理与多路径路由相关的缓存表项。参见第33章“路由缓存与多路径之间的接口”小节,更多的程序在同一章“辅助程序”小节中列出。

分别释放一个rtable和一个dst_entry结构。

路由缓存的垃圾回收程序。参见第33章“rt_garbage_collect函数”小节。

分别完成报文的接收和发送。参见第33章“缓存查找”小节,也可以参见第35章“为接收和发送设置函数”小节。

第36章 相关主题 Page 36-33

Understanding Linux Network Internals 第七部分——路由

rt_garbage_collect

dst_destroy

dst_ifdown

dst_negative_advice

dst_link_failure

dst_set_expires dst_dev_event

RT_CACHE_STAT_INC 变量

ip_fib_local_table

ip_fib_main_table rt_hash_table rt_hash_mask dst_garbage_list fib_tables fib_rules fib_info_cnt fib_info_hash

fib_info_laddrhash fib_info_devhash fib_props

这些程序用于初始化与IPv4协议相关的dst_ops实例。参见第33章“DST与调用协议之间的接口”小节。

DST子系统处理来自netdev_chain通知链事件的钩子函数。参见第32章“外部事件”小节。

更新每个CPU的统计信息。参见本章“统计”小节。 描述

路由表。参见第34章“两个缺省路由表:ip_fib_main_table与ip_fib_local_table”小节。 路由缓存。参见第33章。

路由缓存容量(即哈希表的桶数目)。

由于仍然被引用而不能被删除的dst_entry实例链表。参见第33章。

fib_table实例链表。参见第34章中的图34-1。

路由策略链表。参见第35章“策略路由时的fib_lookup”小节。 所有fib_info实例数目。参见第34章“全局哈希表容量的动态变化”小节。

用于搜索fib_info实例的哈希表。参见第34章“fib_info结构的组织”小节。

用于搜索fib_nh实例的哈希表。参见第34章“下一跳路由器结构的组织”小节。

查找程序fib_semantic_match利用该向量的元素将路由类型映射为返回值。参见第35章“fib_semantic_match函数的返回值”小节。 描述

路由代码使用的关键数据结构。详细描述可以参见本章“在本书路由部分中重要的数据结构”小节。

数据结构

fib_table structure

fn_zone structure

fib_node structure

fib_alias structure

fib_info structure

fib_nh structure

fib_rule structure

rtable structure

dst_entry structure

dst_ops structure

第36章 相关主题 Page 36-34

Understanding Linux Network Internals 第七部分——路由

flowi structure

rt_cache_stat structure

ip_mp_alg_ops structure

36.7 在本书路由部分中重要的文件和目录

图36-6列出了在第七部分章节中用到的文件和目录。

根目录

(通常为usr/src/linux)

include net

linux netipv4core ip_fib.h in_route.h fib_frontend.c dst.c route.h route.h fib_hash.c

dst.h mroute.h fib_rule.c

ip_mp_alg.hip_mp_alg.h fib_semantics.c

route.c

ipmr.c

ip_input.c ip_forward.c ip_output.c multipath.c

multipath_drr.c

multipath_random.c

multipath_rr.c

multipath_wrandom.c

图 36-6 在本书路由部分中重要的文件和目录

第36章 相关主题 Page 36-35

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- huatuo6.com 版权所有 湘ICP备2023023988号-11

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务