cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
阅读:4097回复:6

linux2.6内核网桥分析,供大家参考

楼主#
更多 发布于:2007-03-15 19:20
  Linux网桥分析

一、网桥原理
    网桥(Bridge)也称桥接器,是连接两个局域网的存储转发设备,用它可以完成具有相同或相似体系结构网络系统的连接。一般情况下,被连接的网络系统都具有相同的逻辑链路控制规程(LLC),但媒体访问控制协议(MAC)可以不同。
    网桥工作在数据链路层,将两个LAN连起来,根据MAC地址来转发帧,可以看作一个“低层的路由器”(路由器工作在网络层,根据网络地址如IP地址进行转发)。 远程网桥通过一个通常较慢的链路(如电话线)连接两个远程LAN,对本地网桥而言,性能比较重要,而对远程网桥而言,在长距离上可正常运行是更重要的。

二、Linux网桥的使用
    要配置网桥,要网桥的配置工具bridge-utils。源代码可以在http://bridge.sourceforge.net/bridge-utils/ 下载。编译成功之后,生成网桥配置的工具名称为brctl。
    
    配置说明:
    有五台主机。其中一台主机装有linux ,安装了网桥模块,而且有四块物理网卡,分别连接同一网段的其他主机。我们希望其成为一个网桥,为其他四台主机(IP分别为192.168.1.2 ,192.168.1.3,192.168.1.4,192.168.1.5) 之间转发数据包。同时,为了方便管理,希望网桥能够有一个IP(192.168.1.1),那样管理员就可以在192.168.1.0/24网段内的主机上telnet到网桥,对其进行配置,实现远程管理。
    
    我们为这个逻辑网段一个名称,br_192。首先需要配置这样一个逻辑网段。

    # brctl addbr br_192            (建立一个逻辑网段,名称为br_192)
    
    实际上,我们可以把逻辑网段192.168.1.0/24看作使一个VLAN ,而br_192则是这个VLAN的名称。

    建立一个逻辑网段之后,我们还需要为这个网段分配特定的端口。在Linux中,一个端口实际上就是一个物理网卡。而每个物理网卡的名称则分别为 eth0,eth1,eth2,eth3。我们需要把每个网卡一一和br_192这个网段联系起来,作为br_192中的一个端口。

    # brctl addif br_192 eth0            (让eth0成为br_192的一个端口)
    # brctl addif br_192 eth1            (让eth1成为br_192的一个端口)
    # brctl addif br_192 eth2            (让eth2成为br_192的一个端口)
    # brctl addif br_192 eth3            (让eth3成为br_192的一个端口)

    网桥的每个物理网卡作为一个端口,运行于混杂模式,而且是在链路层工作,所以就不需要IP了。

    # ifconfig eth0 0.0.0.0
    # ifconfig eth1 0.0.0.0
    # ifconfig eth2 0.0.0.0
    # ifconfig eth3 0.0.0.0
 

    然后给br_192的虚拟网卡配置IP:192.168.1.1。那样就能远程管理网桥。

    # ifconfig br_192 192.168.1.1
 

    给br_192配置了IP之后,网桥就能够工作了。192.168.1.0/24网段内的主机都可以telnet到网桥上对其进行配置。

    以上配置的是一个逻辑网段,实际上Linux网桥也能配置成多个逻辑网段(相当于交换机中划分多个VLAN)。具体的方法可以参考bridge-util中的HOWTO
    

三、Linux网桥分析

    首先了解网桥处理包遵循着以下几条原则:

    1 在一个接口上接收到的包不会再在那个接口上发送这个数据包。
    2 每个接收到的数据包都要学习其源MAC地址。
    3 如果数据包是多播包或广播包,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包,如果上层协议栈对多播包感兴趣,则需要把数据包提交给上层协议栈。
    4 如果数据包的目的MAC地址不能在CAM表中找到,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包。
    5 如果能够在CAM表中查询到目的MAC地址,则在特定的端口上发送这个数据包,如果发送端口和接收端口是同一端口,则不发送。
    
    网桥以混杂方式工作,它接收与之连接的所有LAN传送的每一帧。当一帧到达时,网桥必须决定将其丢弃还是转发。如果要转发,则必须决定发往哪个LAN。这需要通过查询网桥中一张大型散列表里的目的地址而作出决定。该表可列出每个可能的目的地,以及它属于哪一条输出线路(LAN)。在插入网桥之初,所有的散列表均为空。由于网桥不知道任何目的地的位置,因而采用扩散算法(flooding algorithm):把每个到来的、目的地不明的帧输出到连在此网桥的所有LAN中(除了发送该帧的LAN)。随着时间的推移,网桥将了解每个目的地的位置。一旦知道了目的地位置,发往该处的帧就只放到适当的LAN上,而不再散发。

    网桥采用的算法是逆向学习法(backward learning)。网桥按混杂的方式工作,故它能看见所连接的任一LAN上传送的帧。查看源地址即可知道在哪个LAN上可访问哪台机器,于是在散列表中添上一项。

    当计算机和网桥加电、断电或迁移时,网络的拓扑结构会随之改变。为了处理动态拓扑问题,每当增加散列表项时,均在该项中注明帧的到达时间。每当目的地已在表中的帧到达时,将以当前时间更新该项。这样,从表中每项的时间即可知道该机器最后帧到来的时间。网桥中有一个进程定期地扫描散列表,清除时间早于当前时间若干分钟的全部表项。于是,如果从LAN上取下一台计算机,并在别处重新连到LAN上的话,那么在几分钟内,它即可重新开始正常工作而无须人工干预。这个算法同时也意味着,如果机器在几分钟内无动作,那么发给它的帧将不得不散发,一直到它自己发送出一帧为止。
    
    在Linux内核网桥的实现中,一个逻辑网段用net_bridge结构体表示。一个逻辑网段需要保留的信息有:
    
    1 本逻辑网段中所有的端口(port_list)
    每个端口用net_bridge_port结构体来表示,从net_bridge_port结构体中可以看出,它主要有:

        1 逻辑网段中的下一个端口(next)
        2 本端口所属的逻辑网段(br)
        3 本端口所指向的物理网卡(dev)
        4 本端口在网桥中的编号(port_no)
        5 用于生成树管理的信息
        一个逻辑网段中可以具有很多个端口,所有的端口都挂在以port_list为链表头的链表上。

    2 本网段中CAM表(hash[BR_HASH_SIZE])
    CAM表中的每个项用net_bridge_fdb_entry结构体代表,每项中有:

        1 用于CAM表连接的链表指针(next_hash,pprev_hash)
        2 此项当前的引用计数(use_count)
        3 MAC地址(addr)
        4 此项所对应的端口(dst)
        5 处理MAC超时(ageing_timer)
        6 是否是本机的MAC地址(is_local)
        7 是否是静态MAC地址(is_static)
    一个逻辑网段中的所有表项形成一个CAM表,他们之间的组织关系是一个HASH链表。HASH链的个数为BR_HASH_SIZE(256)。

    3 本逻辑网段用于和外部通信的虚拟网络设备(dev)
    Linux网桥可以在网桥上为每个逻辑网段配置一个IP,用于和外部通信。实际上这个IP不是配置在一个特定的物理网卡上面, 而是建立一个虚拟的网卡,虚拟网卡可以附在每个同一逻辑网段的物理网卡上,让这个网卡可以象所有的物理网卡一样工作。从而使网桥可以和外部通信。

    4 本逻辑网段虚拟网卡的统计数据(statistics)
    按照Linux网卡驱动的接口,一个网卡的统计信息是由每个网卡的私有数据处理的。一般的写法是用dev->priv来指向每个网卡的统计数据。网卡的get_stats方法就是用来读取统计数据。

    5 用户一个网段的生成树(STP)信息


struct net_bridge
{
    spinlock_t            lock;
    struct list_head        port_list; //桥组中的端口列表    
    struct net_device        *dev;  //网桥都会有一个虚拟设备用来进行管理,就是它了
    struct net_device_stats        statistics; //网桥虚拟网卡的统计数据
    spinlock_t            hash_lock;                    //hash表的读写锁,这个表就是用于存放桥的MAC-PORT对应表
    struct hlist_head        hash[BR_HASH_SIZE];//CAM表
    struct list_head        age_list;        //网桥链表
    unsigned long            feature_mask;

    /* 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;

    u8                group_addr[ETH_ALEN];
    u16                root_port;
    unsigned char            stp_enabled;
    unsigned char            topology_change;
    unsigned char            topology_change_detected;

    struct timer_list        hello_timer;
    struct timer_list        tcn_timer;
    struct timer_list        topology_change_timer;
    struct timer_list        gc_timer;
    struct kobject            ifobj;
};

可以看出,桥中有几个重要的地方:
1、桥的端口成员:port_list;
2、桥的CAM表:hash[BR_HASH_SIZE];
3、桥的虚拟网卡
4、STP

port_list网桥中的端口链表,用struct net_bridge结构表示,它实际上表示的是接收该数据包的网桥的端口的相关信息:
struct net_bridge_port
{
    struct net_bridge        *br;  //当前端口(接收数据包这个)所在的桥组
    struct net_device        *dev; //本端口所指向的物理网卡
    struct list_head        list; //网桥端口组中的下一个端口

    /* 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;

    struct timer_list        forward_delay_timer;
    struct timer_list        hold_timer;
    struct timer_list        message_age_timer;
    struct kobject            kobj;
    struct work_struct        carrier_check;
    struct rcu_head            rcu;
};
这个结构对应了内核缓存中的skb->dev->br_port;
走走看看开源好 Solaris vs Linux
cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
沙发#
发布于:2007-03-15 20:25
处理流程

1 在src/net/core/dev.c的int netif_receive_skb(struct sk_buff *skb)中:

    handle_diverter(skb);

    if (handle_bridge(&skb, &pt_prev, &ret, orig_dev))
        goto out;

    type = skb->protocol;
    
    由handle_bridge函数处理网桥处理.
    

static __inline__ int handle_bridge(struct sk_buff **pskb,
                    struct packet_type **pt_prev, int *ret,
                    struct net_device *orig_dev)
{
    struct net_bridge_port *port;

    //如果是回环数据或者skb->dev->br_port :接收该数据包的端口是网桥端口组的一员,如果接收当前数据包的接口不是网桥的某一物理端口,则其值为NULL;
    if ((*pskb)->pkt_type == PACKET_LOOPBACK ||
        (port = rcu_dereference((*pskb)->dev->br_port)) == NULL)
        return 0;

    if (*pt_prev) {
        *ret = deliver_skb(*pskb, *pt_prev, orig_dev);
        *pt_prev = NULL;
    }
    
    //br_handle_frame_hook :定义了网桥处理函数这段代码将数据包进行转向,转向的后的处理函数是钩子函数br_handle_frame_hook
    return br_handle_frame_hook(port, pskb);
}

2  钩子函数的注册
br_handle_frame_hook用于网桥的处理,在网桥的初始化函数中
static int __init br_init(void)
{
    br_stp_sap = llc_sap_open(LLC_SAP_BSPAN, br_stp_rcv);
    if (!br_stp_sap) {
        printk(KERN_ERR "bridge: can't register sap for STP\n");
        return -EBUSY;
    }

    br_fdb_init();

#ifdef CONFIG_BRIDGE_NETFILTER
    if (br_netfilter_init())
        return 1;
#endif
    brioctl_set(br_ioctl_deviceless_stub);
    
    //网桥真正的处理接口
    br_handle_frame_hook = br_handle_frame;

    br_fdb_get_hook = br_fdb_get;
    br_fdb_put_hook = br_fdb_put;

    register_netdevice_notifier(&br_device_notifier);

    return 0;
}

3、br_handle_frame处理
int br_handle_frame(struct net_bridge_port *p, struct sk_buff **pskb)
{
    struct sk_buff *skb = *pskb;
    const unsigned char *dest = eth_hdr(skb)->h_dest;

    //源地址不能使广播地址或者0地址
    if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
        goto err;

    //见后面分析:更新cam表
    if (unlikely(is_link_local(dest))) {
        skb->pkt_type = PACKET_HOST;
        return NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
                   NULL, br_handle_local_finish) != 0;
    }

    /*众所周之,网桥之所以是网桥,比HUB更智能,是因为它有一个MAC-PORT的表,这样转发数据就不用广播,而查表定端口就可以了
        每次收到一个包,网桥都会学习其来源MAC,添加进这个表。Linux中这个表叫CAM表(这个名字是其它资料上看的)。
        如果桥的状态是LEARNING或FORWARDING(学习或转发),则学习该包的源地址skb->mac.ethernet->h_source, 将其添加到CAM表中*/

    if (p->state == BR_STATE_FORWARDING || p->state == BR_STATE_LEARNING) {
        if (br_should_route_hook) {
            if (br_should_route_hook(pskb))
                return 0;
            skb = *pskb;
            dest = eth_hdr(skb)->h_dest;
        }

        if (!compare_ether_addr(p->br->dev->dev_addr, dest))
            skb->pkt_type = PACKET_HOST;

        NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
            br_handle_frame_finish);
        return 1;
    }

err:
    kfree_skb(skb);
    return 1;
}

//桥组多播地址
const u8 br_group_address[ETH_ALEN] = { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x00 };
/* Does address match the link local multicast address.
 * 01:80:c2:00:00:0X
 */
 
/*
* STP协议的BPDU包的目的MAC采用的是多播目标MAC地址:
* 01-80-c2-00-00-00(Bridge_group_addr:网桥组多播地址)
* 比较目的地址前5位* 是否与多播目标MAC地址相同:
* (!memcmp(dest, bridge_ula, 5)
* 如果相同,如果地址第6位非空
* !(dest[5] & 0xF0))*/
static inline int is_link_local(const unsigned char *dest)
{
    return memcmp(dest, br_group_address, 5) == 0 && (dest[5] & 0xf0) == 0;
}

4 br_handle_frame_finish
int br_handle_frame_finish(struct sk_buff *skb)
{
    const unsigned char *dest = eth_hdr(skb)->h_dest;
    struct net_bridge_port *p = rcu_dereference(skb->dev->br_port);
    struct net_bridge *br;
    struct net_bridge_fdb_entry *dst;
    int passedup = 0;

    if (!p || p->state == BR_STATE_DISABLED)
        goto drop;

    /* insert into forwarding database after filtering to avoid spoofing */
    //更新cam表,防止spoof数据包
    br = p->br;
    br_fdb_update(br, p, eth_hdr(skb)->h_source);

    if (p->state == BR_STATE_LEARNING)
        goto drop;

    /*
    * 如果网桥的虚拟网卡处于混杂模式,那么每个接收到的数据包都需要克隆一份
        * 送到AF_PACKET协议处理体(netif_receive_skb)。*/
    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 (is_multicast_ether_addr(dest)) {
        br->statistics.multicast++;
        br_flood_forward(br, skb, !passedup);
        if (!passedup)
            br_pass_frame_up(br, skb);
        goto out;
    }
 
    /*查询CAM表*/    
    dst = __br_fdb_get(br, dest);
    
    /*
* 用户层常常需要用到一个虚拟的地址来管理网桥,如果目的地址非常,且为本
* 地址地址,则交由上层函数处理
*/
    if (dst != NULL && dst->is_local) {
        if (!passedup)
            br_pass_frame_up(br, skb);
        else
            kfree_skb(skb);
        goto out;
    }

    /* 转发 */
    if (dst != NULL) {
        br_forward(dst->dst, skb);
        goto out;
    }

    /*如果表里边查不到,那么发送到所有的接口……*/
    br_flood_forward(br, skb, 0);

out:
    return 0;
drop:
    kfree_skb(skb);
    goto out;
}
走走看看开源好 Solaris vs Linux
cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
板凳#
发布于:2007-03-15 20:32
STP:生成树协议
生成树协议 Spanning Tree 定义在 IEEE 802.1D 中,是一种桥到桥的链路管理协议,它在防止产生自循环的基础上提供路径冗余。为使以太网更好地工作,两个工作站之间只能有一条活动路径。网络环路的发生有多种原因,最常见的一种是故意生成的冗余,万一一个链路或交换机失败,会有另一个链路或交换机替代。

  STP 是一种桥到桥的通信技术,提供发现网络物理环路的服务。该协议规定了网桥创建无环回loop - free 逻辑拓朴结构的算法。换句话说,STP 提供了一个生成整个第二层网络的无环回树结构。

  生成树协议操作对终端站透明,也就是说,终端站并不知道它们自己是否连接在某单个局域网或多交换局域网中。当两个网桥相互连接在相同的由两台计算机组成的网络中时,生成树协议支持两网桥之间相互交换信息,这样只需要其中一个网桥处理两台计算机之间发送的信息。

  桥接设备之间通过使用网桥协议数据单元(Bridge Protocol Data Unit,BPDU)交换各自状态信息。生成树协议通过发送 BPDU 信息为交换网络配置根交换和根端口,并为每个交换网路区段(switched segment)配置根端口和指定端口。

  网桥中的生成树算法可以用来决定如何使用生成树协议,该算法的优点在于能够避免网桥环路,并确保在多路径情形下网桥能够选择一条最有效的路径。如果最佳路径选择失败,可以使用该算法重新计算网络路径并找出下一条最佳路径。

  利用生成树算法可以决定网络路径(哪台计算机主机在哪个区段),并通过 BPDU 信息交换以上数据。该过程主要分为以下两个步骤:

步骤1:通过评估网桥接收的配置信息以及选择最佳选项,再利用生成树算法来决定网桥发送的最佳信息。
步骤2:一旦选定某发送信息,网桥将该信息与来自无根(non-root)连接的可能配置信息相比较。如果步骤1中选择的最佳选项并不优于可能配置信息,便删除该端口。
 

 协议结构

  网桥协议数据单元(BPDU):  

Protocol ID (2) Version (1) Type (1) Flags (1) Rood ID (8) Root Path (4)
Sender BID (8) Port ID (2) M-Age (2) Max Age (2) Hello (2) FD (2 Bytes)

Protocol ID ― 恒为0。
Version ― 恒为0。
Type ― 决定该帧中所包含的两种 BPDU 格式类型(配置 BPDU 或 TCN BPDU)。
Flags ― 标志活动拓朴中的变化,包含在拓朴变化通知(Topology Change Notifications)的下一部分中。
Root BID ― 包括有根网桥的网桥 ID。会聚后的网桥网络中,所有配置 BPDU 中的该字段都应该具有相同值(单个 VLAN)。NetXRay 可以细分为两个 BID 子字段:网桥优先级和网桥 MAC 地址。
Root Path Cost ― 通向有根网桥(Root Bridge)的所有链路的积累资本。
Sender BID ― 创建当前 BPDU 的网桥 BID。对于单交换机(单个 VLAN)发送的所有 BPDU 而言,该字段值都相同,而对于交换机与交换机之间发送的 BPDU 而言,该字段值不同)
Port ID ― 每个端口值都是唯一的。端口1/1值为0×8001,而端口1/2 值为0×8002。
Message Age ― 记录 Root Bridge 生成当前 BPDU 起源信息的所消耗时间。
Max Age ― 保存 BPDU 的最长时间,也反映了拓朴变化通知(Topology Change Notification)过程中的网桥表生存时间情况。
Hello Time ― 指周期性配置 BPDU 间的时间。
Forward Delay ― 用于在 Listening 和 Learning 状态的时间,也反映了拓朴变化通知(Topology Change Notification)过程中的时间情况。
走走看看开源好 Solaris vs Linux
cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
地板#
发布于:2007-03-15 20:53
以下是资料转贴:

STP的处理函数
/* called under bridge lock */
void br_stp_handle_bpdu(struct sk_buff *skb)
{
unsigned char *buf;
struct net_bridge_port *p;

/*跳过DLC首部*/
buf = skb->mac.raw + 14;
p = skb->dev->br_port;
/*再次做判断*/
if (!p->br->stp_enabled || memcmp(buf, header, 6)) {
kfree_skb(skb);
return;
}

/*BPDU包有两类,由TYPE字段标志,分为配置和TCN(Topology Change Notification,拓朴改变通告)*/

/*如果是配置类型*/
if (buf[6] == BPDU_TYPE_CONFIG) {
/*内核中用struct br_config_bpdu描述一个BPDU包:
struct br_config_bpdu
{
unsigned topology_change:1; //拓朴改变标志
unsigned topology_change_ack:1; //拓朴改变回应标志
bridge_id root; //根ID,用于会聚后的网桥网络中,所有配置 BPDU 中的该字段都应该具有相同值(同VLAN),又可分为两个 BID 子字段:网桥优先级和网桥 MAC 地址
int root_path_cost; //路径开销,通向有根网桥(Root Bridge)的所有链路的积累资本
bridge_id bridge_id; //创建当前 BPDU 的网桥 BID。对于单交换机(单个 VLAN)发送的所有 BPDU 而言,该字段值都相同,而对于交换机与交换机之间发送的 BPDU 而言,该字段值不同)  
port_id port_id; //端口ID,每个端口值都是唯一的。端口1/1值为0×8001,而端口1/2 值为0×8002。
int message_age; //记录 Root Bridge 生成当前 BPDU 起源信息的所消耗时间
int max_age; //保存 BPDU 的最长时间,也反映了拓朴变化通知(Topology Change Notification)过程中的网桥表生存时间情况
int hello_time; //指周期性配置 BPDU 间的时间
int forward_delay; //用于在 Listening 和 Learning 状态的时间,也反映了拓朴变化通知(Topology Change Notification)过程中的时间情况
};
在这个结构中,bpdu包的三个字段没有包含在内:
Protocol ID ― 协议字段,恒为0。  
Version ― 版本字段,恒为0。  
Type ― 决定该帧中所包含的两种 BPDU 格式类型(配置 BPDU 或 TCN BPDU)。 上面用buf[6]直接访问了,这是
因为bpdu之前,还有三个字节的LLC头,再加上ProtocolID(2字节),VersionID(1字节),3+2+1,所以是buf[6]
这是标准的802.3封包方式,与以太网封包略有不同,参见《tcp/ip详解卷一》第二章的第二页的最上面那张图(记得是)
*/

struct br_config_bpdu bpdu;

/*一个辛苦的解包过程……*/
bpdu.topology_change = (buf[7] & 0x01) ? 1 : 0;
bpdu.topology_change_ack = (buf[7] & 0x80) ? 1 : 0;
bpdu.root.prio[0] = buf[8];
bpdu.root.prio[1] = buf[9];
bpdu.root.addr[0] = buf[10];
bpdu.root.addr[1] = buf[11];
bpdu.root.addr[2] = buf[12];
bpdu.root.addr[3] = buf[13];
bpdu.root.addr[4] = buf[14];
bpdu.root.addr[5] = buf[15];
bpdu.root_path_cost =
(buf[16] << 24) |
(buf[17] << 16) |
(buf[18] << 8) |
buf[19];
bpdu.bridge_id.prio[0] = buf[20];
bpdu.bridge_id.prio[1] = buf[21];
bpdu.bridge_id.addr[0] = buf[22];
bpdu.bridge_id.addr[1] = buf[23];
bpdu.bridge_id.addr[2] = buf[24];
bpdu.bridge_id.addr[3] = buf[25];
bpdu.bridge_id.addr[4] = buf[26];
bpdu.bridge_id.addr[5] = buf[27];
bpdu.port_id = (buf[28] << 8) | buf[29];

bpdu.message_age = br_get_ticks(buf+30);
bpdu.max_age = br_get_ticks(buf+32);
bpdu.hello_time = br_get_ticks(buf+34);
bpdu.forward_delay = br_get_ticks(buf+36);

kfree_skb(skb);
br_received_config_bpdu(p, &bpdu); /*调用配置函数*/
return;
}

/*如果是TCN类型*/
if (buf[6] == BPDU_TYPE_TCN) {
br_received_tcn_bpdu(p); /*调用TCN函数*/
kfree_skb(skb);
return;
}
kfree_skb(skb);
}
走走看看开源好 Solaris vs Linux
cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
地下室#
发布于:2007-03-15 20:55
详细的STP协议就不在这里贴了,RFC有现成的。
继续来分析config BPDU:

还是先来大概说说STP的运作流程:
STP需要确定root bridge,root port,designate port,
所以,需要在确定之间进行判断,判断的原则是:
1. 最小的root BID(所有交换机中有最小BID的成为root bridge)
2. 最小的到root bridge路径开销(确定root port)
3. 最小的发送BID(确定指向端口)
4. 最小的端口ID(如果其他标准都相同,根据端口ID确定选择标准,较小的优先)
所以,网桥需要在每收到一个BPDU包的时候,将包中的这些值,与自己原先保存的值相对比,对应的函数是:
br_supersedes_port_info

在确定好这些值后,就需要根据这些值进行选举root bridge,root port,designate port,
运作流程是:
1. 选择root bridge,选举范围是整个网络,选择的流程是交换机相互交换BPDU,
选择依据是根据BID判断谁的BID比较小(优先级小,桥MAC小)
2. 选择root port,选举范围是每个nonbridge的和其他交换机相连的端口之间(同一个交换机上的连接其他交换机的端口)
选择依据是path cost较小,每个nonbridge一个root port,可以收发数据。
3. 选择designate port,选择范围是连接每个网段之间的端口(端口在不同交换机上)
选择依据也是path cost较小,如果相同,进一步比较BID,designate port每个网段一个,可以收发数据。
4. 通过上述选择,没有成为任何角色的端口称作nondesignate port,端口设置为block状态,可以接收数据,但不转发数据。

前面三步是选择的过程,对应函数是br_configuration_update,
第四步是根据选举后的结果,决定端口的状态,对应的函数是:br_port_state_selection
开启STP的交换机端口可能处于5种状态:
1. Block:阻断状态,接收但不转发数据。
2. Listening:侦听状态,不转发数据,可以收发BPDU,执行选举root bridge,root port,designate port等动作。
3. Learning:学习状态,不转发数据,开始学习MAC,为数据转发作准备
4. Forward:转发状态,转发数据。
5. Disable:禁用状态,既不参与STP计算,也不转发数据。
在进行选举之前,需要先用传送过来的BPUD中的相关值,更新自己对应的相关值,对应的函数是:br_record_config_information

对应源码:
/* lock-safe */
void br_received_config_bpdu(struct net_bridge_port *p, struct br_config_bpdu *bpdu)
{
struct net_bridge *br;
int was_root;

if (p->state == BR_STATE_DISABLED)
return;

br = p->br;
read_lock(&br->lock);

was_root = br_is_root_bridge(br);
if (br_supersedes_port_info(p, bpdu)) {
br_record_config_information(p, bpdu);
br_configuration_update(br);
br_port_state_selection(br);

if (!br_is_root_bridge(br) && was_root) {
br_timer_clear(&br->hello_timer);
if (br->topology_change_detected) {
br_timer_clear(&br->topology_change_timer);
br_transmit_tcn(br);
br_timer_set(&br->tcn_timer, jiffies);
}
}

                                /*这个判断的作用不是太明白,盼指点……*/
if (p->port_no == br->root_port) {
br_record_config_timeout_values(br, bpdu);
br_config_bpdu_generation(br);
if (bpdu->topology_change_ack)
br_topology_change_acknowledged(br);
}
}  
                /*如果当前端口是designate port,则根据当前配置信息,生成BPDU,发送出去*/
                else if (br_is_designated_port(p)) {
br_reply(p);
}

read_unlock(&br->lock);
}
br_is_designated_port函数的是看当前桥是否就是指定的根桥,并且当前port 是否就是designate port:
/* called under bridge lock */
int br_is_designated_port(struct net_bridge_port *p)
{
return !memcmp(&p->designated_bridge, &p->br->bridge_id, 8) &&
(p->designated_port == p->port_id);
}
br_reply就是一个提取前前的信息,组包发包的过程。


br_supersedes_port_info这个判断,就是把包中的值,同先前指定的对应值进行判断和比较,经确定是否需要更新:
/* called under bridge lock */
static int br_supersedes_port_info(struct net_bridge_port *p, struct br_config_bpdu *bpdu)
{
int t;

t = memcmp(&bpdu->root, &p->designated_root, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;

if (bpdu->root_path_cost < p->designated_cost)
return 1;
else if (bpdu->root_path_cost > p->designated_cost)
return 0;

t = memcmp(&bpdu->bridge_id, &p->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;

if (memcmp(&bpdu->bridge_id, &p->br->bridge_id, 8))
return 1;

if (bpdu->port_id <= p->designated_port)
return 1;

return 0;
}

在进行更新之前,先把包中对应的值拷过来:
/* called under bridge lock */
static void br_record_config_information(struct net_bridge_port *p, struct br_config_bpdu *bpdu)
{
p->designated_root = bpdu->root;
p->designated_cost = bpdu->root_path_cost;
p->designated_bridge = bpdu->bridge_id;
p->designated_port = bpdu->port_id;

br_timer_set(&p->message_age_timer, jiffies - bpdu->message_age);
}

然后就是进行STP的选举,它们对应的协议的含义前面已经叙述了:
/* called under bridge lock */
void br_configuration_update(struct net_bridge *br)
{
br_root_selection(br);
br_designated_port_selection(br);
}

接着设置端口的状态:
/* called under bridge lock */
void br_port_state_selection(struct net_bridge *br)
{
struct net_bridge_port *p;

p = br->port_list;
while (p != NULL) {
if (p->state != BR_STATE_DISABLED) {
if (p->port_no == br->root_port) {
p->config_pending = 0;
p->topology_change_ack = 0;
br_make_forwarding(p);
} else if (br_is_designated_port(p)) {
br_timer_clear(&p->message_age_timer);
br_make_forwarding(p);
} else {
p->config_pending = 0;
p->topology_change_ack = 0;
br_make_blocking(p);
}
}

p = p->next;
}
}

如果原来自己是根桥,现在不是了,即拓朴已改变,需要发送一个TCN类型的BPDU包,通告更新(另外有一种情况就是自己原来不是根,现在变成了根,在前面br_configuration_update函数调用中,会有类似的处理):
if (!br_is_root_bridge(br) && was_root) {
br_timer_clear(&br->hello_timer);
if (br->topology_change_detected) {
br_timer_clear(&br->topology_change_timer);
br_transmit_tcn(br);
br_timer_set(&br->tcn_timer, jiffies);
}
}

后面那个判断不是很明白,盼指点一下……
走走看看开源好 Solaris vs Linux
cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
5楼#
发布于:2007-03-15 20:56
桥的CAM表是一个struct net_bridge_fdb_entry类型的数组,数组最大为:
#define BR_HASH_BITS 8
#define BR_HASH_SIZE (1 << BR_HASH_BITS)
256个项。
网桥根据目的地址来查表,以确定由哪个接口把包转发出去:
struct net_bridge_fdb_entry
{
struct net_bridge_fdb_entry *next_hash; //用于CAM表连接的链表指针
struct net_bridge_fdb_entry **pprev_hash; //为什么是pprev不是prev呢?还没有仔细去研究
atomic_t use_count; //此项当前的引用计数器
mac_addr addr; //MAC地址
struct net_bridge_port *dst; //此项所对应的物理端口
unsigned long ageing_timer; //处理MAC超时
unsigned is_local:1; //是否是本机的MAC地址
unsigned is_static:1; //是否是静态MAC地址
};

事实上,整个桥的处理,就是一个根据源地址学习(写表),根据目的地址确定发送端口(查表)的过程。当然,还有一个插曲,如处理发给本机的数据,需要交到上层,如果开启了STP,还要处理STP协议
好,那就来写STP的先,由于水平原因,很多地方估计会有错,贴出来,为的是共同分享,共同学习,关于STP的BPDU包的封包结构,不作分析了,大家可以在网上找一大堆出来:

Linux的网桥中的STP的实现分析初步

一、STP的框架结构
STP发送的是BPDU包,该包有所有两种类型:配置和TCN(拓朴变更通知);
对于BPDU包的处理,有两种:接收和发送(废话),
对于配置类型的BPDU包的发送,它是靠定时器来完成的,参BPDU包的几个定时器参数;
对于TCP类型的BPDU包的发送,从名字可以看出来,它是当发现拓朴结构发生变更时发送的,如本机网桥配置的变化,物理接口的变动,分析其它机器变动后发出来的STP包等等。

BPDU的封包采用的是IEEE802封包(本想把封包结构的图片贴上来,找不着在哪儿上传图片)。

前面分析过, br_handle_frame函数中,当网桥开启了STP,且根据目的物理地址判断出这是一个STP包,则交给br_stp_handle_bpdu函数处理。
br_stp_handle_bpdu函数主要是判断是哪种类型的BPDU包,然后调用相关的处理函数,即:
if(type==config)
{
    br_received_config_bpdu();
}
else if(type==tcn)
{
    br_received_tcn_bpdu();
}

这是对接收到BPDU包的处理,关于config类型的BPDU包的发送,后面再分析;TCN包的发送,有一部份是在接收包处理过程中处理的(因为分析config类型的BPDU包的时候,发现拓朴变更,当然要发送TCN包了),所以这里一起来分析。

二、Config类型的BPDU包的接收处理
这个处理过程是在拆完BPDU包后,调用br_received_config_bpdu函数完成的。
还是得先交待一些理论的东西:

STP协议最终是为了在网络中生成一棵无环状的树,以期消除广播风暴以及单播数据帧对网络的影响。它始终在选举三样东东:
1、根网桥;
2、根端口;
3、“指定端口”和“指定网桥”

(这三个概念非常重要,如果你还不清楚,建议查阅相关文档先,否则下边的代码分析也无从谈起了)
然后再根据选举出来的这三个东东,确定端口的状态:阻塞、转发、学习、监听、禁用……
要选举出这三样东东,得有一个判断标志,即算法,STP的判断标准是:
1、判断根桥ID,以最小的为优;
2、判断到根桥的最小路径开销;
3、确定最小发送发BID(Sender BID)
4、确定最小的端口ID

如果前面你查阅了BPDU的封包结构,根桥ID、最小路径开销、发送方网桥的ID、端口ID这几个概念应该没有问题了,不过这里还是简单交一下:
1、根桥ID,我们配置了网桥后,用brctl命令会发现8000.XXXXXX这样一串,这就是网桥的ID号,用一标识每一个网桥,后面的XXXX一般的桥的MAC地址,这样ID值就不会重复。根桥ID,是指网络中所有网桥的ID值最小的那一个,对应的具有根桥ID的桥,当然也是网络的根桥了;

2、最小路径开销
动态路由中也类似这个概念,不过这里用的不是跳数(局域网不比广域网,不一定跳数大就慢,比如跳数小,是10M链路,跳数大的却是千兆链路),最初的开销定义为1000M/链种带宽,当然,这种方式不适用于万兆网了……所以后来又有一个新的,对每一种链路定义一个常数值——详请请查阅相关资料;

3、发送方ID
网桥之前要收敛出一个无环状拓朴,就需要互相发送BPDU包,当然需要把自己的ID告诉对方,这样对方好拿来互相比较;

4、端口ID
端口ID由优先级+端口编号组成,用于标识某个桥的某个端口,后面比较时好用。

生成树算法就是利用上述四个参数在判断,判断过程总是相同的:
1、确定根桥,桥ID最小的(即把包中的桥ID,同自己以前记录的那个最小的桥ID相比,机器加电时,总是以自己的桥ID为根桥ID)的为根桥;

2、确定最小路径开销;

3、确定最小发送方ID;

4、确定最小的端口ID:

这四步非常地重要,后面的所以比较都是这四个步骤。

有了这些概念,来看看对config类型的BPDU包的处理:

void br_received_config_bpdu(struct net_bridge_port *p, struct br_config_bpdu *bpdu)
{
struct net_bridge *br;
int was_root;

if (p->state == BR_STATE_DISABLED)
return;

br = p->br;
read_lock(&br->lock);

/*自己是根桥吗?用自己的br_ID和BPDU包中的根ID相比较*/
was_root = br_is_root_bridge(br);

/*比桥BPDU包中的信息(bpdu)和原先的对应的信息(p),如果需要更新,返回1,相同返回0,不需更新返回-1*/
if (br_supersedes_port_info(p, bpdu)) {
/*刷新自己的相关信息*/
br_record_config_information(p, bpdu);
/*进行root_bridge、port的选举*/
br_configuration_update(br);
/*设置端口状态*/
br_port_state_selection(br);

以上这一段的逻辑概念很简单:
1、把收到的BPDU包中的参数同自己原先记录的相比较,(遵循前面说的四个比较步骤),以判断是否需要进行更新——br_supersedes_port_info(p, bpdu)。
2、如果判断需要进行更新,即上述四个步骤中,有任意一项有变动,则刷新自己的保存记录:br_record_config_information(p, bpdu);
3、因为有变动,就需要改变自己的配置了:br_configuration_update(br);即前面说的,根据四步判断后选举根桥(注:根桥不是在这里选举的,前文说过,它是定时器定时发送BPDU包,然后收到的机器只需改变自己的记录即可)、根端口、指定端口;
4、设置物理端口的转发状态:br_port_state_selection

2.1 br_supersedes_port_info(p, bpdu)


/* called under bridge lock */
static int br_supersedes_port_info(struct net_bridge_port *p, struct br_config_bpdu *bpdu)
{
int t;
/*第一步*/
t = memcmp(&bpdu->root, &p->designated_root, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
/*第二步*/
if (bpdu->root_path_cost < p->designated_cost)
return 1;
else if (bpdu->root_path_cost > p->designated_cost)
return 0;
/*第三步,要同两个桥ID比:已记录的最小发送ID和自己的ID*/
t = memcmp(&bpdu->bridge_id, &p->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;

if (memcmp(&bpdu->bridge_id, &p->br->bridge_id, 8))
return 1;
/*第四步*/
if (bpdu->port_id <= p->designated_port)
return 1;

return 0;
}

2.2 br_record_config_information
如果检测到有变动,则刷新自己的记录先:
/* called under bridge lock */
static void br_record_config_information(struct net_bridge_port *p, struct br_config_bpdu *bpdu)
{
p->designated_root = bpdu->root;
p->designated_cost = bpdu->root_path_cost;
p->designated_bridge = bpdu->bridge_id;
p->designated_port = bpdu->port_id;
/*设置时间戳,关于STP的时间处理,后面来分析*/
br_timer_set(&p->message_age_timer, jiffies - bpdu->message_age);
}

p对应的四个成员的概念对照BPDU封包结构,不难理解其含义:
p->designated_root: 指定的根网桥的网桥ID
p->designated_cost : 指定的到根桥的链路花销
p->designated_bridge: 指定的发送当前BPDU包的网桥的ID
p->designated_port: 指定的发送当前BPDU包的网桥的端口的ID

2。3 br_configuration_update前面说过,根桥的选举不是在这里进行,这里进行根端口和指定端口的选举
/* called under bridge lock */
void br_configuration_update(struct net_bridge *br)
{

                br_root_selection(br);/*选举根端口*/
br_designated_port_selection(br);/*选举指定端口*/
}

2.3.1 根端口的选举br_root_selection根端口的选举同样是以上四个步骤,只是有一点小技巧:它逐个遍历桥的每一个所属端口,找出一个符合条件的,保存下来,再用下一个来与之做比较,用变量root_port 来标志:
/* called under bridge lock */
static void br_root_selection(struct net_bridge *br)
{
struct net_bridge_port *p;
int root_port;

root_port = 0;
/*获得桥的所属端口列表*/
p = br->port_list;
/*这个循环非常重要,它遍历桥的每一个端口,进行以上四步判断,找到一个,将其“保存”下来,然后再用下一个与保存的相比较,直至遍历完,找到最优的那个,这个“保存”打了引号,是因为它仅仅是记当了端口编号:root_port = p->port_no;,然后再将其传递给比较函数br_should_become_root_port*/
while (p != NULL) {
if (br_should_become_root_port(p, root_port))
root_port = p->port_no;

p = p->next;
}

br->root_port = root_port;
/*找完了还没有找到,则认为自己就是根桥……*/
if (!root_port) {
br->designated_root = br->bridge_id;
br->root_path_cost = 0;
}  
/*否则记录相应的值*/
               else {
p = br_get_port(br, root_port);
br->designated_root = p->designated_root;
br->root_path_cost = p->designated_cost + p->path_cost;
}
}

br_should_become_root_port函数用以判断端口p是否应该变成根端口,与它相比较的是原来那个根端口,函数第二个参数则为此的ID号,在函数中调用 br_get_port获取该端口:

/* called under bridge lock */
static int br_should_become_root_port(struct net_bridge_port *p, int root_port)
{
struct net_bridge *br;
struct net_bridge_port *rp;
int t;

br = p->br;
/*若当前端口是关闭状态或为一个指定端口,则不参与选举,返回*/
if (p->state == BR_STATE_DISABLED ||
    br_is_designated_port(p))
return 0;
/*在根端口的选举中,根桥是没有选举权的*/
if (memcmp(&br->bridge_id, &p->designated_root, 8) <= 0)
return 0;

/*没有指定等比较的端口ID(因为第一次它初始化为0的)*/
if (!root_port)
return 1;

/*获取待比较的根端口*/
rp = br_get_port(br, root_port);

/*又是四大步,像打蓝球*/
t = memcmp(&p->designated_root, &rp->designated_root, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;

if (p->designated_cost + p->path_cost <
    rp->designated_cost + rp->path_cost)
return 1;
else if (p->designated_cost + p->path_cost >
 rp->designated_cost + rp->path_cost)
return 0;

t = memcmp(&p->designated_bridge, &rp->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;

if (p->designated_port < rp->designated_port)
return 1;
else if (p->designated_port > rp->designated_port)
return 0;

if (p->port_id < rp->port_id)
return 1;

return 0;
}

这样,遍历完成后,根端口就被选出来了。

2。3。2 指定端口的选举br_designated_port_selection
/* called under bridge lock */
static void br_designated_port_selection(struct net_bridge *br)
{
struct net_bridge_port *p;

p = br->port_list;
while (p != NULL) {
if (p->state != BR_STATE_DISABLED &&
    br_should_become_designated_port(p))
br_become_designated_port(p);

p = p->next;
}
}
事实上这个过程与根端口的选举过程极为类似,没有分析的必要了!
走走看看开源好 Solaris vs Linux
cyliu
论坛版主
论坛版主
  • 注册日期2003-06-13
  • 最后登录2014-04-11
  • 粉丝5
  • 关注0
  • 积分1238分
  • 威望2531点
  • 贡献值0点
  • 好评度577点
  • 原创分14分
  • 专家分10分
6楼#
发布于:2007-03-15 20:57
2。3。3 端口状态选择
 /* called under bridge lock */
void br_port_state_selection(struct net_bridge *br)
{
struct net_bridge_port *p;

p = br->port_list;
while (p != NULL) {
if (p->state != BR_STATE_DISABLED) {
if (p->port_no == br->root_port) {
p->config_pending = 0;
p->topology_change_ack = 0;
br_make_forwarding(p);
} else if (br_is_designated_port(p)) {
br_timer_clear(&p->message_age_timer);
br_make_forwarding(p);
} else {
p->config_pending = 0;
p->topology_change_ack = 0;
br_make_blocking(p);
}
}

p = p->next;
}
}

函数的逻辑结构也很简单:
遍历整个桥所属端口:
while (p != NULL)
如果端口已经DISABLED,则没有判断的必要了:
p->state != BR_STATE_DISABLED

如果端口是根端口,或者是指定端口,就让让它forwarding,否则就让它blocking:

if (p->port_no == br->root_port) {
p->config_pending = 0;
p->topology_change_ack = 0;
br_make_forwarding(p);
} else if (br_is_designated_port(p)) {
br_timer_clear(&p->message_age_timer);
br_make_forwarding(p);
} else {
p->config_pending = 0;
p->topology_change_ack = 0;
br_make_blocking(p);
}

/* called under bridge lock */
static void br_make_forwarding(struct net_bridge_port *p)
{
if (p->state == BR_STATE_BLOCKING) {
printk(KERN_INFO "%s: port %i(%s) entering %s state\n",
       p->br->dev.name, p->port_no, p->dev->name, "listening");

p->state = BR_STATE_LISTENING;
br_timer_set(&p->forward_delay_timer, jiffies);
}
}

/* called under bridge lock */
static void br_make_blocking(struct net_bridge_port *p)
{
if (p->state != BR_STATE_DISABLED &&
    p->state != BR_STATE_BLOCKING) {
if (p->state == BR_STATE_FORWARDING ||
    p->state == BR_STATE_LEARNING)
br_topology_change_detection(p->br);

printk(KERN_INFO "%s: port %i(%s) entering %s state\n",
       p->br->dev.name, p->port_no, p->dev->name, "blocking");

p->state = BR_STATE_BLOCKING;
br_timer_clear(&p->forward_delay_timer);
}
}

都是设置p->state 相应状态位就可以了!!
走走看看开源好 Solaris vs Linux
游客

返回顶部