Linux 內核網橋源碼分析


Linux網橋源碼的實現

轉自: Linux二層網絡協議

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的時候提高效率,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協議的相關定義*/
        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處理,數據傳送。

第二部份,CAM表的學習與查找
前一章說過,CAM表的學習,是通過br_fdb_insert函數,而查找,則是調用了br_forward函數

1、CAM表的結構
每一個地址-端口對應的項稱為fdb項,內核中使用鏈表來組織fdb,它是一個struct net_bridge_fdb_entry
類型:

#define BR_HASH_BITS 8
#define BR_HASH_SIZE (1 << BR_HASH_BITS)

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

內核中,整個CAM表是用br->hash[hash_value]這個數組來存儲的,其中hash_value是根據源MAC地址進行hash運算得出的一個值,
這樣,br->hash[hash]就指向了此源MAC地址對應的fdb項所在的鏈表的首部。這樣說可能有點復雜,可用下圖來表示:
br->hash[hash_0]->fdb1->fdb2->fdb3……
br->hash[hash_1]->fdb1->fdb2->fdb3……
br->hash[hash_2]->fdb1->fdb2->fdb3……
br->hash[hash_3]->fdb1->fdb2->fdb3……
……
其中的hash_0、hash_1……是通過對源MAC地址進行hash運算求出的。so easy……

2、br_fdb_insert
/*
* Function:br_fdb_insert
* Purpose:網橋CAM表的學習,查詢新收到的源MAC-端口在原來表中是否有變化,以便更新CAM表
* Arguments:
*         struct net_bridge *br=>當前網橋
*        struct net_bridge_port *source=>源端口
*        unsigned char *addr=>源地址
*        int is_local=>是否為本地
* Return:
*        void
*/
void br_fdb_insert(struct net_bridge *br,
                   struct net_bridge_port *source,
                   unsigned char *addr,
                   int is_local)
{
        struct net_bridge_fdb_entry *fdb;
        int hash;

        /*
         * CAM表是一個數組,每個數組元素又是一個鏈表,這里根據源地址,求對應的hash值,也就是當前源地址在表中的對應的編號id,
         * 這樣,就可以通過br->hash[id]來訪問該地址對應的fdb項的鏈表了。
        */
        hash = br_mac_hash(addr);

        write_lock_bh(&br->hash_lock);                /*加鎖*/
        fdb = br->hash[hash];                        /*取得當前源地址對應的fdb項鏈表*/
        
        /*如果鏈表不為空,則遍歷該鏈表,找到地址匹配的項,然后替換它*/
        while (fdb != NULL) {
                if (!fdb->is_local &&
                    !memcmp(fdb->addr.addr, addr, ETH_ALEN)) {
                        __fdb_possibly_replace(fdb, source, is_local);
                        write_unlock_bh(&br->hash_lock);
                        return;
                }

                fdb = fdb->next_hash;
        }

        /*如果鏈表為空,則為新的fdb項分配空間,構建fdb項,然后構建hash 鏈表*/
        fdb = kmalloc(sizeof(*fdb), GFP_ATOMIC);
        if (fdb == NULL) {
                write_unlock_bh(&br->hash_lock);
                return;
        }

        memcpy(fdb->addr.addr, addr, ETH_ALEN);
        atomic_set(&fdb->use_count, 1);
        fdb->dst = source;
        fdb->is_local = is_local;
        fdb->is_static = is_local;
        fdb->ageing_timer = jiffies;

        /*因為本項源地址對應的hash值已計算出來了,則直接將本項給當前橋br*/
        __hash_link(br, fdb, hash);

        write_unlock_bh(&br->hash_lock);                /*解鎖*/
}

這個函數中涉及到三個重要函數:
1、br_mac_hask:計算地址對應的hash值;
2、__fdb_possibly_replace:替換fdb項;
3、__hash_link:將當前項fdb插入hash表中;

A、br_mac_hask

函數用於計算地址對應的hash值。
將MAC地址逐字節左移兩位,然后與下一字節值求異或,完成之后,再將高8位和低8位再異或,最后使用return x & (BR_HASH_SIZE - 1);將hash值限定在指定范圍之內。
static __inline__ int br_mac_hash(unsigned char *mac)
{
        unsigned long x;

        x = mac[0];
        x = (x << 2) ^ mac[1];
        x = (x << 2) ^ mac[2];
        x = (x << 2) ^ mac[3];
        x = (x << 2) ^ mac[4];
        x = (x << 2) ^ mac[5];

        x ^= x >> 8;

        /*
         * #define BR_HASH_BITS 8
         * #define BR_HASH_SIZE (1 << BR_HASH_BITS)
         */
        return x & (BR_HASH_SIZE - 1);
}

B、__fdb_possibly_replace
因為在鏈表的循環查找中,發現當前源地址已在表項中存在,所以,需要更新它,這是一個單純的替換操作:

static __inline__ void __fdb_possibly_replace(struct net_bridge_fdb_entry *fdb,
                                              struct net_bridge_port *source,
                                              int is_local)
{
        if (!fdb->is_static || is_local) {
                fdb->dst = source;                        /*更新當前地址所對應的端口*/
                fdb->is_local = is_local;
                fdb->is_static = is_local;
                fdb->ageing_timer = jiffies;
        }
}

C、__hash_link

函數將待插入項ent插入到hash值對應的橋的br->hash[hash]的鏈表的第一個項

static __inline__ void __hash_link(struct net_bridge *br,
                                   struct net_bridge_fdb_entry *ent,
                                   int hash)
{
        /*讓ent->next指向鏈表首部,這樣后邊br->hash[hash]=ent,於是鏈首指針就指向ent了*/
        ent->next_hash = br->hash[hash];
        if (ent->next_hash != NULL)
                ent->next_hash->pprev_hash = &ent->next_hash;                /*回指上一個元素*/
        br->hash[hash] = ent;
        ent->pprev_hash = &br->hash[hash];                /*ent->pprev回指鏈首指針*/
}

3、br_forward
/*
* Function:br_fdb_insert
* Purpose:網橋CAM表的查找,查找待發送數據包目的MAC地址對應的fdb 表項
* Arguments:
*         struct net_bridge *br=>當前網橋
*        unsigned char *addr=>待查找地址
* Return:
*        net_bridge_fdb_entry *=>查找到的fdb項,未查到則為NULL
*/
struct net_bridge_fdb_entry *br_fdb_get(struct net_bridge *br, unsigned char *addr)
{
        struct net_bridge_fdb_entry *fdb;

        read_lock_bh(&br->hash_lock);                /*加鎖*/
        fdb = br->hash[br_mac_hash(addr)];        /*計算地址對應的hash值*/
        
        /*遍歷鏈表,查找與地址相匹配的fdb項*/
        while (fdb != NULL) {
                if (!memcmp(fdb->addr.addr, addr, ETH_ALEN)) {
                        if (!has_expired(br, fdb)) {
                                atomic_inc(&fdb->use_count);
                                read_unlock_bh(&br->hash_lock);
                                return fdb;
                        }

                        read_unlock_bh(&br->hash_lock);
                        return NULL;
                }

                fdb = fdb->next_hash;
        }

        read_unlock_bh(&br->hash_lock);                /*解鎖*/
        return NULL;
}

這樣,網橋中最重要的學習/查表的全過程就這樣了,如果沒有STP,那么全過程就是這樣,當然,如果網橋
打開了STP開關,則網橋需要進行STP的相關處理,STP的處理,是網橋中的一個重要部份,將在下一章進行分析。 

第三部份,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, ;
        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, ;
        if (t < 0)
                return 1;
        else if (t > 0)
                return 0;

        if (memcmp(&bpdu->bridge_id, &p->br->bridge_id, )
                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,  <= 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, ;
        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, ;
        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;
        }
}
事實上這個過程與根端口的選舉過程極為類似,沒有分析的必要了!

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 相應狀態位就可以了!! 

三、選舉完成之后
實在不會取名字了,前面分析了br_received_config_bpdu中前面的判斷、刷新、選舉、設置端口狀態的過程,然而,如果橋認為當前這個BPDU是一個“最優的”(即符合前面判斷四步中的某一步),所作的動作不止於此:
1、如果因為這個BPDU導致拓朴變化了,如自己以前是根橋,現在不是了,需要發送TCN包,進行通告;
2、需要把這個BPDU包繼續轉發下去(如果自己收到數據的端口是根端口的話,那么就有可能有許多交換機(網橋)串在自己的指定端口下邊,總得把這個包能過指定端口再發給它們吧,否則交換機就不叫交換機了)

指下來繼續看代碼:
/*前面說的第1步*/
                     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);
                        }
                }
/*前面說的第2步*/
                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);
                }

tcn包的發送,呆會單獨來分析,先來看br_config_bpdu_generation函數,這個函數也很簡單:遍歷橋的所有端口,如果是指定端口,就發送一個config 類型的BPDU包:
/* called under bridge lock */
void br_config_bpdu_generation(struct net_bridge *br)
{
        struct net_bridge_port *p;

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

                p = p->next;
        }
}
然后就是層層函數調用,組包,最終是調用dev_queue_xmit函數發送出去的。

如果收到這個BPDU包,不是“最優”的,而接收數據包的接口不是根端口,直接將轉發出去就可以了,起個中繼的作用:
else if (br_is_designated_port(p))
{                
                br_reply(p);                
}
br_reply同樣調用了br_transmit_config函數

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM