第一部份 源码框架
一、 网桥原理
传统的中继器,如HUB,是一个单纯的物理层设备,它将每一个收到的数据包,在其所有的端口上广播,由接收主机来判断这个数据包是否是给自己的。这样,网络资源被极大的浪费掉了。
网桥之所以不同于中继器,主要在于其除了有中继的作用外,还有一个更重要的作用,就是学习MAC地址,然后根据每个数据包的目的MAC与自身端口的对应,从关联端口发送数据,而不完全地在整个网段中进行广播。所以,网桥的实现中,有两个关键点:
1、 学习MAC地址,起初,网桥是没有任何地址与端口的对应关系的,它发送数据,还是得想HUB一样,但是每发送一个数据,它都会关心数据包的来源MAC是从自己的哪个端口来的,由于学习,建立地址-端口的对照表(CAM表)。
2、 每发送一个数据包,网桥都会提取其目的MAC地址,从自己的地址-端口对照表(CAM表)中查找由哪个端口把数据包发送出去。
二、 Linux网桥源码的实现
1、 调用
在src/net/core/dev.c的软中断函数static void net_rx_action(struct softirq_action *h)中(line 1479)
#if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE)
if (skb->dev->br_port != NULL &&
br_handle_frame_hook != NULL) {
handle_bridge(skb, pt_prev);
dev_put(rx_dev);
continue;
}
#endif
如果定义了网桥或网桥模块,则由handle_bridge函数处理
skb->dev->br_port :接收该数据包的端口是网桥端口组的一员,如果接收当前数据包的接口不是网桥的某一物理端口,则其值为NULL;
br_handle_frame_hook :定义了网桥处理函数
这段代码将数据包进行转向,转向的后的处理函数是钩子函数br_handle_frame_hook,在此之前,handle_bridge函数还要处理一些其它的事情:
static __inline__ int handle_bridge(struct sk_buff *skb,
struct packet_type *pt_prev)
{
int ret = NET_RX_DROP;
if (pt_prev) {
if (!pt_prev->data)
ret = deliver_to_old_ones(pt_prev, skb, 0);
else {
atomic_inc(&skb->users);
ret = pt_prev->func(skb, skb->dev, pt_prev);
}
}
br_handle_frame_hook(skb);
return ret;
}
pt_prev用于在共享SKB的时候提高效率,这并不是本文关心的重点,贴子http://www.linuxforum.net/forum/ ... mp;o=all&fpart= 具有很高的参考介值,我就不重复了。
handle_bridge函数最后将控制权交由到了br_handle_frame_hook的手上。
2、 钩子函数的注册
br_handle_frame_hook用于网桥的处理,在网桥的初始化函数中(net/bridge/br.c):
static int __init br_init(void)
{
printk(KERN_INFO "NET4: Ethernet Bridge 008 for NET4.0/n");
br_handle_frame_hook = br_handle_frame;
br_ioctl_hook = br_ioctl_deviceless_stub;
#if defined(CONFIG_ATM_LANE) || defined(CONFIG_ATM_LANE_MODULE)
br_fdb_get_hook = br_fdb_get;
br_fdb_put_hook = br_fdb_put;
#endif
register_netdevice_notifier(&br_device_notifier);
return 0;
}
初始化函数中指明了钩子函数实际上指向的是br_hanlde_frame
3、br_handle_frame(br_input.c)
/*网桥处理函数*/
void br_handle_frame(struct sk_buff *skb)
{
struct net_bridge *br;
unsigned char *dest;
struct net_bridge_port *p;
/*获取目的MAC地址*/
dest = skb->mac.ethernet->h_dest;
/*skb->dev->br_port用于指定接收该数据包的端口,若不是属于网桥的端口,则为NULL*/
p = skb->dev->br_port;
if (p == NULL) /*端口不是网桥组端口中*/
goto err_nolock;
/*本端口所属的网桥组*/
br = p->br;
/*加锁,因为在转发中需要读CAM表,所以必须加读锁,避免在这个过程中另外的内核控制路径(如多处理机上另外一个CPU上的系统调用)修改CAM表*/
read_lock(&br->lock);
if (skb->dev->br_port == NULL) /*前面判断过的*/
goto err;
/*br->dev是网桥的虚拟网卡,如果它未UP,或网桥DISABLED,p->state实际上是桥的当前端口的STP计算判断后的状态*/
if (!(br->dev.flags & IFF_UP) ||
p->state == BR_STATE_DISABLED)
goto err;
/*源MAC地址为255.X.X.X,即源MAC是多播或广播,丢弃之*/
if (skb->mac.ethernet->h_source[0] & 1)
goto err;
/*众所周之,网桥之所以是网桥,比HUB更智能,是因为它有一个MAC-PORT的表,这样转发数据就不用广播,而查表定端口就可以了
每次收到一个包,网桥都会学习其来源MAC,添加进这个表。Linux中这个表叫CAM表(这个名字是其它资料上看的)。
如果桥的状态是LEARNING或FORWARDING(学习或转发),则学习该包的源地址skb->mac.ethernet->h_source,
将其添加到CAM表中,如果已经存在于表中了,则更新定时器,br_fdb_insert完成了这一过程*/
if (p->state == BR_STATE_LEARNING ||
p->state == BR_STATE_FORWARDING)
br_fdb_insert(br, p, skb->mac.ethernet->h_source, 0);
/*
* STP协议的BPDU包的目的MAC采用的是多播目标MAC地址:
* 01-80-c2-00-00-00(Bridge_group_addr:网桥组多播地址),这里先判断网桥是否
* 开启了STP(由用户层来控制,如brctl),如果开启了,则比较目的地址前5位
* 是否与多播目标MAC地址相同:
* (!memcmp(dest, bridge_ula, 5)
* 如果相同,如果地址第6位非空
* !(dest[5] & 0xF0))
* 那么这确定是一个STP的BPDU包,则跳转到handle_special_frame,将处理权
* 将给函数br_stp_handle_bpdu
*/
if (br->stp_enabled &&
!memcmp(dest, bridge_ula, 5) &&
!(dest[5] & 0xF0))
goto handle_special_frame;
/*处理钩子函数,然后转交br_handle_frame_finish函数继续处理*/
if (p->state == BR_STATE_FORWARDING) {
NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
br_handle_frame_finish);
read_unlock(&br->lock);
return;
}
err:
read_unlock(&br->lock);
err_nolock:
kfree_skb(skb);
return;
handle_special_frame:
if (!dest[5]) {
br_stp_handle_bpdu(skb);
return;
}
kfree_skb(skb);
}
可见,这个函数中有三个重要的地方:
1、 地址学习:br_fdb_insert
2、 STP的处理:br_stp_handle_bpdu
3、 br_handle_frame_finish,我们还没有查CAM表,转发数据呢……
我们先来看网桥的进一步处理br_handle_frame_finish,地址学习等内容,后面再来分析。
4、br_handle_frame_finish
static int br_handle_frame_finish(struct sk_buff *skb)
{
struct net_bridge *br;
unsigned char *dest;
struct net_bridge_fdb_entry *dst;
struct net_bridge_port *p;
int passedup;
/*前面基本相同*/
dest = skb->mac.ethernet->h_dest;
p = skb->dev->br_port;
if (p == NULL)
goto err_nolock;
br = p->br;
read_lock(&br->lock);
if (skb->dev->br_port == NULL)
goto err;
passedup = 0;
/*
* 如果网桥的虚拟网卡处于混杂模式,那么每个接收到的数据包都需要克隆一份
* 送到AF_PACKET协议处理体(网络软中断函数net_rx_action中ptype_all链的
* 处理)。
*/
if (br->dev.flags & IFF_PROMISC) {
struct sk_buff *skb2;
skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2 != NULL) {
passedup = 1;
br_pass_frame_up(br, skb2);
}
}
/*
* 目的MAC为广播或多播,则需要向本机的上层协议栈传送这个数据包,这里
* 有一个标志变量passedup,用于表示是否传送过了,如果已传送过,那就算了
*/
if (dest[0] & 1) {
br_flood_forward(br, skb, !passedup);
if (!passedup)
br_pass_frame_up(br, skb);
goto out;
}
/*
* 用户层常常需要用到一个虚拟的地址来管理网桥,如果目的地址非常,且为本
* 地址地址,则交由上层函数处理
*/
if (dst != NULL && dst->is_local) {
if (!passedup)
br_pass_frame_up(br, skb);
else
kfree_skb(skb);
br_fdb_put(dst);
goto out;
}
/*查询CAM表,如果查到表了,转发之*/
if (dst != NULL) {
br_forward(dst->dst, skb);
br_fdb_put(dst);
goto out;
}
/*如果表里边查不到,那么只好学习学习HUB了……*/
br_flood_forward(br, skb, 0);
out:
read_unlock(&br->lock);
return 0;
err:
read_unlock(&br->lock);
err_nolock:
kfree_skb(skb);
return 0;
}
在这个函数中,涉及到两个重要方面:
1、 查表:br_forward
2、 网桥数据转发:br_fdb_put。
另外,网桥的处理中,还涉及到内核中一些重要的数据结构:
对Linux上所有接口进行网桥划分,可以把一组端口划分到一个网桥之中,同时一个系统上
允许有多个网桥。内核描述一个网桥,使用了struct net_bridge结构:
struct net_bridge
{
struct net_bridge *next; //下一个网桥
rwlock_t lock; //读写锁
struct net_bridge_port *port_list; //桥组中的端口列表
/*网桥都会有一个虚拟设备用来进行管理,就是它了。说到这里,我想到
了以前一个没有解决的问题:对网桥管理IP配置后,发现其虚拟的MAC地址是动态生成的,取的是桥组中某一个物理端口的MAC地址(好像是
第一个),这样,如果远程管理时就有麻烦:如果你动态调整网桥中的端口,如删除某个网卡出去,用于管理的虚拟网卡的地址就有可以改
变,导致不能远程管理,盼指点如何解决此问题呢?也许看完整个代码就会也答案……*/
struct net_device dev;
struct net_device_stats statistics; //网桥虚拟网卡的统计数据
rwlock_t hash_lock; //hash表的读写锁,这个表就是用于存放桥的MAC-PORT对应表
struct net_bridge_fdb_entry *hash[BR_HASH_SIZE]; //就是这张表了,也叫CAM表
struct timer_list tick;
/*以下定义了STP协议所使用的信息,参见STP协议的相关定义,我的小站上
http://www.skynet.org.cn/viewthread.php?tid=90&fpage=1也有对协议的相关分析 */
bridge_id designated_root;
int root_path_cost;
int root_port;
int max_age;
int hello_time;
int forward_delay;
bridge_id bridge_id;
int bridge_max_age;
int bridge_hello_time;
int bridge_forward_delay;
unsigned stp_enabled:1;
unsigned topology_change:1;
unsigned topology_change_detected:1;
struct br_timer hello_timer;
struct br_timer tcn_timer;
struct br_timer topology_change_timer;
struct br_timer gc_timer;
int ageing_time;
int gc_interval;
};
可以看出,桥中有几个重要的地方:
1、桥的端口成员:struct net_bridge_port *port_list;
2、桥的CAM表:struct net_bridge_fdb_entry *hash[BR_HASH_SIZE];
3、桥的虚拟网卡
4、STP
桥的虚拟网卡是一个struct net_device设备,它在2.4中是如此庞大,要对它在这里进行分析无疑是非常困难的,改天大家一起讨论吧。
STP的相关成员的定义与STP包的结构是紧密相关的,看了其包结构,可以分析出这些成员了,不再一一列举了。
网桥中的端口,用struct net_bridge结构表示,它实际上表示的是接收该数据包的网桥的端口的相关信息:
struct net_bridge_port
{
struct net_bridge_port *next; //网桥端口组中的下一个端口
struct net_bridge *br; //当前端口(接收数据包这个)所在的桥组
struct net_device *dev; //本端口所指向的物理网卡
int port_no; //本端口在网桥中的编号
port_id port_id;
int state;
int path_cost;
bridge_id designated_root;
int designated_cost;
bridge_id designated_bridge;
port_id designated_port;
unsigned topology_change_ack:1;
unsigned config_pending:1;
int priority;
struct br_timer forward_delay_timer;
struct br_timer hold_timer;
struct br_timer message_age_timer;
};
这个结构对应了内核缓存中的skb->dev->br_port;