网桥

来源:互联网 发布:arksz410788k数据手册 编辑:程序博客网 时间:2024/04/30 10:54

原文地址:(zz)Linux-网桥原理分析(一) 作者:yinguochun1234


1  前言

本文的参考分析的源代码版本是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_BRIDGECONDIG_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;
}

4.3  添加删除端口

仅仅创建网桥,还是不够的。实际应用中的网桥需要添加实际的端口(即物理接口),如例子中的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(= 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 *= dev->br_port;
    
    if (!|| 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,他们之间的关系如下图:

展开来如下图:

说明:

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地址

}

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]:

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);
}

6.2  数据库更新

当网桥收到一个数据包时,它会获取该数据的源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();
}


6.3 创建数据项

在更新函数里面已为某一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;
}


6.4 查找数据项

查找分两种:一种是数据项更新时候的查找,另一种是转发报文时候查找,两者区别是转发时查找需要判断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);
}


6.5   MAC地址过期清理

桥建立时设置一个定时器,循环检测,如果发现有过期的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 (= 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);
}


out:
    return 0;
}

7.4  Br_pass_frame_up

在上个函数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_HOOKNF_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);
}

7.7   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;
}

7.8   Br_dev_queue_push_xmit

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) 单播但找不到端口映射的,则除了接收端口外其余端口都需要转发;

参考文献

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



0 0
原创粉丝点击