原文连接:http://hi.baidu.com/_kouu/item/25787d38efec56637c034bd0linux
什么是桥接?
简单来讲,桥接就是把一台机器上的若干个网络接口“链接”起来。其结果是,其中一个网口收到的报文会被复制给其余网口并发送出去。以使得网口之间的报文可以互相转发。 交换机就是这样一个设备,它有若干个网口,而且这些网口是桥接起来的。因而,与交换机相连的若干主机就可以经过交换机的报文转发而互相通讯。
以下图:主机A发送的报文被送到交换机S1的eth0口,因为eth0与eth一、eth2桥接在一块儿,故而报文被复制到eth1和eth2,而且发送出去,而后被主机B和交换机S2接收到。而S2又会将报文转发给主机C、D。
交换机在报文转发的过程当中并不会篡改报文数据,只是作原样复制。然而桥接却并非在物理层实现的,而是在数据链路层。交换机可以理解数据链路层的报文,因此实际上桥接却又不是单纯的报文转发。 交换机会关心填写在报文的数据链路层头部中的Mac地址信息(包括源地址和目的地址),以便了解每一个Mac地址所表明的主机都在什么位置(与本交换机的哪一个网口相连)。在报文转发时,交换机就只须要向特定的网口转发便可,从而避免没必要要的网络交互。这个就是交换机的“地址学习”。可是若是交换机遇到一个本身未学习到的地址,就不会知道这个报文应该从哪一个网口转发,则只好将报文转发给全部网口(接收报文的那个网口除外)。 好比主机C向主机A发送一个报文,报文来到了交换机S1的eth2网口上。假设S1刚刚启动,尚未学习到任何地址,则它会将报文转发给eth0和eth1。同时,S1会根据报文的源Mac地址,记录下“主机C是经过eth2网口接入的”。因而当主机A向C发送报文时,S1只须要将报文转发到eth2网口便可。而当主机D向C发送报文时,假设交换机S2将报文转发到了S1的eth2网口(实际上S2也多半会由于地址学习而不这么作),则S1会直接将报文丢弃而不作转发(由于主机C就是从eth2接入的)。
然而,网络拓扑不多是永不改变的。假设咱们将主机B和主机C换个位置,当主机C发出报文时(无论发给谁),交换机S1的eth1口收到报文,因而交换机S1会更新其学习到的地址,将原来的“主机C是经过eth2网口接入的”改成“主机C是经过eth1网口接入的”。 可是若是主机C一直不发送报文呢?S1将一直认为“主机C是经过eth2网口接入的”,因而将其余主机发送给C的报文都从eth2转发出去,结果报文就发丢了。因此交换机的地址学习须要有超时策略。对于交换机S1来讲,若是距离最后一次收到主机C的报文已通过去必定时间了(默认为5分钟),则S1须要忘记“主机C是经过eth2网口接入的”这件事情。这样一来,发往主机C的报文又会被转发到全部网口上去,而其中从eth1转发出去的报文将被主机C收到。
linux的桥接实现
相关模型 linux内核支持网口的桥接(目前只支持以太网接口)。可是与单纯的交换机不一样,交换机只是一个二层设备,对于接收到的报文,要么转发、要么丢弃。小型的交换机里面只须要一块交换芯片便可,并不须要CPU。而运行着linux内核的机器自己就是一台主机,有可能就是网络报文的目的地。其收到的报文除了转发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被本身消化。 linux内核是经过一个虚拟的网桥设备来实现桥接的。这个虚拟设备能够绑定若干个以太网接口设备,从而将它们桥接起来。以下图(摘自ULNI):
网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来讲,只看获得br0,由于桥接是在数据链路层实现的,上层不须要关心桥接的细节。因而协议栈上层须要发送的报文被送到br0,网桥设备的处理代码再来判断报文该被转发到eth0或是eth1,或者二者皆是;反过来,从eth0或从eth1接收到的报文被提交给网桥的处理代码,在这里会判断报文该转发、丢弃、或提交到协议栈上层。 而有时候eth0、eth1也可能会做为报文的源地址或目的地址,直接参与报文的发送与接收(从而绕过网桥)。
相关数据结构 要使用桥接功能,咱们须要在编译内核时指定相关的选项,并让内核加载桥接模块。而后经过“brctl addbr {br_name}”命令新增一个网桥设备,最后经过“brctl addif {eth_if_name}”命令绑定若干网络接口。完成这些操做后,内核中的数据结构关系以下图所示(摘自ULNI):
其中最左边的net_device是一个表明网桥的虚拟设备结构,它关联了一个net_bridge结构,这是网桥设备所特有的数据结构。 在net_bridge结构中,port_list成员下挂一个链表,链表中的每个节点(net_bridge_port结构)关联到一个真实的网口设备的net_device。网口设备也经过其br_port指针作反向的关联(那么显然,一个网口最多只能同时被绑定到一个网桥)。 net_bridge结构中还维护了一个hash表,是用来处理地址学习的。当网桥准备转发一个报文时,以报文的目的Mac地址为key,若是能够在hash表中索引到一个net_bridge_fdb_entry结构,经过这个结构能找到一个网口设备的net_device,因而报文就应该从这个网口转发出去;不然,报文将从全部网口转发。
接收过程 在《linux网络报文接收发送浅析》一文中咱们看到,网口设备接收到的报文最终经过net_receive_skb函数被网络协议栈所接收。
net_receive_skb(skb); 这个函数主要作三件事情: 一、若是有抓包程序须要skb,将skb复制给它们; 二、处理桥接; 三、将skb提交给网络层;
这里咱们只关心第2步。那么,如何判断一个skb是否须要作桥接相关的处理呢?skb->dev指向了接收这个skb的设备,若是这个net_device的br_port不为空(它指向一个net_bridge_port结构),则表示这个net_device正在被桥接,而且经过net_bridge_port结构中的br指针能够找到网桥设备的net_device结构。因而调用到br_handle_frame函数,让桥接的代码来处理这个报文;
br_handle_frame(net_bridge_port, skb); 若是skb的目的Mac地址与接收该skb的网口的Mac地址相同,则结束桥接处理过程(返回到net_receive_skb函数后,这个skb会最终被提交给网络层); 不然,调用到br_handle_frame_finish函数将报文转发,而后释放skb(返回到net_receive_skb函数后,这个skb就不会往网络层提交了);
br_handle_frame_finish(skb); 首先经过br_fdb_update函数更新网桥设备的地址学习hash表中对应于skb的源Mac地址的记录(更新时间戳及其所指向的net_bridge_port结构); 若是skb的目的地址与本机的其余网口的Mac地址相同(可是与接收该skb的网口的Mac地址不一样,不然在上一个函数就返回了),就调用br_pass_frame_up函数,该函数会将skb->dev替换成网桥设备的dev,而后再调用netif_receive_skb来处理这个报文。这下子netif_receive_skb函数被递归调用了,可是这一次却不会再触发网桥的相关处理函数,由于skb->dev已经被替换,skb->dev->br_port已是空了。因此这一次netif_receive_skb函数最终会将skb提交给网络层; 不然,经过__br_fdb_get函数在网桥设备的地址学习hash表中查找skb的目的Mac地址所对应的dev,若是找到(且经过其时间戳认定该记录未过时),则调用br_forward将报文转发给这个dev;而若是找不到则调用br_flood_forward进行转发,该函数会遍历网桥设备中的port_list,找到每个绑定的dev(除了与skb->dev相同的那个),而后调用br_forward将其转发;
br_forward(net_bridge_port, skb); 将skb->dev替换成将要进行转发的dev,而后调用br_forward_finish,然后者又会调用br_dev_queue_push_xmit。 最终,br_dev_queue_push_xmit会调用dev_queue_xmit将报文发送出去(见《linux网络报文接收发送浅析》)。注意,此时skb->dev已经被替换成进行转发的dev了,报文会从这个网口被转发出去;
发送过程 在《linux网络报文接收发送浅析》一文中咱们看到,协议栈上层须要发送报文时,调用dev_queue_xmit(skb)函数。若是这个报文须要经过网桥设备来发送,则skb->dev指向一个网桥设备。网桥设备没有使用发送队列(dev->qdisc为空),因此dev_queue_xmit将直接调用dev->hard_start_xmit函数,而网桥设备的hard_start_xmit等于函数br_dev_xmit;
br_dev_xmit(skb, dev); 经过__br_fdb_get函数在网桥设备的地址学习hash表中查找skb的目的Mac地址所对应的dev,若是找到,则调用br_deliver将报文发送给这个dev;而若是找不到则调用br_flood_deliver进行发送,该函数会遍历网桥设备中的port_list,找到每个绑定的dev,而后调用br_deliver将其发送(此处逻辑与以前的转发很像);
br_deliver(net_bridge_port, skb); 这个函数的逻辑与以前转发时调用的br_forward很像。先将skb->dev替换成将要进行转发的dev,而后调用br_forward_finish。如前面所述,br_forward_finish又会调用到br_dev_queue_push_xmit,后者最终调用dev_queue_xmit将报文发送出去。
以上过程忽略了对于广播或多播Mac地址的处理,若是Mac地址是广播或多播地址,就向全部绑定的dev转发报文就好了。
另外,关于地址学习的过时记录,专门有一个定时器周期性地调用br_fdb_cleanup函数来将它们清除。
生成树协议
对于网桥来讲,报文的转发、地址学习其实都是很简单的事情。在简单的网络环境中,这就已经足够了。 而对于复杂的网络环境,每每须要对数据通路作必定的冗余,以便当网络中某个交换机出现故障、或交换机的某个网口出现故障时,整个网络还可以正常使用。 那么,咱们假设在上面的网络拓扑中增长一条冗余的链接,看看会发生什么事情吧。
假设交换机S1和S2都是刚刚启动(没有学习到任何地址),此时主机C向B发送一个报文。交换机S2的eth2口收到报文,并将其转发到eth0、eth一、eth3,而且记录下“主机C由eth2接入”。交换机S1在其eth2和eth3口都会收到报文,eth2口收到的报文又会从eth3口(及其余口)转发出去、eth3口收到的报文也会从eth2口(及其余口)转发出去。因而交换机S2的eth0、eth1口又将再次收到这个报文,报文的源地址仍是主机C。因而S2相继更新学习到的地址,记录下“主机C由eth0接入”,而后又更新为“主机C由eth1接入”。而后报文又继续被转发给交换机S1,S1又会转发回S2。造成一个回路,周而复始,而且每一次轮回还会致使报文被复制给其余网口,最终造成网络风暴。整个网络可能就瘫痪了。 可见,咱们以前讨论的交换机是不能在这样的带有环路的拓扑中使用的。可是若是要想给网络添加必定的冗余链接,则又一定会存在环路,这该怎么办呢? IEEE规范定义了生成树协议(STP),若是网络拓扑中的交换机支持这种协议,则它们会经过BPUD报文(网桥协议数据单元)进行通讯,相互协调,暂时阻塞掉某些交换机的某些网口,使得网络拓扑不存在环路,成为一个树型结构。而当网络中某些交换机出现故障,这些被暂时阻塞掉的网口又会从新启用,以保持整个网络的连通性。
由一个带有环路的图生成一棵树的算法是很简单的,可是,正所谓“不识庐山真面目,只缘身在此山中”,网络中的每一台交换机都不知道确切的网络拓扑,而且网络拓扑还可能动态地改变。要经过交换机间的信息传递(传递BPUD报文)来生成这么一棵树,那就不是一件简单的事情了。来看看生成树协议是怎么作到的吧。
肯定树根 要生成一棵树,第一步是肯定树根。协议规定,只有做为树根节点的交换机才能发送BPUD报文,以协调其余交换机。当一台交换机启动时,它不知道谁是树根,则他会把本身就看成树根,从它的各个网口发出BPUD报文。 BPUD报文能够说是代表发送者身份的报文,里面含有一个“root_id”,也就是发送者的ID(发送者都认为本身就是树根)。这个ID由两部份组成,优先级+Mac地址。ID越小则该交换机越重要,越应该被任命为树根。ID中的优先级是由网络管理员来指定的,固然性能越好的交换机应该被指定为越高的优先级(即越小的值)。两个交换机的ID比较,首先比较的就是优先级。而若是优先级相同,则比较其Mac地址。就比如两我的地位至关,只好按姓氏笔划排列了。而交换机的Mac地址是全世界惟一的,因此交换机ID不会相同。
一开始,各个交换机都自觉得是地认为本身是树根,都发出了BPUD报文,并在其中代表了本身的身份。而各个交换机天然也会收到来自于其余交换机的BPUD报文,若是发现别人的ID更小(优先级更高),这时,交换机才意识到“天外有天、人外有人”,因而中止本身愚昧的“自称树根”的举动。而且将收到的带有更高优先级的BPUD报文转发,让其余人也知道有这么个高优先级的交换机存在。 最终,全部交换机会达成共识,知道网络中有一个ID为XXXX的家伙,他才是树根。
肯定上行口 肯定了树根,也就肯定了网络拓扑的最顶层。而其余交换机则须要肯定本身的某个网口,做为其向上(树根方向)转发报文的网口(上行口)。想想,若是一个交换机有多个上行口,则网络拓扑必然会存在回路。因此一个交换机的上行口有且只有一个。 那么这个惟一的上行口怎么肯定呢?取各个网口中,到树根的开销最小的那一个。
上面说到,树根发出的BPUD报文会被其余交换机所转发,最终每一个交换机的某些网口会收到这个BPUD。BPUD中还有这么三个字段,“到树根的开销”、“交换机ID”、“网口ID”。交换机在转发BPUD时,会更新这三个字段,把“交换机ID”更新为本身的ID,把“网口ID”更新为转发该BPUD的那个网口的编号,而“到树根的开销”则被增长必定的值(根据实际的转发开销,由交换机本身决定。多是个大概值)。树根最初发出的BPUD,“到树根的开销”为0。每转发一次,该字段就被增长相应的开销值。 假设树根发出了一个BPUD,因为转发,一个交换机的同一个网口可能会屡次收到这个BPUD报文的复本。这些复本可能通过了不一样的转发路径才来到这个网口,所以有着不一样的“到树根的开销”、“交换机ID”、“网口ID”。这三个字段的值越小,表示按照该BPUD转发的路径,到达树根的开销越小,就认为该BPUD的优先级越高(其实后两个字段也只是启到“按姓氏笔划排列”的做用)。交换机会记录下在其每个网口上收到的优先级最高的BPUD,而且只有当一个网口当前收到的这个BPUD比它所记录的BPUD(也就是曾经收到的优先级最高的BPUD)的优先级还高时,这个交换机才会将该BPUD报文由其余网口转发出去。最后,比较各个网口所记录的BPUD的优先级,最高者被做为交换机的上行口。
肯定须要被阻塞的下行口 交换机除了其上行口以外的其余网口都是下行口。交换机的上行路径不会存在环路,由于交换机都只有惟一的上行口。 而不一样交换机的多个下行口有多是相互连通的,会造成环路。(这些下行口也不必定是直接相连,多是由物理层的转发设备将多个交换机的多个下行口连在一块儿。)生成树协议的最后一道工序就是在这一组相互连通的下行口中,选择一个让其转发报文,其余网口都被阻塞。由此消除存在的环路。而那些没有与其余下行口相连的下行口则不在考虑之列,它们不会引发环路,都照常转发。 不过,既然下行口两两相连会产生回路,是否是把这些相连的下行口都阻塞就行了呢?前面提到过可能存在物理层设备将多个网口同时连在一块儿的状况(如集线器Hub,尽管如今已经不多用了),如图:
假设交换机S2的eth2口和交换机S3的eth1口是互相连通的两个下行口,若是武断地将这两网口都阻塞,则主机E就被断网了。因此,这两个网口还必须留下一个来提供报文转发服务。
那么对于一组相互连通的下行口,该选择谁来做为这个惟一能转发报文的网口呢? 上面说到,每一个交换机在收到优先级最高的BPUD时,都会将其转发。转发的时候,“到树根的开销”、“交换机ID”、“网口ID”都会被更新。因而对于一组相互连通的下行口,从谁那里转发出来的BPUD优先级最高,就说明从它到达树根的开销最小。因而这个网口就能够继续转发报文,而其余网口都被阻塞。 从实现上来讲,每一个网口需记录下本身转发出去的BPUD的优先级是多少。若是其没有收到比该优先级更高的BPUD(没有与其余下行口相连,收不到BPUD;或者与其余下行口相连,可是收到的BPUD优先级较低),则网口能够转发;不然网口被阻塞。
通过交换机之间的这一系列BPUD报文交换,生成树完成。然而网络拓扑也可能由于一些人为因素(如网络调整)或非人为因素(如交换机故障)而发生改变。因而生成树协议中还定义了不少机制来检测这种改变,然后触发新一轮的BPUD报文交换,造成新的生成树。这个过程就再也不赘述了。算法