原文:https://blog.csdn.net/qq_29422251/article/details/76849261
----------------------------------
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收到。
linux內核支持網口的橋接(目前只支持以太網接口)。但是與單純的交換機不同,交換機只是一個二層設備,對於接收到的報文,要么轉發、要么丟棄。小型 的交換機里面只需要一塊交換芯片即可,並不需要CPU。而運行着linux內核的機器本身就是一台主機,有可能就是網絡報文的目的地。其收到的報文除了轉 發和丟棄,還可能被送到網絡協議棧的上層(網絡層),從而被自己消化。
linux內核是通過一個虛擬的網橋設備來實現橋接的。這個虛擬設備可以綁定若干個以太網接口設備,從而將它們橋接起來。如下圖(摘自ULNI):

網橋設備br0綁定了eth0和eth1。對於網絡協議棧的上層來說,只看得到br0,因為橋接是在數據鏈路層實現的,上層不需要關心橋接的細節。於是協 議棧上層需要發送的報文被送到br0,網橋設備的處理代碼再來判斷報文該被轉發到eth0或是eth1,或者兩者皆是;反過來,從eth0或從eth1接 收到的報文被提交給網橋的處理代碼,在這里會判斷報文該轉發、丟棄、或提交到協議棧上層。
而有時候eth0、eth1也可能會作為報文的源地址或目的地址,直接參與報文的發送與接收(從而繞過網橋)。
概括來說,網橋實現最重要的兩點:
1. MAC學習:學習MAC地址,起初,網橋是沒有任何地址與端口的對應關系的,它發送數據,還是得想HUB一樣,但是每發送一個數據,它都會關心數據包的來源MAC是從自己的哪個端口來的,由於學習,建立地址-端口的對照表(CAM表)。
2. 報文轉發:每發送一個數據包,網橋都會提取其目的MAC地址,從自己的地址-端口對照表(CAM表)中查找由哪個端口把數據包發送出去。
3 網橋的配置
在Linux里面使用網橋非常簡單,僅需要做兩件事情就可以配置了。其一是在編譯內核里把CONFIG_BRIDGE或CONDIG_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(p = 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 *p = dev->br_port; if (!p || 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; } |
5網橋數據結構
網橋最主要有三個數據結構: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 (i = 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); } |
7 網橋數據包的處理流程
網橋處理包遵循以下幾條原則:
1. 在一個接口上接收的包不會再在那個接口上發送這個數據包;
2. 每個接收到的數據包都要學習其源地址;
3. 如果數據包是多播或廣播包,則要在同一個網段中除了接收端口外的其他所有端口發送這個數據包,如果上層協議棧對多播包感興趣,則需要把數據包提交給上層協議棧;
4. 如果數據包的目的MAC地址不能再CAM表中找到,則要在同一個網段中除了接收端口外的其他所有端口發送這個數據包;
5. 如果能夠在CAM表中查詢到目的MAC地址,則在特定的端口上發送這個數據包,如果發送端口和接收端口是同一端口則不發送;
網橋在整個網絡子系統中處理可用下列簡圖說明:

網絡數據包在軟終端處理時會進行網橋部分處理,大致的處理流程如下(處理函數調用鏈):

7.1 netif_receive_skb
netif_recerve_skb函數主要做三件事情:
1. 如果有抓包程序(socket)需要skb,則將skb復制給他們;
2. 處理橋接,即如果開啟了網橋,進行網橋處理;
3. 將skb交給網絡層;
int netif_receive_skb(struct sk_buff *skb) { struct packet_type *ptype, *pt_prev; struct net_device *orig_dev; int ret = NET_RX_DROP; unsigned short type;
/* if we've gotten here through NAPI, check netpoll */ if (skb->dev->poll && netpoll_rx(skb)) return NET_RX_DROP;
if (!skb->tstamp.off_sec) net_timestamp(skb);
if (!skb->input_dev) skb->input_dev = skb->dev;
orig_dev = skb_bond(skb);
__get_cpu_var(netdev_rx_stat).total++;
skb->h.raw = skb->nh.raw = skb->data; skb->mac_len = skb->nh.raw - skb->mac.raw;
pt_prev = NULL;
rcu_read_lock();
#ifdef CONFIG_NET_CLS_ACT if (skb->tc_verd & TC_NCLS) { skb->tc_verd = CLR_TC_NCLS(skb->tc_verd); goto ncls; } #endif
/*--yangxh mark: 當網絡設備收到網絡數據包時,最終會在軟件中斷環境里調用此函數 檢查該數據包是否有packet socket來接收該包,如果有則往該socket 拷貝一份,由deliver_skb來完成。 --*/ list_for_each_entry_rcu(ptype, &ptype_all, list) { if (!ptype->dev || ptype->dev == skb->dev) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } }
#ifdef CONFIG_NET_CLS_ACT if (pt_prev) { ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = NULL; /* noone else should process this after*/ } else { skb->tc_verd = SET_TC_OK2MUNGE(skb->tc_verd); }
ret = ing_filter(skb);
if (ret == TC_ACT_SHOT || (ret == TC_ACT_STOLEN)) { kfree_skb(skb); goto out; }
skb->tc_verd = 0; ncls: #endif
handle_diverter(skb);
/*-- 先試着將該數據包讓網橋函數來處理,如果該數據包的入口接口確實是網橋接口, 則按網橋方式來處理,如果不是網橋接口的數據包,則不應該讓網橋來處理 --*/ if (handle_bridge(&skb, &pt_prev, &ret, orig_dev)) goto out;
/*--對該數據包轉發到它L3協議的處理函數--*/ type = skb->protocol; list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) { if (ptype->type == type && (!ptype->dev || ptype->dev == skb->dev)) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } }
if (pt_prev) { ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev); } else { kfree_skb(skb); /* Jamal, now you will not able to escape explaining * me how you were going to use this. :-) */ ret = NET_RX_DROP; }
out: rcu_read_unlock(); return ret; } |
7.2 Br_handle_frame
1. 如果skb的目的Mac地址與接收該skb的網口的Mac地址相同,則結束橋接處理過程(返回到net_receive_skb函數后,這個skb會最終 被提交給網絡層);
2. 否則,調用到br_handle_frame_finish函數將報文轉發,然后釋放skb(返回到net_receive_skb函數后,這個skb就 不會往網絡層提交了);
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; /*--網橋狀態為disable,返回錯誤,丟棄數據包--*/ if (p->state == BR_STATE_DISABLED) goto err; /*--源MAC地址為非法,返回錯誤,丟棄數據包--*/ if (!is_valid_ether_addr(eth_hdr(skb)->h_source)) goto err; /*--如果網橋狀態處於學習狀態,則更新數據庫--*/ if (p->state == BR_STATE_LEARNING) br_fdb_update(p->br, p, eth_hdr(skb)->h_source);
/*--如果是STP的BPDU數據包,則進入STP處理--*/ if (p->br->stp_enabled && !memcmp(dest, bridge_ula, 5) && !(dest[5] & 0xF0)) { if (!dest[5]) { NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev, NULL, br_stp_handle_bpdu); return 1; } } else if (p->state == BR_STATE_FORWARDING) { /*--如果該接口處於Forwarding狀態,並且該報文必需要走L3層進行轉發, 則直接返回--*/ if (br_should_route_hook) { if (br_should_route_hook(pskb)) return 0; skb = *pskb; dest = eth_hdr(skb)->h_dest; }
/*-- 當用內核創建一個網橋的同時也會創建一個虛擬的網絡設備,它的名字 為網橋的名字,保存在p->br->dev指針里。P->br->dev和port_list里面的 接口共同組成一個網橋。如果該報文是要發往此接,則標記skb->pkt_type為 PACKET_HOST。因為報文最終是要發送到p->br->dev的輸送隊列里面, 正如一般的網卡驅動程序將數據包送往到某個net_device的輸入隊列一樣, 這樣bridge功能充當了虛擬網卡(如例子中的br0)驅動, 應當設置skb->pkt_type為PACKET_HOST, 表明數據包是要發送該接口,而非是因為打開混雜模式而接收到的。 --*/ 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; }
|
7.3Br_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 = skb->dev->br_port; struct net_bridge *br = p->br; struct net_bridge_fdb_entry *dst; int passedup = 0;
/*-- 對所有報的源MAC地址進行學習,這是網橋的特點之一, 通過對源地址的學習來建立MAC地址到端口的映射。 --*/ /* insert into forwarding database after filtering to avoid spoofing */ br_fdb_update(p->br, p, eth_hdr(skb)->h_source); /*--如果網橋的虛擬網卡處於混雜模式,那么每個接收到的數據包都需要克隆一份送到 AF_PACKET協議處理體(網絡軟中斷函數net_rx_action中ptype_all鏈的處理)--*/ if (br->dev->flags & IFF_PROMISC) { struct sk_buff *skb2; /*--skb2非空,表明要發往本機,br_pass_frame_up函數完成發往本機的工作--*/ skb2 = skb_clone(skb, GFP_ATOMIC); if (skb2 != NULL) { passedup = 1; br_pass_frame_up(br, skb2); } } if (dest[0] & 1) { /*--此報文是廣播或組播報文, 由br_flood_forward函數把報文向所有端口轉發出去 如果本地協議棧已經發過了,則算了,不再發送--*/ br_flood_forward(br, skb, !passedup); if (!passedup) br_pass_frame_up(br, skb); goto out; }
/*--__br_fdb_get函數先查MAC-端口映射表,表中每項是通過結構 struct net_bridge_fdb_entry來描述的,這一步是網橋的關鍵。 這個報文應從哪個接口轉發出去就看它了。 如果這個報文應發往本機,那么skb置空。不需要再轉發了 因為發往本機接口從邏輯上來說本身就是一個轉發,后續有上層協議棧處理 --*/ 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; } /*--找到MAC映射,則發往對應的目的端口--*/ if (dst != NULL) { br_forward(dst->dst, skb); goto out; } /*--dst==NULL,沒有賺到映射,則廣播--*/ br_flood_forward(br, skb, 0);
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_HOOK在NF_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) 單播但找不到端口映射的,則除了接收端口外其余端口都需要轉發;
8 參考文獻
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