1 前言
本文的参考分析的源代码版本是2.6.15,我是边学习边总结,学习的过程中得益于Linux论坛(http://linux.chinaunix.net/bbs/)上大侠们总结分析的文档,他山之石可以攻玉,学习过程中我也会边学边总结,开源的发展在于共享,我也抛块砖,望能引到玉!
由于自身水平有限,且相关的参考资料较少,因此其中的结论不能保证完全正确,如果在阅读本文的过程中发现了问题欢迎及时与作者联系。也希望能有机会和大家多多交流学习心得!
简单来说,桥接就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。
交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的。于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信。
如下图:主机A发送的报文被送到交换机S1的eth0口,由于eth0与eth1、eth2桥接在一起,故而报文被复制到eth1和eth2,并且发送出 去,然后被主机B和交换机S2接收到。而S2又会将报文转发给主机C、D。
![](http://blog.chinaunix.net/photo/109828_100908155439.jpg)
交换机在报文转发的过程中并不会篡改报文数据,只是做原样复制。然而桥接却并不是在物理层实现的,而是在数据链路层。交换机能够理解数据链路层的报文,所 以实际上桥接却又不是单纯的报文转发。
交换机会关心填写在报文的数据链路层头部中的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内核支持网口的桥接(目前只支持以太网接口)。但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发、要么丢弃。小型 的交换机里面只需要一块交换芯片即可,并不需要CPU。而运行着linux内核的机器本身就是一台主机,有可能就是网络报文的目的地。其收到的报文除了转 发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己消化。
linux内核是通过一个虚拟的网桥设备来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如下图(摘自ULNI):
![](http://blog.chinaunix.net/photo/109828_100908155447.jpg)
网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看得到br0,因为桥接是在数据链路层实现的,上层不需要关心桥接的细节。于是协 议栈上层需要发送的报文被送到br0,网桥设备的处理代码再来判断报文该被转发到eth0或是eth1,或者两者皆是;反过来,从eth0或从eth1接 收到的报文被提交给网桥的处理代码,在这里会判断报文该转发、丢弃、或提交到协议栈上层。
而有时候eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收(从而绕过网桥)。
概括来说,网桥实现最重要的两点:
1. MAC学习:学习MAC地址,起初,网桥是没有任何地址与端口的对应关系的,它发送数据,还是得想HUB一样,但是每发送一个数据,它都会关心数据包的来源MAC是从自己的哪个端口来的,由于学习,建立地址-端口的对照表(CAM表)。
2. 报文转发:每发送一个数据包,网桥都会提取其目的MAC地址,从自己的地址-端口对照表(CAM表)中查找由哪个端口把数据包发送出去。
在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 初始化
static int __init br_init(void) { br_fdb_init(); //网桥数据库初始化,分配slab缓冲区 #ifdef CONFIG_BRIDGE_NETFILTER if (br_netfilter_init()) //netfilter钩子初始化 return 1; #endif brioctl_set(br_ioctl_deviceless_stub); //设置ioctl钩子函数:br_ioctl_hook br_handle_frame_hook = br_handle_frame;//设置报文处理钩子:br_ioctl_hook //网桥数据库处理钩子 br_fdb_get_hook = br_fdb_get; br_fdb_put_hook = br_fdb_put; //在netdev_chain通知链表上注册 register_netdevice_notifier(&br_device_notifier); return 0; } |
4.2 新建网桥
前面说到通过brctl addbr br0命令建立网桥,此处用户控件调用的brctl命令最终对应到内核中的br_ioctl_deviceless_stub处理函数:
int br_ioctl_deviceless_stub(unsigned int cmd, void __user *uarg) { switch (cmd) { case SIOCGIFBR: case SIOCSIFBR: return old_deviceless(uarg); case SIOCBRADDBR: //新建网桥 case SIOCBRDELBR: //删除网桥 { char buf[IFNAMSIZ]; if (!capable(CAP_NET_ADMIN)) return -EPERM; //copy_from_user:把用户空间的数据拷入内核空间 if (copy_from_user(buf, uarg, IFNAMSIZ)) return -EFAULT; buf[IFNAMSIZ-1] = 0; if (cmd == SIOCBRADDBR) return br_add_bridge(buf); return br_del_bridge(buf); } } return -EOPNOTSUPP; } |
在这里,我们传入的cmd为SIOCBRADDBR.转入br_add_bridge(buf)中进行:
int br_add_bridge(const char *name) { struct net_device *dev; int ret; //为虚拟桥新建一个net_device dev = new_bridge_dev(name); if (!dev) return -ENOMEM; rtnl_lock(); //由内核确定接口名字,例如eth0 eth1等 if (strchr(dev->name, '%')) { ret = dev_alloc_name(dev, dev->name); if (ret < 0) goto err1; } //向内核注册此网络设备 ret = register_netdevice(dev); if (ret) goto err2; /* network device kobject is not setup until * after rtnl_unlock does it's hotplug magic. * so hold reference to avoid race. */ dev_hold(dev); rtnl_unlock(); //在sysfs中建立相关信息 ret = br_sysfs_addbr(dev); dev_put(dev); if (ret) unregister_netdev(dev); out: return ret; err2: free_netdev(dev); err1: rtnl_unlock(); goto out; } |
网桥是一个虚拟的设备,它的注册跟实际的物理网络设备注册是一样的。我们关心的是网桥对应的net_device结构是什么样的,继续跟踪进new_bridge_dev:
static struct net_device *new_bridge_dev(const char *name) { struct net_bridge *br; struct net_device *dev; //分配net_device dev = alloc_netdev(sizeof(struct net_bridge), name, br_dev_setup); if (!dev) return NULL; //网桥的私区结构为net_bridge br = netdev_priv(dev); //私区结构中的dev字段指向设备本身 br->dev = dev; spin_lock_init(&br->lock); //队列初始化。在port_list中保存了这个桥上的端口列表 INIT_LIST_HEAD(&br->port_list); spin_lock_init(&br->hash_lock); //下面这部份代码跟stp协议相关,我们暂不关心 br->bridge_id.prio[0] = 0x80; br->bridge_id.prio[1] = 0x00; memset(br->bridge_id.addr, 0, ETH_ALEN); br->stp_enabled = 0; br->designated_root = br->bridge_id; br->root_path_cost = 0; br->root_port = 0; br->bridge_max_age = br->max_age = 20 * HZ; br->bridge_hello_time = br->hello_time = 2 * HZ; br->bridge_forward_delay = br->forward_delay = 15 * HZ; br->topology_change = 0; br->topology_change_detected = 0; br->ageing_time = 300 * HZ; INIT_LIST_HEAD(&br->age_list); br_stp_timer_init(br); return dev; } |
在br_dev_setup中还做了一些另外在函数指针初始化:
void br_dev_setup(struct net_device *dev) { //将桥的MAC地址设为零 memset(dev->dev_addr, 0, ETH_ALEN); //初始化dev的部分函数指针,因为目前网桥设备主适用于以及网, //以太网的部分功能对它也适用 ether_setup(dev); //设置设备的ioctl函数为br_dev_ioctl dev->do_ioctl = br_dev_ioctl; //网桥与一般网卡不同,网桥统一统计它的数据包和字节数等信息 dev->get_stats = br_dev_get_stats; // 网桥接口的数据包发送函数,真实设备要向外发送数据时,是通过网卡向外发送数据 // 而该网桥设备要向外发送数据时,它的处理逻辑与网桥其它接口的基本一致。 dev->hard_start_xmit = br_dev_xmit; dev->open = br_dev_open; dev->set_multicast_list = br_dev_set_multicast_list; dev->change_mtu = br_change_mtu; dev->destructor = free_netdev; SET_MODULE_OWNER(dev); dev->stop = br_dev_stop; dev->tx_queue_len = 0; dev->set_mac_address = NULL; dev->priv_flags = IFF_EBRIDGE; } |
仅仅创建网桥,还是不够的。实际应用中的网桥需要添加实际的端口(即物理接口),如例子中的eth1, eth2等。应用程序在使用ioctl来为网桥增加物理接口,对应内核函数br_dev_ioctl的代码和分析如下:
int br_dev_ioctl(struct net_device *dev, struct ifreq *rq, int cmd) { struct net_bridge *br = netdev_priv(dev); switch(cmd) { case SIOCDEVPRIVATE: return old_dev_ioctl(dev, rq, cmd); case SIOCBRADDIF: //添加 case SIOCBRDELIF: //删除 //同一处理函数,默认为添加 return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF); } pr_debug("Bridge does not support ioctl 0x%x\n", cmd); return -EOPNOTSUPP; } |
下面分析具体的添加删除函数add_del_if:
static int add_del_if(struct net_bridge *br, int ifindex, int isadd) { struct net_device *dev; int ret; if (!capable(CAP_NET_ADMIN)) return -EPERM; dev = dev_get_by_index(ifindex); if (dev == NULL) return -EINVAL; if (isadd) ret = br_add_if(br, dev); else ret = br_del_if(br, dev); dev_put(dev); return ret; } |
对应的添加删除函数分别为:br_add_if, br_del_if;
br_add_if:
int br_add_if(struct net_bridge *br, struct net_device *dev) { struct net_bridge_port *p; int err = 0; /*--Kernel仅支持以太网网桥--*/ if (dev->flags & IFF_LOOPBACK || dev->type != ARPHRD_ETHER) return -EINVAL; /*--把网桥接口当作物理接口加入到另一个网桥中,是不行的, 逻辑和代码上都会出现 loop--*/ if (dev->hard_start_xmit == br_dev_xmit) return -ELOOP; /*--该物理接口已经绑定到另一个网桥了--*/ if (dev->br_port != NULL) return -EBUSY; /*--为该接口创建一个网桥端口数据,并初始化好该端口的相关数据--*/ if (IS_ERR(p = new_nbp(br, dev, br_initial_port_cost(dev)))) return PTR_ERR(p); /*--将该接口的物理地址写入到 MAC-端口映射表中, 该MAC是属于网桥内部端口的固定MAC地址, 它在fdb中的记录是固定的,不会失效(agged)--*/ if ((err = br_fdb_insert(br, p, dev->dev_addr))) destroy_nbp(p); /*--添加相应的系统文件信息--*/ else if ((err = br_sysfs_addif(p))) del_nbp(p); else { /*--打开该接口的混杂模式,网桥中的各个端口必须处于混杂模式, 网桥才能正确工作--*/ dev_set_promiscuity(dev, 1); /*--加到端口列表--*/ list_add_rcu(&p->list, &br->port_list); /*--STP相关设置-*/ spin_lock_bh(&br->lock); br_stp_recalculate_bridge_id(br); br_features_recompute(br); if ((br->dev->flags & IFF_UP) && (dev->flags & IFF_UP) && netif_carrier_ok(dev)) br_stp_enable_port(p); spin_unlock_bh(&br->lock); /*--设置设备的mtu--*/ dev_set_mtu(br->dev, br_min_mtu(br)); } return err; } |
br_del_if:
int br_del_if(struct net_bridge *br, struct net_device *dev) { struct net_bridge_port *p = dev->br_port; if (!p || p->br != br) return -EINVAL; br_sysfs_removeif(p); del_nbp(p); spin_lock_bh(&br->lock); br_stp_recalculate_bridge_id(br); br_features_recompute(br); spin_unlock_bh(&br->lock); return 0; } |
网桥最主要有三个数据结构:struct net_bridge,struct net_bridge_port,struct net_bridge_fdb_entry,他们之间的关系如下图:
![](http://blog.chinaunix.net/photo/109828_100908155455.jpg)
展开来如下图:
![](http://blog.chinaunix.net/photo/109828_100908155518.jpg)
说明:
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
struct net_bridge { spinlock_t lock; //读写锁 //网桥所有端口的链表,其中每个元素都是一个net_bridge_port结构 struct list_head port_list; struct net_device *dev; //网桥对应的设备 struct net_device_stats statistics; //网桥对应的虚拟网卡的统计数据 spinlock_t hash_lock; //hash表的锁 /*--CAM: 保存forwarding database的一个hash链表(这个也就是地址学习的东东, 所以通过hash能 快速定位),这里每个元素都是一个net_bridge_fsb_entry结构--*/ struct hlist_head hash[BR_HASH_SIZE]; struct list_head age_list; /* STP */ //与stp 协议对应的数据 bridge_id designated_root; bridge_id bridge_id; u32 root_path_cost; unsigned long max_age; unsigned long hello_time; unsigned long forward_delay; unsigned long bridge_max_age; unsigned long ageing_time; unsigned long bridge_hello_time; unsigned long bridge_forward_delay; u16 root_port; unsigned char stp_enabled; unsigned char topology_change; unsigned char topology_change_detected; //stp要用的一些定时器列表。 struct timer_list hello_timer; struct timer_list tcn_timer; struct timer_list topology_change_timer; struct timer_list gc_timer; struct kobject ifobj; } |
2. struct net_bridge_port
struct net_bridge_port { struct net_bridge *br; //从属于的网桥设备 struct net_device *dev;//表示链接到这个端口的物理设备 struct list_head list; /* STP */ //stp相关的一些参数。 u8 priority; u8 state; u16 port_no; //本端口在网桥中的编号 unsigned char topology_change_ack; unsigned char config_pending; port_id port_id; port_id designated_port; bridge_id designated_root; bridge_id designated_bridge; u32 path_cost; u32 designated_cost; //端口定时器,也就是stp控制超时的一些定时器列表 struct timer_list forward_delay_timer; struct timer_list hold_timer; struct timer_list message_age_timer; struct kobject kobj; struct rcu_head rcu; } |
3. struct net_bridge_fdb_entry
struct net_bridge_fdb_entry { struct hlist_node hlist; //桥的端口(最主要的两个域就是这个域和下面的mac地址域) struct net_bridge_port *dst; struct rcu_head rcu; //当使用RCU策略,才用到 atomic_t use_count; //引用计数 unsigned long ageing_timer; //MAC超时时间 mac_addr addr; //mac地址。 unsigned char is_local; //是否为本机的MAC地址 unsigned char is_static; //是否为静态MAC地址 } |
这里所说的网桥数据库指的是CAM表,即struct net_bridge结构中的hash表,数据库的维护对应的是对结构struct net_bridge_fdb_entry的操作;
众所周知,网桥需要维护一个MAC地址-端口映射表,端口是指网桥自身提供的端口,而MAC地址是指与端口相连的另一端的MAC地址。当网桥收到一个报文时,先获取它的源MAC,更新数据库,然后读取该报文的目标MAC地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端口以外的所有端口转发。
数据库使用kmem_cache_create函数进行创建,使用kmem_cache_desctory进行销毁。路径:[/net/bridge/br_fdb.c]:
void __init br_fdb_init(void) { br_fdb_cache = kmem_cache_create("bridge_fdb_cache", sizeof(struct net_bridge_fdb_entry), 0, SLAB_HWCACHE_ALIGN, NULL, NULL); } |
当网桥收到一个数据包时,它会获取该数据的源MAC地址,然后对数据库进行更新。如果该MAC地址不在数库中,则创新一个数据项。如果存在,更新它的年龄。数据库使用hash表的结构方式,便于高效查询。下面是hash功能代码的分析:
路径:[/net/bridge/br_fdb.c]
void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source, const unsigned char *addr) { /*--br_mac_hash函数是hash表中的hash函数,具体算法过程可参阅该函数代码; br->hash就是数据库的hash表,每个hash值对应一个链表; 数据库的每项为net_bridge_fdb_entry结构--*/ struct hlist_head *head = &br->hash[br_mac_hash(addr)]; struct net_bridge_fdb_entry *fdb; /* some users want to always flood. */ if (hold_time(br) == 0) return; rcu_read_lock(); fdb = fdb_find(head, addr); /*--如果找到对应的fdb,更新fdb->dst,fdb->ageing_timer--*/ if (likely(fdb)) { /* attempt to update an entry for a local interface */ if (unlikely(fdb->is_local)) { if (net_ratelimit()) printk(KERN_WARNING "%s: received packet with " " own address as source address\n", source->dev->name); } else { /* fastpath: update of existing entry */ fdb->dst = source; fdb->ageing_timer = jiffies; } } else { /*--没有找到,则新建一个fdb--*/ spin_lock_bh(&br->hash_lock); if (!fdb_find(head, addr)) fdb_create(head, source, addr, 0); /* else we lose race and someone else inserts * it first, don't bother updating */ spin_unlock_bh(&br->hash_lock); } rcu_read_unlock(); } |
在更新函数里面已为某一MAC找到了它所属于的Hash链表,因此,创建函数只需要在该链上添加一个数据项即可。
static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head, struct net_bridge_port *source, const unsigned char *addr, int is_local) { struct net_bridge_fdb_entry *fdb; /*--申请数据区--*/ fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC); if (fdb) { memcpy(fdb->addr.addr, addr, ETH_ALEN); atomic_set(&fdb->use_count, 1); hlist_add_head_rcu(&fdb->hlist, head); /*--添加到链表--*/ fdb->dst = source; fdb->is_local = is_local; fdb->is_static = is_local; fdb->ageing_timer = jiffies; //MAC年龄 } return fdb; } |
查找分两种:一种是数据项更新时候的查找,另一种是转发报文时候查找,两者区别是转发时查找需要判断MAC地址是否过期,即我们常说的MAC老化;更新时则不用判断;
网桥更新一MAC地址时,不管该地址是否已经过期了,只需遍历该MAC地址对应的Hash链表,然后更新年龄,此时它肯定不过期了。
网桥要转发数据时,除了要找到该目标MAC的出口端口外,还要判断该记录是否过期了。
更新时查找:
static inline struct net_bridge_fdb_entry *fdb_find(struct hlist_head *head, const unsigned char *addr) { struct hlist_node *h; struct net_bridge_fdb_entry *fdb; /*--遍历链表比较地址--*/ hlist_for_each_entry_rcu(fdb, h, head, hlist) { if (!compare_ether_addr(fdb->addr.addr, addr)) return fdb; } return NULL; } |
转发时查找:
struct net_bridge_fdb_entry *__br_fdb_get(struct net_bridge *br, const unsigned char *addr) { struct hlist_node *h; struct net_bridge_fdb_entry *fdb; /*--遍历链表比较地址--*/ hlist_for_each_entry_rcu(fdb, h, &br->hash[br_mac_hash(addr)], hlist) { if (!compare_ether_addr(fdb->addr.addr, addr)) { /*--判断是否过期--*/ if (unlikely(has_expired(br, fdb))) break; return fdb; } } return NULL; } |
比较一下,转发时多了一个函数处理:has_expired, Has_expired函数来决定该数据项是否是过期的,代码如下:
/*--数据项的可保留时间根据拓扑结构是否改变来决定, 改变则为forward_delay,否则为ageing_time--*/ /* if topology_changing then use forward_delay (default 15 sec) * otherwise keep longer (default 5 minutes) */ static __inline__ unsigned long hold_time(const struct net_bridge *br) { return br->topology_change ? br->forward_delay : br->ageing_time; } static __inline__ int has_expired(const struct net_bridge *br, const struct net_bridge_fdb_entry *fdb) { /*--1. 如果该数据项是静态的,即不是学习过来的,它永远不会过期。 因为它就是网桥自己端口的地址 2. 如果最近更新时间加上可保留时间大于当前时间,即老化时间还在以后, 表示尚未过期,time_before_eq返回真,否则返回假 --*/ return !fdb->is_static && time_before_eq(fdb->ageing_timer + hold_time(br), jiffies); } |
桥建立时设置一个定时器,循环检测,如果发现有过期的MAC,则清除对应的数据项,MAC地址过期清除由函数br_fdb_cleanup实现:
/*--定时器循环检查MAC地址是否过期 定时器在桥初始化中定义开启--*/ void br_fdb_cleanup(unsigned long _data) { struct net_bridge *br = (struct net_bridge *)_data; unsigned long delay = hold_time(br);/*--获取MAC地址可保留时间--*/ int i; spin_lock_bh(&br->hash_lock); for (i = 0; i < BR_HASH_SIZE; i++) { struct net_bridge_fdb_entry *f; struct hlist_node *h, *n; /*--如果该地址不是静态的,并且已经过期,则从数据库中清除该MAC映射--*/ hlist_for_each_entry_safe(f, h, n, &br->hash[i], hlist) { if (!f->is_static && time_before_eq(f->ageing_timer + delay, jiffies)) fdb_delete(f); } } spin_unlock_bh(&br->hash_lock); /*--更新检查定时器--*/ mod_timer(&br->gc_timer, jiffies + HZ/10); } |
在上个函数Br_handle_frame_finish中如果报文是需要发往本地协议栈处理的,则由函数Br_pass_frame_up实现:
static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb) { struct net_device *indev; br->statistics.rx_packets++; br->statistics.rx_bytes += skb->len; indev = skb->dev; skb->dev = br->dev;/*--报文中的dev被赋予网桥本身的虚拟dev--*/ NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL, br_pass_frame_up_finish); } |
这段代码非常简单,对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为空,不进入网桥处理流程 ;从而进入上层协议栈处理;
7.5 Br_forward
void br_forward(const struct net_bridge_port *to, struct sk_buff *skb) { /*--should_deliver: 是否符合转发条件 __br_forward: 转发--*/ if (should_deliver(to, skb)) { __br_forward(to, skb); return; } kfree_skb(skb); } |
7.6 __br_forward
static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb) { struct net_device *indev; indev = skb->dev; skb->dev = to->dev; /*--替换报文中的dev为转发端口对应的dev--*/ skb->ip_summed = CHECKSUM_NONE; NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev, br_forward_finish); } |
int br_forward_finish(struct sk_buff *skb) { NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit); return 0; } |
int br_dev_queue_push_xmit(struct sk_buff *skb) { /*--报文长度超过dev发送的mtu限制,丢弃报文--*/ /* drop mtu oversized packets except tso */ if (skb->len > skb->dev->mtu && !skb_shinfo(skb)->tso_size) kfree_skb(skb); else { #ifdef CONFIG_BRIDGE_NETFILTER /* ip_refrag calls ip_fragment, doesn't copy the MAC header. */ nf_bridge_maybe_copy_header(skb); #endif skb_push(skb, ETH_HLEN); dev_queue_xmit(skb); } return 0; } |
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