目 录
本文的参考分析的源代码版本是2.6.15,我是边学习边总结,学习的过程中得益于Linux论坛(
http://linux.chinaunix.net/bbs/
)上大侠们总结分析的文档,他山之石可以攻玉,学习过程中我也会边学边总结,开源的发展在于共享,我也抛块砖,望能引到玉!
由于自身水平有限,且相关的参考资料较少,因此其中的结论不能保证完全正确,如果在阅读本文的过程中发现了问题欢迎及时与作者联系。也希望能有机会和大家多多交流学习心得!
2
网桥的原理
2.1
桥接的概念
简单来说,桥接就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。
交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的。于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信。
如下图:主机A发送的报文被送到交换机S1的eth0口,由于eth0与eth1、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收到。
2.2
linux
的桥接实现
linux
内核支持网口的桥接(目前只支持以太网接口)。但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发、要么丢弃。小型
的交换机里面只需要一块交换芯片即可,并不需要
CPU
。而运行着
linux
内核的机器本身就是一台主机,有可能就是网络报文的目的地。其收到的报文除了转
发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己消化。
linux
内核是通过一个虚拟的网桥设备来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如下图(摘自
ULNI
):
网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看得到br0,因为桥接是在数据链路层实现的,上层不需要关心桥接的细节。于是协 议栈上层需要发送的报文被送到br0,网桥设备的处理代码再来判断报文该被转发到eth0或是eth1,或者两者皆是;反过来,从eth0或从eth1接 收到的报文被提交给网桥的处理代码,在这里会判断报文该转发、丢弃、或提交到协议栈上层。
而有时候eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收(从而绕过网桥)。
2.3
网桥的功能
概括来说,网桥实现最重要的两点:
1.
MAC学习:学习MAC地址,起初,网桥是没有任何地址与端口的对应关系的,它发送数据,还是得想HUB一样,但是每发送一个数据,它都会关心数据包的来源MAC是从自己的哪个端口来的,由于学习,建立地址-端口的对照表(CAM表)。
2.
报文转发:每发送一个数据包,网桥都会提取其目的MAC地址,从自己的地址-端口对照表(CAM表)中查找由哪个端口把数据包发送出去。
3
网桥的配置
在
Linux
里面使用网桥非常简单,仅需要做两件事情就可以配置了。其一是在编译内核里把
CONFIG_BRIDGE
或
CONDIG_BRIDGE_MODULE
编译选项打开;其二是安装
brctl
工具。第一步是使内核协议栈支持网桥,第二步是安装用户空间工具,通过一系列的
ioctl
调用来配置网桥。下面以一个相对简单的实例来贯穿全文,以便分析代码。
Linux机器有4个网卡,分别是eth0~eth4,其中eth0用于连接外网,而eth1, eth2, eth3都连接到一台PC机,用于配置网桥。只需要用下面的命令就可以完成网桥的配置:
Brctl addbr br0 (建立一个网桥br0, 同时在Linux内核里面创建虚拟网卡br0)
Brctl addif br0 eth1
Brctl addif br0 eth2
Brctl addif br0 eth3 (分别为网桥br0添加接口eth1, eth2和eth3)
其中br0作为一个网桥,同时也是虚拟的网络设备,它即可以用作网桥的管理端口,也可作为网桥所连接局域网的网关,具体情况视你的需求而定。要使用br0接口时,必需为它分配IP地址。为正常工作,PC1, PC2,PC3和br0的IP地址分配在同一个网段。
在内核,网桥是以模块的方式存在,注册源码路径:\net\brige\br.c:
4.1 初始化
|
4.2 新建网桥
前面说到通过brctl addbr br0命令建立网桥,此处用户控件调用的brctl命令最终对应到内核中的br_ioctl_deviceless_stub处理函数:
buf |
在这里,我们传入的cmd为SIOCBRADDBR.转入br_add_bridge(buf)中进行:
rtnl_lock err2 |
网桥是一个虚拟的设备,它的注册跟实际的物理网络设备注册是一样的。我们关心的是网桥对应的net_device结构是什么样的,继续跟踪进new_bridge_dev:
spin_lock_init br br_stp_timer_init |
在br_dev_setup中还做了一些另外在函数指针初始化:
|
4.3
添加删除端口
仅仅创建网桥,还是不够的。实际应用中的网桥需要添加实际的端口(即物理接口),如例子中的
eth1, eth2
等。应用程序在使用
ioctl
来为网桥增加物理接口,对应内核函数
br_dev_ioctl
的代码和分析如下:
pr_debug |
下面分析具体的添加删除函数add_del_if:
dev dev_put |
对应的添加删除函数分别为:br_add_if, br_del_if;
br_add_if:
|
br_del_if:
br_sysfs_removeif spin_lock_bh |
5
网桥数据结构
网桥最主要有三个数据结构:struct net_bridge,struct net_bridge_port,struct net_bridge_fdb_entry,他们之间的关系如下图:
展开来如下图:
说明:
1.
其中最左边的
net_device
是一个代表网桥的虚拟设备结构,它关联了一个
net_bridge
结构,这是网桥设备所特有的数据结构。
2.
在
net_bridge
结构中,
port_list
成员下挂一个链表,链表中的每一个节点(
net_bridge_port
结构)关联到一个真实的网口设
备的
net_device
。网口设备也通过其
br_port
指针做反向的关联(那么显然,一个网口最多只能同时被绑定到一个网桥)。
3.
net_bridge
结构中还维护了一个
hash
表,是用来处理地址学习的。当网桥准备转发一个报文时,以报文的目的
Mac
地址为
key
,如果可以在
hash
表中索引到一个
net_bridge_fdb_entry
结构,通过这个结构能找到一个网口设备的
net_device
,于是报文就应该从这个网
口转发出去;否则,报文将从所有网口转发。
各个结构体具体内容如下:
struct net_bridge
u16 root_port |
2.
struct net_bridge_port
|
3. struct net_bridge_fdb_entry
atomic_t use_count |
6
网桥数据库的维护
这里所说的网桥数据库指的是CAM表,即struct net_bridge结构中的hash表,数据库的维护对应的是对结构struct net_bridge_fdb_entry的操作;
众所周知,网桥需要维护一个
MAC
地址
–
端口映射表,端口是指网桥自身提供的端口,而
MAC
地址是指与端口相连的另一端的
MAC
地址。当网桥收到一个报文时,先获取它的源
MAC
,更新数据库,然后读取该报文的目标
MAC
地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端口以外的所有端口转发。
6.1
数据库的创建和销毁
数据库使用
kmem_cache_create
函数进行创建,使用
kmem_cache_desctory
进行销毁。路径:
[/net/bridge/br_fdb.c]:
|
6.2
数据库更新
当网桥收到一个数据包时,它会获取该数据的源
MAC
地址,然后对数据库进行更新。如果该
MAC
地址不在数库中,则创新一个数据项。如果存在,更新它的年龄。数据库使用
hash
表的结构方式,便于高效查询。下面是
hash
功能代码的分析:
路径:[/net/bridge/br_fdb.c]
rcu_read_lock |
6.3
创建数据项
在更新函数里面已为某一
MAC
找到了它所属于的
Hash
链表,因此,创建函数只需要在该链上添加一个数据项即可。
fdb |
6.4
查找数据项
查找分两种:一种是数据项更新时候的查找,另一种是转发报文时候查找,两者区别是转发时查找需要判断
MAC
地址是否过期,即我们常说的
MAC
老化;更新时则不用判断;
网桥更新一
MAC
地址时,不管该地址是否已经过期了,只需遍历该
MAC
地址对应的
Hash
链表,然后更新年龄,此时它肯定不过期了。
网桥要转发数据时,除了要找到该目标
MAC
的出口端口外,还要判断该记录是否过期了。
更新时查找:
|
转发时查找:
|
比较一下,转发时多了一个函数处理:has_expired,
Has_expired
函数来决定该数据项是否是过期的,代码如下:
|
6.5
MAC
地址过期清理
桥建立时设置一个定时器,循环检测,如果发现有过期的MAC,则清除对应的数据项,MAC地址过期清除由函数br_fdb_cleanup实现:
spin_lock_bh |
7
网桥数据包的处理流程
网桥处理包遵循以下几条原则:
1.
在一个接口上接收的包不会再在那个接口上发送这个数据包;
2.
每个接收到的数据包都要学习其源地址;
3.
如果数据包是多播或广播包,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包,如果上层协议栈对多播包感兴趣,则需要把数据包提交给上层协议栈;
4.
如果数据包的目的MAC地址不能再CAM表中找到,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包;
5.
如果能够在CAM表中查询到目的MAC地址,则在特定的端口上发送这个数据包,如果发送端口和接收端口是同一端口则不发送;
网桥在整个网络子系统中处理可用下列简图说明:
网络数据包在软终端处理时会进行网桥部分处理,大致的处理流程如下(处理函数调用链):
7.1
netif_receive_skb
netif_recerve_skb函数主要做三件事情:
1.
如果有抓包程序(socket)需要skb,则将skb复制给他们;
2.
处理桥接,即如果开启了网桥,进行网桥处理;
3.
将skb交给网络层;
orig_dev __get_cpu_var skb pt_prev rcu_read_lock ret skb handle_diverter out |
7.2
Br_handle_frame
1.
如果
skb
的目的
Mac
地址与接收该
skb
的网口的
Mac
地址相同,则结束桥接处理过程(返回到
net_receive_skb
函数后,这个
skb
会最终
被提交给网络层);
2.
否则,调用到
br_handle_frame_finish
函数将报文转发,然后释放
skb
(返回到
net_receive_skb
函数后,这个
skb
就
不会往网络层提交了);
NF_HOOK err |
7.3
Br_handle_frame_finish
out |
7.4
Br_pass_frame_up
在上个函数Br_handle_frame_finish中如果报文是需要发往本地协议栈处理的,则由函数Br_pass_frame_up实现:
br indev NF_HOOK |
这段代码非常简单,对
net_bridge
的数据统计进行更新以后,再更新
skb->dev
,最后通过
NF_HOOK
在
NF_BR_LOCAL_IN
挂接点上调用回了
netif_receive_skb
;
在
netif_receive_skb
函数中,调用了
handle_bridge
函数,重新触发了网桥处理流程,现在发往网桥虚拟设备的数据包又回到了
netif_receive_skb,
那么网桥的处理过程会不会又被调用呢?在
linux/net/bridge/br_if.c
里面可以看到
br_add_if
函数,实际上的操作是将某一网口加入网桥组,这个函数调用了
new_nbp(br, dev);
用以填充
net_bridge
以及
dev
结构的重要成员,里面将
dev->br_port
设定为一个新建的
net_bridge_port
结构,而上面的
br_pass_frame_up
函数将
skb->dev
赋成了
br->dev,
实际上
skb->dev
变成了网桥建立的虚拟设备,这个设备是网桥本身而不是桥组的某一端口,系统没有为其调用
br_add_if
,所以这个
net_device
结构的
br_port
指针没有进行赋值;
br_port
为空,不进入网桥处理流程
;从而进入上层协议栈处理;
kfree_skb |
7.6
__br_forward
indev NF_HOOK |
7.7
Br_forward_finish
|
7.8
Br_dev_queue_push_xmit
dev_queue_xmit |
7.9
报文处理总结
进入桥的数据报文分为几个类型,桥对应的处理方法也不同:
1.
报文是本机发送给自己的,桥不处理,交给上层协议栈;
2.
接收报文的物理接口不是网桥接口,桥不处理,交给上层协议栈;
3.
进入网桥后,如果网桥的状态为Disable
,则将包丢弃不处理;
4.
报文源地址无效(广播,多播,以及00:00:00:00:00:00
),丢包;
5.
如果是STP
的BPDU包,进入STP处理,处理后不再转发,也不再交给上层协议栈;
6.
如果是发给本机的报文,桥直接返回,交给上层协议栈,不转发;
7.
需要转发的报文分三种情况:
1)
广播或多播,则除接收端口外的所有端口都需要转发一份;
2)
单播并且在CAM
表中能找到端口映射的,只需要网映射端口转发一份即可;
3)
单播但找不到端口映射的,则除了接收端口外其余端口都需要转发;
8
参考文献
1.
http://hi.baidu.com/_kouu/blog/item/ad2abf3ffa61cf3170cf6cd7.html
2.
http://hi.baidu.com/jrckkyy/blog/item/3bedbef37234d0c70b46e08b.html
3.
http://blog.csdn.net/linyt/archive/2010/01/15/5191512.aspx
4.
http://www.loosky.net/?p=307
5.
http://blog.csdn.net/zhaodm/archive/2006/12/25/1460041.aspx
6.
http://blog.chinaunix.net/u/12313/showart_246678.html