分类: 系统运维
2009-06-16 09:32:00
近年来,基于 IP 多播的业务越来越受到重视,特别是消费类电子产品的普及,对于多播业务的支持提出了一个新的要求。一般说来,像 ADSL Router 等消费类电子产品并没有必要去实现一个完整且复杂的多播路由协议,针对于此,IGMP Proxy 以及 IGMP Snooping 等解决方案被提出来,以更好的透过这些设备来支持多播业务的部署。
近年来许多城市都已建立了自己的 IP 宽带城域网,同时建立了城市间的 IP 宽带网络;而以 ADSL 为代表的宽带接入技术在近几年来也在大力推广。这些 IP 宽带城域网是以 Ethernet/PPPoE 为基础 , 而基于 IP 的各种接入技术适应了网络 IP 化的发展趋势,在组网的灵活性、业务的支持能力等方面显现出极大的优势。
IP 多播 (multicast) 是一种单点到多点的协议体系,它将 IP 数据包从一个源地址传送到多个目的地,但数据拷贝只传输一份。 IP 多播将 IP 数据包“尽最大努力”传输到一个构成多播群组的主机或路由器集合,群组的各个成员可以分布于相互独立的物理网络上。与单播 (unicast) 相比,多播可以大大的节省网络带宽,提高了数据传送效率;与广播 (broadcast) 相比又减少了主干网出现拥塞的可能性,避免了广播的“泛洪”。多播的出现解决了一个源向特定多个目的发送消息的方法。 IP 多播技术在实时数据传送、多媒体会议、数据拷贝、游戏和仿真、视频点播等诸多方面都有广泛的应用前景。但是,目前多播业务的开展中还有一些问题,特别是针对于消费类电子产品,不适合于去实现一个完整的多播路由协议,因此,有必要提供一个简单的办法去支持多播。
多播采用的特殊的地址格式,在 IP 层来说,它使用的是 D 类地址,具体格式如图 1:
而在数据链路层,IP 多播地址与以太网地址有一个映射关系:将多播组 ID 中的低 23bit 映射到以太网地址的低 23bit 。而在以太网地址中,为了指明一个多播地址,任何一个以太网地址的首字节必须是 01,这意味着与 IP 多播相对应的以太网地址范围从 01:00:5e:00:00:00 到 01:00:5e:7f:ff:ff 。 IP 地址与以太网多播地址的映射关系如图 2:
由上面的映射过程可以知道,由于多播 ID 中的最高 5bit 在映射过程中被忽略,因此每个以太网多播地址对应的多播组并不唯一,每个多播以太网地址可以被映射到 32(25) 个多播组。
多播路由的一种常见的思路就是在多播组成员之间构造一棵扩展分布树 (Spanning Tree) 。在一个特定的“发送源、目的组”对上的 IP 多播流量都是通过这个扩展树从发送源传输到接受者的,这个扩展树连接了该多播组中所有主机。不同的 IP 多播路由协议使用不同的技术来构造这些多播扩展树,一旦这个树构造完成,所有的多播流量都将通过它来传播。按照分布树的构造原理,可以分为源多播树和共享分发树两种。
这种情况下,是假设多播组成员密集地分布在网络中,也就是说,网络大多数的子网都至少包含一个多播组成员,而且网络带宽足够大,这种被称作“密集模式” (Dense-Mode) 的多播路由协议依赖于广播技术将数据“推”向网络中所有的路由器。密集模式路由协议包括距离向量多播路由协议 DVMRP(Distance Vector Multicast Routing Protocol)、多播开放最短路径优先协议 MOSPF (Multicast Open Shortest Path First) 和密集模式独立多播协议 PIM-DM(Protocol-Independent Multicast Dense-Mode) 等。
这种类型则假设多播组成员在网络中是稀疏分散的,并且网络不能提供足够的传输带宽,比如 Internet 上通过 ISDN 线路连接分散在许多不同地区的大量用户。在这种情况下,广播就会浪费许多不必要的网络带宽从而可能导致严重的网络性能问题。于是稀疏模式多播路由协议必须依赖于具有路由选择能力的技术来建立和维持多播树。稀疏模式主要有基于核心树的多播协议 CBT(Core Based Tree) 和稀疏模式独立协议多播 PIM-SM( Protocol-Independent Multicast Sparse Mode) 。
在网络拓扑结构变得很简单时,上述多播路由协议显得有些繁杂,因此,有必要针对比较常见的简单网络拓扑提出一个简单的解决办法。在家庭以及中小型企业,视 DSLAM/Router 为节点的话,网络拓扑多为单根无环树,这种情况下,IGMP Snooping、IGMP Proxy 和 CGMP(Cisco Group Management Protocol) 对多播问题的解决更简单有效。因此,在网络边界上,如果网络拓扑结构为单根树,IGMP Snooping、IGMP Proxy 和 CGMP(Cisco Group Management Protocol) 可以满足用户对多播业务的部署需要。由于消费类电子产品多不是位于骨干网络中,因此对于这种设备的应用场景,简化其应用很有必要性。
一般的多播路由协议其核心在于怎么使用生成树 (spanning tree) 算法来剪枝,使一个无向图成为一棵树,但很多情况下,网络拓扑已经是一颗树了,没有必要在去运行这样一个完整的多播路由协议,此时,根据 IGMP 包的内容来转发多播包是合适的。一个典型的例子是像 DSLAM/ADSL Router 这类网络边界聚集盒 (edge aggregation box) 的使用场景。在这类设备中,假想的部署情况是一端连接核心网 ( 比如电信 ) ;而另一端连接多个客户 ( 比如 PC) 。使用 IGMP Proxy 不仅使得这些设备支持多播的实现更加的简单,也减轻了用户操作上的开销。这样,这些设备可以独立于核心网络上支持多播路由协议的设备,因此它能简单的去部署。而实现上的简单性所带来的鲁棒性 (Robustness) 也使得这些设备能更好的部署开去。
IGMP Proxy 设备有一个上行接口 (Upstream Interface)、一个或多个下行接口 (Downstream Interface),在上行接口上执行主机 (host) 在 IGMP Proxy 中的所扮演的角色,在下行接口上则执行路由器 (Router) 在 IGMP Proxy 中的所扮演的角色。代理设备自行维护一个所有下行接口的相关状态表项 ( 例如,多播地址、组定时器、过滤模式、源成员列表等 ) 。
下面是 IGMP Proxy 的不同网络拓扑结构的运行情况:
这是一种最为常见的使用情景,通过明确指定上行接口和下行接口,IGMP Proxy 设备则拦截了终端用户的 IGMP 请求并进行相关处理后,再将它转发给上层路由器。在这之间,IGMP Proxy 设备利用收到的 IGMP 包构造出转发所需要的多播路由即可。
当网络拓扑基于冗余目的使用多个 IGMP Proxy 设备时,必须首先选择一个设备去转发多播包,解决的办法非常的简单,选择 IP 地址较小的代理设备实现多播转发。这种办法简单易行,在实现 IGMP Proxy 时候必须支持。
由于 IGMP Proxy 是以简化网络边界设备的多播路由配置为目的,因此图 2-5 所示情况被认为是用户配置错误,为求实现简单,没有强制要求实现“上行环回”的检测,而是将这种配置错误的处理交给了最终的用户;但是多播路由协议所使用的生成树 (STP) 算法解决了该问题。
IGMP Proxy 的实现需要充分利用 Linux 内核对多播的支持,因此,需要对 Linux 内核详细了解。整个实现中内核与应用程序的交互示意以及多播包的处理流程如图 6:
IGMP
IGMPv3 定义于 RFC3376, 增加了对“源过滤”的支持,即系统能够报告它只接收到发往来自特定源地址的某一特定多播组的数据报感兴趣,或者是只对除了某些特定源地址之外的数据感兴趣。这个信息能够被多播路由协议用于避免把某些来自特定源地址的多播数据报发往对它不感兴趣的网络。
IGMPv2 在 RFC2236 中说明,增加了对“低离开延迟”的支持,即多播路由器获知相连的网络中的某一个组中已经没有组成员所花费的时间大大减少。而 v3 增加了对“源过滤”的支持,即系统有能力报告对发往某个特定多播地址的数据报,只希望接收某些特定源的,以支持特定源多播 (source-specific multicast),或者只希望接收除了某些特定源的。 IGMPv3 被设计为能够跟 IGMPv1,IGMPv2 互操作。
IGMPv1 在 RFC1112 中定义,只有查询和报告两种类型的包。
和其它任何 IP 包一样,多播包也是在 ip_rcv() 中接收,正常情况下,会进入 ip_route_input() 中查询路由缓冲系统以确定包的下一步处理方式,ip_check_mc() 会检查计算机是否请求了多播组,这意味着会丢弃所有不受欢迎的多播包 ( 还记得之前的多播组和 MAC 地址的映射关系吗 ) 。接收了多播包之后 ( 即请求了多播组或者该机器是一台多播路由器 ),进入 ip_route_input_mc() ;这里会有不同的路径,对于终端,会进入 ip_local_deliver(),该函数在将这些包转发到应用层去处理;对于多播路由器,首先会用 ip_mr_input() 处理所有的包 ( 包括目的地是本机的包 ) 。 ip_mr_input() 最重要的任务是使用 ipmr_cache_find() 在多播路由缓存中查找缓存项,目的地为本机的包则调用 ip_local_deliver() 处理,而去往其它地方的多播包则在 ip_mr_forward() 中根据找到的多播路由缓存转发。
Linux 对多播路由的支持最初是为了与 mrouted 配合使用,后来则增加了对 PIMd 的支持,其核心数据结构主要是虚拟网络设备 (Virtual Network Devices) 和多播转发缓存 (Multicast Forwarding Cache) 。
struct vif_device { struct net_device *dev; /* Device we are using */ unsigned long bytes_in,bytes_out; unsigned long pkt_in,pkt_out; /* Statistics */ unsigned long rate_limit; /* Traffic shaping (NI) */ unsigned char threshold; /* TTL threshold */ unsigned short flags; /* Control flags */ __be32 local,remote; /* Addresses(remote for tunnels)*/ int link; /* Physical interface index */ }; |
其中,dev 指向所使用的实际的网络设备,bytes_in 和 bytes_out 统计传输的字节数,pkt_in 和 pkt_out 则统计已处理的包的数量,threshold 表明通过该虚拟网络设备发送的包的阈值,而 flags 标示其是网络适配器 (VIFF_SRCRT) 还是 IP 隧道 (VIFF_TUNNEL),local 和 remote 则根据 flags 的取值,可以表示隧道的起点、终点或者网络设备的 IP 地址,link 则指明实际物理网络设备的索引值。
出现 vif_device 的原因要从 Linux 的实现说起,在 Linux 内核中,多播包可以通过两种不同的方式被转发 :(a) 直接通过 LAN 口的网卡 (b) 使用 IP 隧道技术。为了统一内核对多播的支持,抽象出了虚拟网络设备 (VIF) 。在内核中,使用结构 vif_device(include/linux/mroute.h) 代表用 net_device 描述的物理网络设备或者 IP-IP 隧道。在 vif_table(net/ipv4/ipmr.c) 数组中存储所有的虚拟网络设备,而每台计算机能够维护 MAXVIFS 个虚拟网络设备,MAXVIFS 的最大值为 32,由于这是在代码中硬性规定的,因此,导致了一个限制,一台 Linux 机器最多只能连接 MAXVIFS 台多播路由器。
struct mfc_cache { struct mfc_cache *next; /* Next entry on cache line */ __be32 mfc_mcastgrp; /* Group the entry belongs to */ __be32 mfc_origin; /* Source of packet */ vifi_t mfc_parent; /* Source interface */ int mfc_flags; /* Flags on line */ union { struct { unsigned long expires; struct sk_buff_head unresolved;/* Unresolved buffers */ } unres; struct { unsigned long last_assert; int minvif; int maxvif; unsigned long bytes; unsigned long pkt; unsigned long wrong_if; unsigned char ttls[MAXVIFS]; /* TTL thresholds */ } res; } mfc_un; }; |
其中,next 是为了构成哈希表中的单链表; mfc_mcastgrp 和 mfc_origin 两者结合在一起,构成查找该哈希表的键,mfc_origin 是发送方的 IP 地址,而 mfc_mcastgrp 是多播组地址; mfc_parent 指明在 vif_table 数组中的索引以指明所使用的虚拟网络设备来接手去向本 MFC 缓存项的包; mfc_un 是一个联合体,当多播路由守护进程没有结束路由选择时候,会用到 unres,其中的 unresolved 构成一个缓冲队列,同时多播路由选路持续时间则不能超过 expires ;当多播路由守护程序已经设置好该多播路由缓存时,会填充 res,其中的 minvif 和 maxvif 限定虚拟网络设备目前所使用的范围,很显然,maxvif 不能大于 MAXVIFS(32) ; ttls 数组则存储了 TTL 值,用于与多播包的 TTL 值进行比较以限定多播包的阈值。
struct mfc_cache(include/linux/mroute.h) 提供了多播转发所需要的全部信息,实际上,该结构实现了多播路由表。它使用 mfc_cache_array[MFC_LINES] 构成一个以数组下标为哈希键的哈希链表,加快了多播路由的查找,其中需要注意的是 MFC_LINES 定义为 64 。如果要为传入的多播包寻找路由,则会搜寻多播转发缓存哈希表,以匹配合适的缓存项,查找所用的参数是输入端的 IP 地址和多播组地址。
IGMP Proxy 上行接口行为 (Host)
为了和旧版本的路由器兼容,支持 IGMPv3 Proxy 的设备在上行为接口必须满足所有支持 IGMPv3 的主机 (host) 的行为,必须能够在 v1 和 v2 兼容模式下操作。它必须为每一个本地接口维护一个与相连网络兼容模式相关的状态。兼容模式变量有三种可能的状态:IGMPv1,IGMPv2 和 IGMPv3,该变量在每一个接口上都维护一个,其值取决于接口接收到的普通查询的版本,还有接口上的旧版本查询者存在定时器。
兼容模式变量的取值取决于在前一个旧版本查询者存在超时时间内,是否收到旧版本的普通查询。上行接口的兼容模式确定如下:
上行接口兼容模式 (Host) | 定时器状态 |
IGMPv3( 缺省 ) | IGMPv2 查询者存在没有在运行且 IGMPv1 查询者存在也没有在运行 |
IGMPv2 | IGMPv2 查询者存在正在运行且 IGMPv1 查询者存在不在运行 |
IGMPv1 | IGMPv1 查询者存在正在运行 |
当上行接口收到一个查询,该查询会造成它的查询者存在定时器被更新,并且需要相应地调整它的兼容模式,它立即调整兼容模式。当上行接口的兼容模式是 IGMPv3,就在该接口上使用 IGMPv3 协议。当上行接口的兼容模式是 IGMPv2,就在该接口上使用 IGMPv2 协议。当上行接口的兼容模式是 IGMPv1,就在该接口上使用 IGMPv1 协议。一台 IGMPv1 路由器会发送最大响应代码为 0 的普通查询,这时,最大响应时间必须被解释为 100(10 秒 ) 。一台 IGMPv2 路由器发送的普通查询,其最大响应代码被解释为最大响应时间。即,该字段的全范围段是线性的,不存在指数表示法。任何时候上行接口改变了它的兼容模式,会结束掉所有的未完成的响应和重传定时器。
IGMP Proxy 下行接口行为 ( 多播路由器 )
由于组网情况简单,只考虑存在旧版本的组成员的情况。 [ 另外一种存在旧版本查询者的情况不考虑,原因是查询只能是多播路由器发出,那种情况下使用 IGMP Proxy 已经没什么太大必要 ]
IGMPv3 路由器可能被放置在一个网络中,该网络中还有主机没有被升级到 IGMPv3 。为了跟旧版本的主机兼容,IGMPv3 路由器必须在 IGMPv1 或 IGMPv2 兼容模式下运行。 IGMPv3 为每一个组记录维护一个兼容模式。组的兼容模式由组的兼容模式变量来决定,该变量可能是下列几个值之一 :IGMPv1,IGMPv2 和 IGMPv3 。每个组记录都有该变量,它的值取决于该组所接收到成员报告的版本,以及该组旧版本主机存在定时器。
组兼容模式变量的值取决于在上一个旧版本主机存在超时时间内,是否收到一个更旧版本的报告。组兼容模式的设置依据下列规则:
下行接口的组兼容模式 (Router) | 定时器状态 |
IGMPv3( 缺省 ) | IGMPv2 主机存在没有运行且 IGMPv1 主机存在也没有运行 |
IGMPv2 | IGMPv2 主机存在正运行且 IGMPv1 主机存在没有运行 |
IGMPv1 | IGMPv1 主机存在正运行 |
如果一台路由器收到一个报告,该报告造成该路由器的旧版本主机存在定时器被更新并且其兼容模式也要发生相应的变化,它应当立即切换其兼容模式。当组兼容模式是 IGMPv3,路由器为该组使用 IGMPv3 协议。
当组兼容模式是 IGMPv2,路由器在内部把下面的 IGMPv2 消息转化为它们等价的 IGMPv3 模式:
IGMPv2 消息 | IGMPv3 等价形式 |
报告 | IS_EX({}) |
离开 | TO_IN({}) |
IGMPv3 的阻止消息被忽略,在 TO_EX() 消息中,源列表也被忽略,即任何 TO_EX() 消息被处理为 TO_EX({}) 。
当组兼容模式是 IGMPv1,路由器在内部把下面的该组的 IGMPv1,IGMPv2 消息转化为它们的等价的 IGMPv3 形式:
IGMP 消息 | IGMPv3 等价形式 |
IGMPv1 报告 | IS_EX({}) |
IGMPv2 报告 | IS_EX({}) |
代码的基本流程如下:
在主线程中执行必要的初始化,然后启用两个线程。其中的 igmprt_timer_thread 线程用于 IGMP Proxy 中的各种 timer 的管理,而 igmprt_input_thread 线程则在上行接口处理输入的 IGMP 查询,回应报告;在下行接口,则进行查询,并处理返回的报告;然后通过 MRT_ADD_MFC 添加多播路由缓存。
Linux 中多播路由的限制
在 mroute.h 中,Linux 定义了:
MRT_INIT /* Activate the kernel mroute code */ MRT_DONE /* Shutdown the kernel mroute */ MRT_ADD_VIF /* Add a virtual interface */ MRT_DEL_VIF /* Delete a virtual interface */ MRT_ADD_MFC /* Add a multicast forwarding entry */ MRT_DEL_MFC /* Delete a multicast forwarding entry */ MRT_VERSION /* Get the kernel multicast version */ MRT_ASSERT /* Activate PIM assert mode */ MRT_PIM /* enable PIM code */ |
这些选项需要使用 setsockopt() 函数来设置,其中 MRT_INIT 只能设置一次,并且,现在的内核也只能支持一个 IGMP Proxy 上行口;出现这些问题的最主要的原因在于,多播路由其实还没有出现 socket 上的标准配置项,由于 Linux 最初是为了和 mrouted 配合,因此直接使用了它的配置项,现在看来,就算是像 Linux 这种网络功能为优势的 OS,多播的支持仍然有大量的工作需要去做。
本文详细解释了 IGMP Proxy 技术,并提出一个基于 Linux 的实现上的考虑。由于 Linux 在嵌入式设备上的使用,如果能实现该技术,明显能够简化某些不必支持复杂多播路由但又需要支持多播业务的多播支持实现。
赵军,2005 年毕业于华中科技大学电气与电子工程学院,有 3 年基于 Linux 的 Router 开发经验,现在则开发基于 Linux 的高清 / 标清视频解码器,但仍然关注 Linux 在网络方面的发展。 |