1. 程式人生 > >【轉載】網橋原理

【轉載】網橋原理

1  前言

[原文 ](https://blog.csdn.net/qq_29422251/article/details/76849261)

本文的參考分析的原始碼版本是2.6.15,我是邊學習邊總結,學習的過程中得益於linux論壇(http://linux.chinaunix.net/bbs/)上大俠們總結分析的文件,他山之石可以攻玉,學習過程中我也會邊學邊總結,開源的發展在於共享,我也拋塊磚,望能引到玉!

由於自身水平有限,且相關的參考資料較少,因此其中的結論不能保證完全正確,如果在閱讀本文的過程中發現了問題歡迎及時與作者聯絡。也希望能有機會和大家多多交流學習心得!

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也可能會作為報文的源地址或目的地址,直接參與報文的傳送與接收(從而繞過網橋)。

2.3  網橋的功能

概括來說,網橋實現最重要的兩點:

1. MAC學習:學習MAC地址,起初,網橋是沒有任何地址與埠的對應關係的,它傳送資料,還是得想HUB一樣,但是每傳送一個數據,它都會關心資料包的來源MAC是從自己的哪個埠來的,由於學習,建立地址-埠的對照表(CAM表)。

2. 報文轉發:每傳送一個數據包,網橋都會提取其目的MAC地址,從自己的地址-埠對照表(CAM表)中查詢由哪個埠把資料包傳送出去。

Linux裡面使用網橋非常簡單,僅需要做兩件事情就可以配置了。其一是在編譯核心裡把CONFIG_BRIDGECONDIG_BRIDGE_MODULE編譯選項開啟;其二是安裝brctl工具。第一步是使核心協議棧支援網橋,第二步是安裝使用者空間工具,通過一系列的ioctl呼叫來配置網橋。下面以一個相對簡單的例項來貫穿全文,以便分析程式碼。

Linux機器有4個網絡卡,分別是eth0~eth4,其中eth0用於連線外網,而eth1, eth2, eth3都連線到一臺PC機,用於配置網橋。只需要用下面的命令就可以完成網橋的配置:

Brctl addbr br0 (建立一個網橋br0, 同時在Linux核心裡面建立虛擬網絡卡br0)

Brctl addif br0 eth1

Brctl addif br0 eth2

Brctl addif br0 eth3 (分別為網橋br0新增介面eth1, eth2和eth3)

其中br0作為一個網橋,同時也是虛擬的網路裝置,它即可以用作網橋的管理埠,也可作為網橋所連線區域網的閘道器,具體情況視你的需求而定。要使用br0介面時,必需為它分配IP地址。為正常工作,PC1, PC2,PC3和br0的IP地址分配在同一個網段。

在核心,網橋是以模組的方式存在,註冊原始碼路徑:\net\brige\br.c:

4.1 初始化
 

static int __init br_init(void)
{
    br_fdb_init(); 
//網橋資料庫初始化,分配slab緩衝區



#ifdef CONFIG_BRIDGE_NETFILTER
    if (br_netfilter_init()) 
//netfilter鉤子初始化

        return 1;
#endif
    brioctl_set(br_ioctl_deviceless_stub); 
//設定ioctl鉤子函式:br_ioctl_hook

    br_handle_frame_hook = br_handle_frame;
//設定報文處理鉤子:br_ioctl_hook


    
//網橋資料庫處理鉤子

    br_fdb_get_hook = br_fdb_get;
    br_fdb_put_hook = br_fdb_put;

    
//在netdev_chain通知連結串列上註冊

    register_netdevice_notifier(&br_device_notifier);

    return 0;
}

4.2 新建網橋

前面說到通過brctl addbr br0命令建立網橋,此處使用者控制元件呼叫的brctl命令最終對應到核心中的br_ioctl_deviceless_stub處理函式:
 

int br_ioctl_deviceless_stub(unsigned int cmd, void __user *uarg)
{
    switch (cmd) {
    case SIOCGIFBR:
    case SIOCSIFBR:
        return old_deviceless(uarg);
        
    case SIOCBRADDBR: 
//新建網橋

    case SIOCBRDELBR: 
//刪除網橋

    {
        char buf[IFNAMSIZ];

        if (!capable(CAP_NET_ADMIN))
            return -EPERM;
            
        
//copy_from_user:把使用者空間的資料拷入核心空間

        if (copy_from_user(buf, uarg, IFNAMSIZ))
            return -EFAULT;

        buf[IFNAMSIZ-1] = 0;
        if (cmd == SIOCBRADDBR)
            return br_add_bridge(buf);

        return br_del_bridge(buf);
    }
    }
    return -EOPNOTSUPP;
}

在這裡,我們傳入的cmd為SIOCBRADDBR.轉入br_add_bridge(buf)中進行: 

int br_add_bridge(const char *name)
{
    struct net_device *dev;
    int ret;
    
    
//為虛擬橋新建一個net_device

    dev = new_bridge_dev(name);
    if (!dev) 
        return -ENOMEM;

    rtnl_lock();
    
//由核心確定介面名字,例如eth0 eth1等

    if (strchr(dev->name, '%')) {
        ret = dev_alloc_name(dev, dev->name);
        if (ret < 0)
            goto err1;
    }
    
//向核心註冊此網路裝置

    ret = register_netdevice(dev);
    if (ret)
        goto err2;

    
/* network device kobject is not setup until
     * after rtnl_unlock does it's hotplug magic.
     * so hold reference to avoid race.
     */

    dev_hold(dev);
    rtnl_unlock();
    
    
//在sysfs中建立相關資訊

    ret = br_sysfs_addbr(dev);
    dev_put(dev);

    if (ret) 
        unregister_netdev(dev);
 out:
    return ret;

 err2:
    free_netdev(dev);
 err1:
    rtnl_unlock();
    goto out;
}

網橋是一個虛擬的裝置,它的註冊跟實際的物理網路設備註冊是一樣的。我們關心的是網橋對應的net_device結構是什麼樣的,繼續跟蹤進new_bridge_dev:

static struct net_device *new_bridge_dev(const char *name)
{
    struct net_bridge *br;
    struct net_device *dev;

    
//分配net_device

    dev = alloc_netdev(sizeof(struct net_bridge), name,
             br_dev_setup);
    
    if (!dev)
        return NULL;
    
//網橋的私區結構為net_bridge

    br = netdev_priv(dev);
    
//私區結構中的dev欄位指向裝置本身

    br->dev = dev;

    spin_lock_init(&br->lock);
    
//佇列初始化。在port_list中儲存了這個橋上的埠列表

    INIT_LIST_HEAD(&br->port_list);
    spin_lock_init(&br->hash_lock);

    
//下面這部份程式碼跟stp協議相關,我們暫不關心

    br->bridge_id.prio[0] = 0x80;
    br->bridge_id.prio[1] = 0x00;
    memset(br->bridge_id.addr, 0, ETH_ALEN);

    br->stp_enabled = 0;
    br->designated_root = br->bridge_id;
    br->root_path_cost = 0;
    br->root_port = 0;
    br->bridge_max_age = br->max_age = 20 * HZ;
    br->bridge_hello_time = br->hello_time = 2 * HZ;
    br->bridge_forward_delay = br->forward_delay = 15 * HZ;
    br->topology_change = 0;
    br->topology_change_detected = 0;
    br->ageing_time = 300 * HZ;
    INIT_LIST_HEAD(&br->age_list);

    br_stp_timer_init(br);

    return dev;
}

在br_dev_setup中還做了一些另外在函式指標初始化: 

void br_dev_setup(struct net_device *dev)
{
    
//將橋的MAC地址設為零

    memset(dev->dev_addr, 0, ETH_ALEN);
     
//初始化dev的部分函式指標,因為目前網橋裝置主適用於以及網,

     
//乙太網的部分功能對它也適用

    ether_setup(dev);
    
    
//設定裝置的ioctl函式為br_dev_ioctl

    dev->do_ioctl = br_dev_ioctl;
    
//網橋與一般網絡卡不同,網橋統一統計它的資料包和位元組數等資訊

    dev->get_stats = br_dev_get_stats;
    
// 網橋介面的資料包傳送函式,真實裝置要向外傳送資料時,是通過網絡卡向外傳送資料 

    
// 而該網橋裝置要向外傳送資料時,它的處理邏輯與網橋其它介面的基本一致。 

    dev->hard_start_xmit = br_dev_xmit;
    dev->open = br_dev_open;
    dev->set_multicast_list = br_dev_set_multicast_list;
    dev->change_mtu = br_change_mtu;
    dev->destructor = free_netdev;
    SET_MODULE_OWNER(dev);
    dev->stop = br_dev_stop;
    dev->tx_queue_len = 0;
    dev->set_mac_address = NULL;
    dev->priv_flags = IFF_EBRIDGE;
}

僅僅建立網橋,還是不夠的。實際應用中的網橋需要新增實際的埠(即物理介面),如例子中的eth1, eth2等。應用程式在使用ioctl來為網橋增加物理介面,對應核心函式br_dev_ioctl的程式碼和分析如下: 


int br_dev_ioctl(struct net_device *dev, struct ifreq *rq, int cmd)
{
    struct net_bridge *br = netdev_priv(dev);

    switch(cmd) {
    case SIOCDEVPRIVATE:
        return old_dev_ioctl(dev, rq, cmd);

    case SIOCBRADDIF: 
//新增

    case SIOCBRDELIF: 
//刪除

        
//同一處理函式,預設為新增

        return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF);

    }

    pr_debug("Bridge does not support ioctl 0x%x\n", cmd);
    return -EOPNOTSUPP;
}

下面分析具體的新增刪除函式add_del_if:


static int add_del_if(struct net_bridge *br, int ifindex, int isadd)
{
    struct net_device *dev;
    int ret;

    if (!capable(CAP_NET_ADMIN))
        return -EPERM;

    dev = dev_get_by_index(ifindex);
    if (dev == NULL)
        return -EINVAL;
    
    if (isadd)
        ret = br_add_if(br, dev);
    else
        ret = br_del_if(br, dev);

    dev_put(dev);
    return ret;
}


對應的新增刪除函式分別為:br_add_if, br_del_if;

br_add_if:

int br_add_if(struct net_bridge *br, struct net_device *dev)
{
    struct net_bridge_port *p;
    int err = 0;

    /*--Kernel僅支援乙太網網橋--*/
    if (dev->flags & IFF_LOOPBACK || dev->type != ARPHRD_ETHER)
        return -EINVAL;

    
/*--把網橋介面當作物理介面加入到另一個網橋中,是不行的,
        邏輯和程式碼上都會出現 loop--*/

    if (dev->hard_start_xmit == br_dev_xmit)
        return -ELOOP;

    /*--該物理介面已經繫結到另一個網橋了--*/
    if (dev->br_port != NULL)
        return -EBUSY;

    /*--為該介面建立一個網橋埠資料,並初始化好該埠的相關資料--*/
    if (IS_ERR(= new_nbp(br, dev, br_initial_port_cost(dev))))
        return PTR_ERR(p);
        
    
/*--將該介面的實體地址寫入到 MAC-埠對映表中,
        該MAC是屬於網橋內部埠的固定MAC地址, 
        它在fdb中的記錄是固定的,不會失效(agged)--*/

     if ((err = br_fdb_insert(br, p, dev->dev_addr)))
        destroy_nbp(p);
     /*--新增相應的系統檔案資訊--*/
    else if ((err = br_sysfs_addif(p)))
        del_nbp(p);
    else {
        
/*--開啟該介面的混雜模式,網橋中的各個埠必須處於混雜模式,
            網橋才能正確工作--*/

        dev_set_promiscuity(dev, 1);
        
        /*--加到埠列表--*/
        list_add_rcu(&p->list, &br->port_list);

        /*--STP相關設定-*/
        spin_lock_bh(&br->lock);
        br_stp_recalculate_bridge_id(br);
        br_features_recompute(br);
        if ((br->dev->flags & IFF_UP) 
         && (dev->flags & IFF_UP) && netif_carrier_ok(dev))
            br_stp_enable_port(p);
        spin_unlock_bh(&br->lock);
        
        /*--設定裝置的mtu--*/
        dev_set_mtu(br->dev, br_min_mtu(br));
    }

    return err;
}


br_del_if:

int br_del_if(struct net_bridge *br, struct net_device *dev)
{
    struct net_bridge_port *= dev->br_port;
    
    if (!|| p->br != br) 
        return -EINVAL;

    br_sysfs_removeif(p);
    del_nbp(p);

    spin_lock_bh(&br->lock);
    br_stp_recalculate_bridge_id(br);
    br_features_recompute(br);
    spin_unlock_bh(&br->lock);

    return 0;
}


網橋最主要有三個資料結構:struct net_bridge,struct net_bridge_port,struct net_bridge_fdb_entry,他們之間的關係如下圖:

展開來如下圖:

說明:

1.       其中最左邊的net_device是一個代表網橋的虛擬裝置結構,它關聯了一個net_bridge結構,這是網橋裝置所特有的資料結構。

2.       net_bridge結構中,port_list成員下掛一個連結串列,連結串列中的每一個節點(net_bridge_port結構)關聯到一個真實的網口裝置的net_device。網口裝置也通過其br_port指標做反向的關聯(那麼顯然,一個網口最多隻能同時被繫結到一個網橋)。

3.       net_bridge結構中還維護了一個hash表,是用來處理地址學習的。當網橋準備轉發一個報文時,以報文的目的Mac地址為key,如果可以在 hash表中索引到一個net_bridge_fdb_entry結構,通過這個結構能找到一個網口裝置的net_device,於是報文就應該從這個網口轉發出去;否則,報文將從所有網口轉發。

各個結構體具體內容如下:

struct net_bridge

struct net_bridge
{
    spinlock_t            lock; 
//讀寫鎖

    
//網橋所有埠的連結串列,其中每個元素都是一個net_bridge_port結構

    struct list_head        port_list; 
    struct net_device        *dev; 
//網橋對應的裝置

    struct net_device_stats        statistics; 
//網橋對應的虛擬網絡卡的統計資料

    spinlock_t            hash_lock; 
//hash表的鎖

    
/*--CAM: 儲存forwarding database的一個hash連結串列(這個也就是地址學習的東東,
    所以通過hash能 快速定位),這裡每個元素都是一個net_bridge_fsb_entry結構--*/

    struct hlist_head        hash[BR_HASH_SIZE]; 
    struct list_head        age_list;

    /* STP */ 
//與stp 協議對應的資料

    bridge_id            designated_root;
    bridge_id            bridge_id;
    u32                root_path_cost;
    unsigned long            max_age;
    unsigned long            hello_time;
    unsigned long            forward_delay;
    unsigned long            bridge_max_age;
    unsigned long            ageing_time;
    unsigned long            bridge_hello_time;
    unsigned long            bridge_forward_delay;

    u16                root_port;
    unsigned char            stp_enabled;
    unsigned char            topology_change;
    unsigned char            topology_change_detected;
    
//stp要用的一些定時器列表。

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


2.  struct net_bridge_port

struct net_bridge_port
{
    struct net_bridge        *br; 
//從屬於的網橋裝置

    struct net_device        *dev;
//表示連結到這個埠的物理裝置

    struct list_head        list;

    /* STP */ 
//stp相關的一些引數。

    u8                priority;
    u8                state;
    u16                port_no; 
//本埠在網橋中的編號

    unsigned char            topology_change_ack;
    unsigned char            config_pending;
    port_id                port_id;
    port_id                designated_port;
    bridge_id            designated_root;
    bridge_id            designated_bridge;
    u32                path_cost;
    u32                designated_cost;
    
//埠定時器,也就是stp控制超時的一些定時器列表

    struct timer_list        forward_delay_timer;
    struct timer_list        hold_timer;
    struct timer_list        message_age_timer;
    struct kobject            kobj;
    struct rcu_head            rcu;
}

3. struct net_bridge_fdb_entry

struct net_bridge_fdb_entry
{
    struct hlist_node        hlist;
    
//橋的埠(最主要的兩個域就是這個域和下面的mac地址域) 

    struct net_bridge_port *dst;
    
    struct rcu_head            rcu; 
//當使用RCU策略,才用到

    
    atomic_t                use_count; 
//引用計數

    unsigned long            ageing_timer; 
//MAC超時時間

    mac_addr                addr; 
//mac地址。    

    
    unsigned char            is_local; 
//是否為本機的MAC地址

    unsigned char            is_static; 
//是否為靜態MAC地址

}

這裡所說的網橋資料庫指的是CAM表,即struct net_bridge結構中的hash表,資料庫的維護對應的是對結構struct net_bridge_fdb_entry的操作;

眾所周知,網橋需要維護一個MAC地址-埠對映表,埠是指網橋自身提供的埠,而MAC地址是指與埠相連的另一端的MAC地址。當網橋收到一個報文時,先獲取它的源MAC,更新資料庫,然後讀取該報文的目標MAC地址,查詢該資料庫,如果找到,根據找到條目的埠進行轉發;否則會把資料包向除入口埠以外的所有埠轉發。

資料庫使用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);
}

當網橋收到一個數據包時,它會獲取該資料的源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();
}


在更新函式裡面已為某一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;
}


查詢分兩種:一種是資料項更新時候的查詢,另一種是轉發報文時候查詢,兩者區別是轉發時查詢需要判斷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);
}


橋建立時設定一個定時器,迴圈檢測,如果發現有過期的MAC,則清除對應的資料項,MAC地址過期清除由函式br_fdb_cleanup實現:

/*--定時器迴圈檢查MAC地址是否過期
    定時器在橋初始化中定義開啟--*/

void br_fdb_cleanup(unsigned long _data)
{
    struct net_bridge *br = (struct net_bridge *)_data;
    unsigned long delay = hold_time(br);/*--獲取MAC地址可保留時間--*/
    int i;

    spin_lock_bh(&br->hash_lock);
    for (= 0; i < BR_HASH_SIZE; i++) {
        struct net_bridge_fdb_entry *f;
        struct hlist_node *h, *n;

        /*--如果該地址不是靜態的,並且已經過期,則從資料庫中清除該MAC對映--*/
        hlist_for_each_entry_safe(f, h, n, &br->hash[i], hlist) {
            if (!f->is_static && 
             time_before_eq(f->ageing_timer + delay, jiffies)) 
                fdb_delete(f);
        }
    }
    spin_unlock_bh(&br->hash_lock);
    
    /*--更新檢查定時器--*/
    mod_timer(&br->gc_timer, jiffies + HZ/10);
}

網橋處理包遵循以下幾條原則:

1.  在一個介面上接收的包不會再在那個介面上傳送這個資料包;

2.  每個接收到的資料包都要學習其源地址;

3.  如果資料包是多播或廣播包,則要在同一個網段中除了接收埠外的其他所有埠傳送這個資料包,如果上層協議棧對多播包感興趣,則需要把資料包提交給上層協議棧;

4.  如果資料包的目的MAC地址不能再CAM表中找到,則要在同一個網段中除了接收埠外的其他所有埠傳送這個資料包;

5.  如果能夠在CAM表中查詢到目的MAC地址,則在特定的埠上傳送這個資料包,如果傳送埠和接收埠是同一埠則不傳送;

網橋在整個網路子系統中處理可用下列簡圖說明: 

網路資料包在軟終端處理時會進行網橋部分處理,大致的處理流程如下(處理函式呼叫鏈):

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 =