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函數