作者: liusz 

#1  Linux网桥实现分析-第一部份,源码框架


Linux 网桥实现分析

作者:kendo

第一部份 源码框架

一、        网桥原理
传统的中继器,如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;




整个网桥的源码框架就这样了,学习,查表,进行STP处理,数据传送。与网络书籍上讲的原理一模一样(废话)。而学习,查表,进行STP处理,数据传送这四个方面的内容,后面章节会陆续分析。